✨ Features: - Modern tourism website with responsive design - AdminJS admin panel with image editor integration - PostgreSQL database with comprehensive schema - Docker containerization - Image upload and gallery management 🛠 Tech Stack: - Backend: Node.js + Express.js - Database: PostgreSQL 13+ - Frontend: HTML/CSS/JS with responsive design - Admin: AdminJS with custom components - Deployment: Docker + Docker Compose - Image Processing: Sharp with optimization 📱 Admin Features: - Routes/Tours management (city, mountain, fishing) - Guides profiles with specializations - Articles and blog system - Image editor with upload/gallery/URL options - User management and authentication - Responsive admin interface 🎨 Design: - Korean tourism focused branding - Mobile-first responsive design - Custom CSS with modern aesthetics - Image optimization and gallery - SEO-friendly structure 🔒 Security: - Helmet.js security headers - bcrypt password hashing - Input validation and sanitization - CORS protection - Environment variables
690 lines
21 KiB
JavaScript
690 lines
21 KiB
JavaScript
/**
|
||
* Image Editor Modal Component
|
||
* Предоставляет интерфейс для загрузки, обрезки и редактирования изображений
|
||
*/
|
||
|
||
class ImageEditor {
|
||
constructor(options = {}) {
|
||
this.options = {
|
||
targetFolder: 'routes',
|
||
aspectRatio: null, // null = свободная обрезка, или например 16/9
|
||
maxWidth: 1200,
|
||
maxHeight: 800,
|
||
...options
|
||
};
|
||
|
||
this.modal = null;
|
||
this.canvas = null;
|
||
this.ctx = null;
|
||
this.image = null;
|
||
this.imageData = null;
|
||
this.cropBox = null;
|
||
this.isDragging = false;
|
||
this.lastMousePos = { x: 0, y: 0 };
|
||
this.rotation = 0;
|
||
this.flipHorizontal = false;
|
||
this.flipVertical = false;
|
||
|
||
this.onSave = options.onSave || (() => {});
|
||
this.onCancel = options.onCancel || (() => {});
|
||
|
||
this.createModal();
|
||
}
|
||
|
||
createModal() {
|
||
// Создаем модальное окно
|
||
const modalHTML = `
|
||
<div class="image-editor-overlay">
|
||
<div class="image-editor-modal">
|
||
<div class="image-editor-header">
|
||
<h3>Редактор изображений</h3>
|
||
<button class="close-btn">×</button>
|
||
</div>
|
||
|
||
<div class="image-editor-body">
|
||
<!-- Область загрузки -->
|
||
<div class="upload-area" id="uploadArea">
|
||
<div class="upload-content">
|
||
<div class="upload-icon">📷</div>
|
||
<p>Перетащите изображение сюда или <button class="btn-link" id="selectFileBtn">выберите файл</button></p>
|
||
<input type="file" id="fileInput" accept="image/*" style="display: none;">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Область редактирования -->
|
||
<div class="editor-area" id="editorArea" style="display: none;">
|
||
<div class="editor-toolbar">
|
||
<button class="tool-btn" id="rotateLeftBtn" title="Повернуть влево">↺</button>
|
||
<button class="tool-btn" id="rotateRightBtn" title="Повернуть вправо">↻</button>
|
||
<button class="tool-btn" id="flipHorizontalBtn" title="Отразить горизонтально">⟷</button>
|
||
<button class="tool-btn" id="flipVerticalBtn" title="Отразить вертикально">↕</button>
|
||
<button class="tool-btn" id="resetCropBtn" title="Сбросить обрезку">⌕</button>
|
||
</div>
|
||
|
||
<div class="canvas-container">
|
||
<canvas id="editorCanvas"></canvas>
|
||
<div class="crop-overlay" id="cropOverlay">
|
||
<div class="crop-box" id="cropBox">
|
||
<div class="crop-handle nw"></div>
|
||
<div class="crop-handle ne"></div>
|
||
<div class="crop-handle sw"></div>
|
||
<div class="crop-handle se"></div>
|
||
<div class="crop-handle n"></div>
|
||
<div class="crop-handle s"></div>
|
||
<div class="crop-handle e"></div>
|
||
<div class="crop-handle w"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="image-info">
|
||
<span id="imageInfo">Размер: 0x0</span>
|
||
<span id="cropInfo">Обрезка: не выбрана</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="image-editor-footer">
|
||
<button class="btn btn-secondary" id="cancelBtn">Отмена</button>
|
||
<button class="btn btn-primary" id="saveBtn" disabled>Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Добавляем стили
|
||
if (!document.getElementById('image-editor-styles')) {
|
||
const styles = document.createElement('style');
|
||
styles.id = 'image-editor-styles';
|
||
styles.textContent = `
|
||
.image-editor-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 10000;
|
||
}
|
||
|
||
.image-editor-modal {
|
||
background: white;
|
||
border-radius: 8px;
|
||
width: 90vw;
|
||
max-width: 900px;
|
||
max-height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.image-editor-header {
|
||
padding: 20px;
|
||
border-bottom: 1px solid #ddd;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.image-editor-header h3 {
|
||
margin: 0;
|
||
color: #333;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #999;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
color: #333;
|
||
}
|
||
|
||
.image-editor-body {
|
||
flex: 1;
|
||
padding: 20px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.upload-area {
|
||
border: 2px dashed #ccc;
|
||
border-radius: 8px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
background: #f9f9f9;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.upload-area.dragover {
|
||
border-color: #007bff;
|
||
background: #e3f2fd;
|
||
}
|
||
|
||
.upload-content .upload-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.upload-content p {
|
||
margin: 0;
|
||
color: #666;
|
||
}
|
||
|
||
.btn-link {
|
||
background: none;
|
||
border: none;
|
||
color: #007bff;
|
||
text-decoration: underline;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.editor-area {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.editor-toolbar {
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: center;
|
||
padding: 12px;
|
||
background: #f8f9fa;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.tool-btn {
|
||
background: white;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 8px 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.tool-btn:hover {
|
||
background: #007bff;
|
||
color: white;
|
||
border-color: #007bff;
|
||
}
|
||
|
||
.canvas-container {
|
||
position: relative;
|
||
display: flex;
|
||
justify-content: center;
|
||
background: #f0f0f0;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#editorCanvas {
|
||
max-width: 100%;
|
||
max-height: 400px;
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
.crop-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.crop-box {
|
||
position: absolute;
|
||
border: 2px solid #007bff;
|
||
background: rgba(0, 123, 255, 0.1);
|
||
pointer-events: all;
|
||
cursor: move;
|
||
}
|
||
|
||
.crop-handle {
|
||
position: absolute;
|
||
width: 10px;
|
||
height: 10px;
|
||
background: #007bff;
|
||
border: 1px solid white;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.crop-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
|
||
.crop-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
|
||
.crop-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
|
||
.crop-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
|
||
.crop-handle.n { top: -5px; left: 50%; margin-left: -5px; cursor: n-resize; }
|
||
.crop-handle.s { bottom: -5px; left: 50%; margin-left: -5px; cursor: s-resize; }
|
||
.crop-handle.e { top: 50%; right: -5px; margin-top: -5px; cursor: e-resize; }
|
||
.crop-handle.w { top: 50%; left: -5px; margin-top: -5px; cursor: w-resize; }
|
||
|
||
.image-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 8px;
|
||
background: #f8f9fa;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
.image-editor-footer {
|
||
padding: 20px;
|
||
border-top: 1px solid #ddd;
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.btn {
|
||
padding: 8px 16px;
|
||
border-radius: 4px;
|
||
border: 1px solid;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #f8f9fa;
|
||
color: #333;
|
||
border-color: #ddd;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #007bff;
|
||
color: white;
|
||
border-color: #007bff;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
`;
|
||
document.head.appendChild(styles);
|
||
}
|
||
|
||
// Добавляем модальное окно в DOM
|
||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||
this.modal = document.querySelector('.image-editor-overlay');
|
||
|
||
this.bindEvents();
|
||
}
|
||
|
||
bindEvents() {
|
||
// Закрытие модального окна
|
||
this.modal.querySelector('.close-btn').addEventListener('click', () => this.close());
|
||
this.modal.querySelector('#cancelBtn').addEventListener('click', () => this.close());
|
||
|
||
// Клик по overlay для закрытия
|
||
this.modal.addEventListener('click', (e) => {
|
||
if (e.target === this.modal) this.close();
|
||
});
|
||
|
||
// Загрузка файла
|
||
const fileInput = this.modal.querySelector('#fileInput');
|
||
const selectFileBtn = this.modal.querySelector('#selectFileBtn');
|
||
const uploadArea = this.modal.querySelector('#uploadArea');
|
||
|
||
selectFileBtn.addEventListener('click', () => fileInput.click());
|
||
fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
|
||
|
||
// Drag & Drop
|
||
uploadArea.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.add('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('dragleave', () => {
|
||
uploadArea.classList.remove('dragover');
|
||
});
|
||
|
||
uploadArea.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove('dragover');
|
||
const files = e.dataTransfer.files;
|
||
if (files.length > 0) {
|
||
this.loadImage(files[0]);
|
||
}
|
||
});
|
||
|
||
// Инструменты редактирования
|
||
this.modal.querySelector('#rotateLeftBtn').addEventListener('click', () => this.rotate(-90));
|
||
this.modal.querySelector('#rotateRightBtn').addEventListener('click', () => this.rotate(90));
|
||
this.modal.querySelector('#flipHorizontalBtn').addEventListener('click', () => this.flipHorizontal = !this.flipHorizontal, this.redraw());
|
||
this.modal.querySelector('#flipVerticalBtn').addEventListener('click', () => this.flipVertical = !this.flipVertical, this.redraw());
|
||
this.modal.querySelector('#resetCropBtn').addEventListener('click', () => this.resetCrop());
|
||
|
||
// Сохранение
|
||
this.modal.querySelector('#saveBtn').addEventListener('click', () => this.save());
|
||
}
|
||
|
||
handleFileSelect(e) {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
this.loadImage(file);
|
||
}
|
||
}
|
||
|
||
async loadImage(file) {
|
||
if (!file.type.startsWith('image/')) {
|
||
alert('Пожалуйста, выберите изображение');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('image', file);
|
||
|
||
try {
|
||
const response = await fetch('/api/images/upload-image', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
this.imageData = result;
|
||
this.showEditor();
|
||
this.loadImageToCanvas(result.tempUrl);
|
||
} else {
|
||
alert(result.error || 'Ошибка загрузки изображения');
|
||
}
|
||
} catch (error) {
|
||
console.error('Upload error:', error);
|
||
alert('Ошибка загрузки изображения');
|
||
}
|
||
}
|
||
|
||
loadImageToCanvas(imageUrl) {
|
||
this.image = new Image();
|
||
this.image.onload = () => {
|
||
this.initCanvas();
|
||
this.initCropBox();
|
||
this.redraw();
|
||
this.updateInfo();
|
||
this.modal.querySelector('#saveBtn').disabled = false;
|
||
};
|
||
this.image.src = imageUrl;
|
||
}
|
||
|
||
showEditor() {
|
||
this.modal.querySelector('#uploadArea').style.display = 'none';
|
||
this.modal.querySelector('#editorArea').style.display = 'block';
|
||
}
|
||
|
||
initCanvas() {
|
||
this.canvas = this.modal.querySelector('#editorCanvas');
|
||
this.ctx = this.canvas.getContext('2d');
|
||
|
||
// Вычисляем размеры canvas
|
||
const containerWidth = 600; // максимальная ширина
|
||
const containerHeight = 400; // максимальная высота
|
||
|
||
const imageRatio = this.image.width / this.image.height;
|
||
const containerRatio = containerWidth / containerHeight;
|
||
|
||
if (imageRatio > containerRatio) {
|
||
this.canvas.width = containerWidth;
|
||
this.canvas.height = containerWidth / imageRatio;
|
||
} else {
|
||
this.canvas.width = containerHeight * imageRatio;
|
||
this.canvas.height = containerHeight;
|
||
}
|
||
|
||
this.scaleX = this.canvas.width / this.image.width;
|
||
this.scaleY = this.canvas.height / this.image.height;
|
||
}
|
||
|
||
initCropBox() {
|
||
const overlay = this.modal.querySelector('#cropOverlay');
|
||
const cropBox = this.modal.querySelector('#cropBox');
|
||
|
||
// Устанавливаем размеры overlay как у canvas
|
||
const canvasRect = this.canvas.getBoundingClientRect();
|
||
const containerRect = this.canvas.parentElement.getBoundingClientRect();
|
||
|
||
overlay.style.width = `${canvasRect.width}px`;
|
||
overlay.style.height = `${canvasRect.height}px`;
|
||
overlay.style.left = `${canvasRect.left - containerRect.left}px`;
|
||
overlay.style.top = `${canvasRect.top - containerRect.top}px`;
|
||
|
||
// Инициализируем crop box (50% от центра)
|
||
const boxWidth = canvasRect.width * 0.6;
|
||
const boxHeight = canvasRect.height * 0.6;
|
||
const boxLeft = (canvasRect.width - boxWidth) / 2;
|
||
const boxTop = (canvasRect.height - boxHeight) / 2;
|
||
|
||
cropBox.style.width = `${boxWidth}px`;
|
||
cropBox.style.height = `${boxHeight}px`;
|
||
cropBox.style.left = `${boxLeft}px`;
|
||
cropBox.style.top = `${boxTop}px`;
|
||
|
||
this.cropBox = {
|
||
x: boxLeft / canvasRect.width,
|
||
y: boxTop / canvasRect.height,
|
||
width: boxWidth / canvasRect.width,
|
||
height: boxHeight / canvasRect.height
|
||
};
|
||
|
||
this.bindCropEvents(cropBox, overlay);
|
||
}
|
||
|
||
bindCropEvents(cropBox, overlay) {
|
||
let resizing = false;
|
||
let moving = false;
|
||
let startPos = { x: 0, y: 0 };
|
||
let startBox = {};
|
||
|
||
// Обработчик для перемещения
|
||
cropBox.addEventListener('mousedown', (e) => {
|
||
if (e.target === cropBox) {
|
||
moving = true;
|
||
startPos = { x: e.clientX, y: e.clientY };
|
||
startBox = { ...this.cropBox };
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
|
||
// Обработчик для изменения размера
|
||
const handles = cropBox.querySelectorAll('.crop-handle');
|
||
handles.forEach(handle => {
|
||
handle.addEventListener('mousedown', (e) => {
|
||
resizing = handle.className.replace('crop-handle ', '');
|
||
startPos = { x: e.clientX, y: e.clientY };
|
||
startBox = { ...this.cropBox };
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
});
|
||
});
|
||
|
||
// Обработчики движения и отпускания мыши
|
||
document.addEventListener('mousemove', (e) => {
|
||
if (moving || resizing) {
|
||
this.updateCropBox(e, startPos, startBox, moving ? 'move' : resizing, overlay);
|
||
}
|
||
});
|
||
|
||
document.addEventListener('mouseup', () => {
|
||
moving = false;
|
||
resizing = false;
|
||
});
|
||
}
|
||
|
||
updateCropBox(e, startPos, startBox, action, overlay) {
|
||
const overlayRect = overlay.getBoundingClientRect();
|
||
const deltaX = (e.clientX - startPos.x) / overlayRect.width;
|
||
const deltaY = (e.clientY - startPos.y) / overlayRect.height;
|
||
|
||
let newBox = { ...startBox };
|
||
|
||
if (action === 'move') {
|
||
newBox.x = Math.max(0, Math.min(1 - startBox.width, startBox.x + deltaX));
|
||
newBox.y = Math.max(0, Math.min(1 - startBox.height, startBox.y + deltaY));
|
||
} else {
|
||
// Изменение размера в зависимости от handle
|
||
if (action.includes('n')) newBox.y += deltaY, newBox.height -= deltaY;
|
||
if (action.includes('s')) newBox.height += deltaY;
|
||
if (action.includes('w')) newBox.x += deltaX, newBox.width -= deltaX;
|
||
if (action.includes('e')) newBox.width += deltaX;
|
||
|
||
// Ограничиваем минимальные размеры
|
||
if (newBox.width < 0.1) newBox.width = 0.1;
|
||
if (newBox.height < 0.1) newBox.height = 0.1;
|
||
|
||
// Ограничиваем границы
|
||
if (newBox.x < 0) newBox.x = 0;
|
||
if (newBox.y < 0) newBox.y = 0;
|
||
if (newBox.x + newBox.width > 1) newBox.width = 1 - newBox.x;
|
||
if (newBox.y + newBox.height > 1) newBox.height = 1 - newBox.y;
|
||
}
|
||
|
||
this.cropBox = newBox;
|
||
this.updateCropBoxDisplay(overlay);
|
||
this.updateInfo();
|
||
}
|
||
|
||
updateCropBoxDisplay(overlay) {
|
||
const cropBoxElement = this.modal.querySelector('#cropBox');
|
||
const overlayRect = overlay.getBoundingClientRect();
|
||
|
||
cropBoxElement.style.left = `${this.cropBox.x * overlayRect.width}px`;
|
||
cropBoxElement.style.top = `${this.cropBox.y * overlayRect.height}px`;
|
||
cropBoxElement.style.width = `${this.cropBox.width * overlayRect.width}px`;
|
||
cropBoxElement.style.height = `${this.cropBox.height * overlayRect.height}px`;
|
||
}
|
||
|
||
rotate(degrees) {
|
||
this.rotation = (this.rotation + degrees) % 360;
|
||
this.redraw();
|
||
this.updateInfo();
|
||
}
|
||
|
||
redraw() {
|
||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||
|
||
this.ctx.save();
|
||
|
||
// Перемещаем к центру
|
||
this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2);
|
||
|
||
// Поворот
|
||
this.ctx.rotate(this.rotation * Math.PI / 180);
|
||
|
||
// Отражение
|
||
this.ctx.scale(this.flipHorizontal ? -1 : 1, this.flipVertical ? -1 : 1);
|
||
|
||
// Рисуем изображение
|
||
this.ctx.drawImage(
|
||
this.image,
|
||
-this.canvas.width / 2,
|
||
-this.canvas.height / 2,
|
||
this.canvas.width,
|
||
this.canvas.height
|
||
);
|
||
|
||
this.ctx.restore();
|
||
}
|
||
|
||
resetCrop() {
|
||
// Сброс crop box к исходному состоянию
|
||
this.cropBox = { x: 0.2, y: 0.2, width: 0.6, height: 0.6 };
|
||
this.updateCropBoxDisplay(this.modal.querySelector('#cropOverlay'));
|
||
this.updateInfo();
|
||
}
|
||
|
||
updateInfo() {
|
||
const imageInfo = this.modal.querySelector('#imageInfo');
|
||
const cropInfo = this.modal.querySelector('#cropInfo');
|
||
|
||
imageInfo.textContent = `Размер: ${this.image.width}x${this.image.height}`;
|
||
|
||
const cropWidth = Math.round(this.cropBox.width * this.image.width);
|
||
const cropHeight = Math.round(this.cropBox.height * this.image.height);
|
||
cropInfo.textContent = `Обрезка: ${cropWidth}x${cropHeight}`;
|
||
}
|
||
|
||
async save() {
|
||
const saveBtn = this.modal.querySelector('#saveBtn');
|
||
saveBtn.disabled = true;
|
||
saveBtn.textContent = 'Сохранение...';
|
||
|
||
try {
|
||
const cropData = {
|
||
x: this.cropBox.x * this.image.width,
|
||
y: this.cropBox.y * this.image.height,
|
||
width: this.cropBox.width * this.image.width,
|
||
height: this.cropBox.height * this.image.height
|
||
};
|
||
|
||
const response = await fetch('/api/images/process-image', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
tempId: this.imageData.tempId,
|
||
rotation: this.rotation,
|
||
flipHorizontal: this.flipHorizontal,
|
||
flipVertical: this.flipVertical,
|
||
cropData,
|
||
targetFolder: this.options.targetFolder
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
this.onSave(result.url);
|
||
this.close();
|
||
} else {
|
||
alert(result.error || 'Ошибка сохранения');
|
||
saveBtn.disabled = false;
|
||
saveBtn.textContent = 'Сохранить';
|
||
}
|
||
} catch (error) {
|
||
console.error('Save error:', error);
|
||
alert('Ошибка сохранения изображения');
|
||
saveBtn.disabled = false;
|
||
saveBtn.textContent = 'Сохранить';
|
||
}
|
||
}
|
||
|
||
close() {
|
||
// Удаляем временный файл
|
||
if (this.imageData && this.imageData.tempId) {
|
||
fetch(`/api/images/temp-image/${this.imageData.tempId}`, {
|
||
method: 'DELETE'
|
||
}).catch(console.error);
|
||
}
|
||
|
||
this.onCancel();
|
||
if (this.modal) {
|
||
this.modal.remove();
|
||
}
|
||
}
|
||
|
||
show() {
|
||
if (this.modal) {
|
||
this.modal.style.display = 'flex';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Глобально доступная функция для открытия редактора
|
||
window.openImageEditor = function(options = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
const editor = new ImageEditor({
|
||
...options,
|
||
onSave: (url) => resolve(url),
|
||
onCancel: () => reject(new Error('Canceled'))
|
||
});
|
||
editor.show();
|
||
});
|
||
}; |