USER
// images.js
// Assumes uploadFile, showContextMenu, addResizeListeners are available globally
socket.on('new_image', (data) => {
const image = createImageElement(data);
document.getElementById('board').appendChild(image);
});
socket.on('updated_image', (data) => {
const image = document.querySelector(`.image[data-id="${data.id}"]`);
if (image) {
image.style.left = `${data.x}px`;
image.style.top = `${data.y}px`;
image.style.width = `${data.width}px`;
image.style.height = `${data.height}px`;
image.querySelector('img').src = data.url;
}
});
socket.on('element_deleted', (data) => {
if (data.type === 'image') {
const element = document.querySelector(`.image[data-id="${data.id}"]`);
if (element) {
element.remove();
}
}
});
function createImageElement(data) {
const image = document.createElement('div');
image.className = 'image';
image.dataset.id = data.id;
image.style.left = `${data.x}px`;
image.style.top = `${data.y}px`;
image.style.width = `${data.width}px`;
image.style.height = `${data.height}px`;
const img = document.createElement('img');
img.src = data.url;
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'contain'; // Maintains aspect ratio
image.appendChild(img);
addImageEventListeners(image, data.id);
return image;
}
function addImageEventListeners(image, id) {
image.addEventListener('mousedown', (e) => {
if (e.button !== 0) return; // Left mouse button
e.preventDefault();
e.stopPropagation();
const startX = e.clientX;
const startY = e.clientY;
const startLeft = parseFloat(image.style.left) || 0;
const startTop = parseFloat(image.style.top) || 0;
function moveImage(e) {
const deltaX = (e.clientX - startX) / scale;
const deltaY = (e.clientY - startY) / scale;
image.style.left = `${startLeft + deltaX}px`;
image.style.top = `${startTop + deltaY}px`;
const x = parseFloat(image.style.left) || 0;
const y = parseFloat(image.style.top) || 0;
const width = image.offsetWidth;
const height = image.offsetHeight;
socket.emit('update_image', { id, url: image.querySelector('img').src, x, y, width, height });
}
document.addEventListener('mousemove', moveImage);
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', moveImage);
}, { once: true });
});
image.addEventListener('touchstart', (e) => {
e.preventDefault();
e.stopPropagation();
const startX = e.touches[0].clientX;
const startY = e.touches[0].clientY;
const startLeft = parseFloat(image.style.left) || 0;
const startTop = parseFloat(image.style.top) || 0;
function moveImage(e) {
const deltaX = (e.touches[0].clientX - startX) / scale;
const deltaY = (e.touches[0].clientY - startY) / scale;
image.style.left = `${startLeft + deltaX}px`;
image.style.top = `${startTop + deltaY}px`;
const x = parseFloat(image.style.left) || 0;
const y = parseFloat(image.style.top) || 0;
const width = image.offsetWidth;
const height = image.offsetHeight;
socket.emit('update_image', { id, url: image.querySelector('img').src, x, y, width, height });
}
document.addEventListener('touchmove', moveImage);
document.addEventListener('touchend', () => {
document.removeEventListener('touchmove', moveImage);
}, { once: true });
});
image.addEventListener('dblclick', (e) => {
e.preventDefault();
e.stopPropagation();
showContextMenu(image, id, e.clientX, e.clientY);
});
image.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
e.preventDefault();
e.stopPropagation();
showContextMenu(image, id, e.touches[0].clientX, e.touches[0].clientY);
}
});
addResizeListeners(image, id, 'image');
}
function handleExistingImages() {
document.querySelectorAll('.image').forEach(image => {
const id = image.dataset.id;
const url = image.querySelector('img').src;
const x = image.offsetLeft;
const y = image.offsetTop;
const width = image.offsetWidth;
const height = image.offsetHeight;
image.addEventListener('dblclick', (e) => {
e.preventDefault();
e.stopPropagation();
showContextMenu(image, id, e.clientX, e.clientY);
});
addImageEventListeners(image, id);
});
}
// board.js
let scale = 0.5; // Initial scale
let translateX = 0;
let translateY = 0;
const board = document.getElementById('board');
const boardWrapper = document.getElementById('board-wrapper');
// Constants for zoom
const MIN_SCALE = 0.1; // Minimum scale
const MAX_SCALE = 5; // Maximum scale
const ZOOM_FACTOR = 0.1; // Zoom speed
// Apply initial scale
board.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
boardWrapper.style.backgroundPosition = `${translateX}px ${translateY}px`;
// Show images after setting scale
document.querySelectorAll('.image').forEach(img => {
img.classList.remove('hidden');
});
boardWrapper.addEventListener('wheel', (e) => {
e.preventDefault();
const previousScale = scale;
scale += e.deltaY < 0 ? ZOOM_FACTOR : -ZOOM_FACTOR;
scale = Math.min(Math.max(MIN_SCALE, scale), MAX_SCALE);
// Calculate cursor position relative to the board
const rect = board.getBoundingClientRect();
const offsetX = (e.clientX - rect.left) / previousScale;
const offsetY = (e.clientY - rect.top) / previousScale;
// Adjust translate to keep cursor point at the same board position
translateX -= offsetX * (scale - previousScale);
translateY -= offsetY * (scale - previousScale);
board.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
boardWrapper.style.backgroundPosition = `${translateX}px ${translateY}px`;
});
boardWrapper.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
e.preventDefault();
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const startDistance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
const startScale = scale;
function zoom(e) {
if (e.touches.length !== 2) return;
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const currentDistance = Math.hypot(touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY);
const newScale = startScale * (currentDistance / startDistance);
scale = Math.min(Math.max(MIN_SCALE, newScale), MAX_SCALE);
// Calculate midpoint of touches
const midX = (touch1.clientX + touch2.clientX) / 2;
const midY = (touch1.clientY + touch2.clientY) / 2;
// Calculate offset relative to previous scale
const rect = board.getBoundingClientRect();
const offsetX = (midX - rect.left) / startScale;
const offsetY = (midY - rect.top) / startScale;
// Adjust translate to keep midpoint at same board position
translateX -= offsetX * (scale - startScale);
translateY -= offsetY * (scale - startScale);
board.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
boardWrapper.style.backgroundPosition = `${translateX}px ${translateY}px`;
}
document.addEventListener('touchmove', zoom);
document.addEventListener('touchend', () => {
document.removeEventListener('touchmove', zoom);
}, { once: true });
}
});
let isPanning = false;
let startPanX, startPanY;
boardWrapper.addEventListener('mousedown', (e) => {
if (e.button === 2) { // Right mouse button
isPanning = true;
startPanX = e.clientX - translateX;
startPanY = e.clientY - translateY;
boardWrapper.style.cursor = 'grabbing';
}
});
boardWrapper.addEventListener('mouseup', (e) => {
if (e.button === 2) { // Right mouse button
isPanning = false;
boardWrapper.style.cursor = 'grab';
}
});
boardWrapper.addEventListener('mousemove', (e) => {
if (!isPanning) return;
e.preventDefault();
translateX = e.clientX - startPanX;
translateY = e.clientY - startPanY;
board.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
boardWrapper.style.backgroundPosition = `${translateX}px ${translateY}px`;
});
// Add touch events for panning
boardWrapper.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
isPanning = true;
startPanX = e.touches[0].clientX - translateX;
startPanY = e.touches[0].clientY - translateY;
boardWrapper.style.cursor = 'grabbing';
}
});
boardWrapper.addEventListener('touchend', (e) => {
isPanning = false;
boardWrapper.style.cursor = 'grab';
});
boardWrapper.addEventListener('touchmove', (e) => {
if (!isPanning) return;
e.preventDefault();
translateX = e.touches[0].clientX - startPanX;
translateY = e.touches[0].clientY - startPanY;
board.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
boardWrapper.style.backgroundPosition = `${translateX}px ${translateY}px`;
});
// Prevent context menu on board-wrapper
boardWrapper.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
// static/js/main.js
// Проверяем, что 'socket' уже инициализирован
if (!window.socket) {
console.error('Socket is not initialized!');
}
// Event listener для кнопки 'add-note'
document.getElementById('add-note').addEventListener('click', () => {
const x = Math.floor(Math.random() * window.innerWidth);
const y = Math.floor(Math.random() * window.innerHeight);
const width = 200; // Ширина по умолчанию
const height = 40; // Высота по умолчанию
socket.emit('create_note', { text: 'New Comment', x, y, width, height });
});
// Event listener для кнопки 'add-image'
document.getElementById('add-image').addEventListener('click', () => {
const formHtml = `
<div id="image-form" style="position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); background:white; padding:20px; border-radius:8px; box-shadow:0 0 10px rgba(0,0,0,0.1); z-index: 1000;">
<label for="image-url" style="display:block; margin-bottom:8px;">Image URL:</label>
<input type="text" id="image-url" placeholder="Enter image URL" style="width: calc(100% - 22px); padding:10px; margin-bottom:12px; border:1px solid #ccc; border-radius:4px;"><br>
<label for="image-file" style="display:block; margin-bottom:8px;">Or Upload File:</label>
<input type="file" id="image-file" accept="image/*" style="margin-bottom:12px;"><br>
<button id="add-image-submit" style="padding:10px 20px; background-color:#007bff; color:white; border:none; border-radius:4px; cursor:pointer; margin-right:8px;">Add Image</button>
<button id="cancel-image" style="padding:10px 20px; background-color:#ccc; color:white; border:none; border-radius:4px; cursor:pointer;">Cancel</button>
</div>
`;
document.body.insertAdjacentHTML('beforeend', formHtml);
document.getElementById('add-image-submit').addEventListener('click', async () => {
const url = document.getElementById('image-url').value;
const fileInput = document.getElementById('image-file');
let imageUrl;
if (url) {
imageUrl = url;
} else if (fileInput.files.length > 0) {
const file = fileInput.files[0];
imageUrl = await uploadFile(file);
}
if (imageUrl) {
const x = Math.floor(Math.random() * window.innerWidth);
const y = Math.floor(Math.random() * window.innerHeight);
const img = new Image();
img.src = imageUrl;
img.onload = () => {
const maxWidth = 720;
const maxHeight = 720;
let width = img.naturalWidth;
let height = img.naturalHeight;
if (width > maxWidth || height > maxHeight) {
const ratio = Math.min(maxWidth / width, maxHeight / height);
width *= ratio;
height *= ratio;
}
socket.emit('create_image', { url: imageUrl, x, y, width, height });
closeModal();
};
}
});
document.getElementById('cancel-image').addEventListener('click', closeModal);
function closeModal() {
const form = document.getElementById('image-form');
if (form) {
form.remove();
}
}
});
// Обработка существующих заметок и изображений после загрузки страницы
document.addEventListener('DOMContentLoaded', () => {
handleExistingNotes();
handleExistingImages();
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Miro Clone</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<div id="board-wrapper">
<div id="board">
{% for note in notes %}
<div class="note" data-id="{{ note.id }}" style="left: {{ note.x }}px; top: {{ note.y }}px; width: {{ note.width }}px; height: {{ note.height }}px;">
<textarea>{{ note.text }}</textarea>
</div>
{% endfor %}
{% for image in images %}
<div class="image hidden" data-id="{{ image.id }}" style="left: {{ image.x }}px; top: {{ image.y }}px; width: {{ image.width }}px; height: {{ image.height }}px;">
<img src="{{ image.url }}" style="width: 100%; height: 100%; object-fit: contain;">
</div>
{% endfor %}
</div>
</div>
<button id="add-note">Add Comment</button>
<button id="add-image">Add Image</button>
<!-- Подключение Socket.io -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<!-- Инициализация Socket -->
<script src="{{ url_for('static', filename='js/socket.js') }}"></script>
<!-- Другие JavaScript файлы -->
<script src="{{ url_for('static', filename='js/upload.js') }}"></script>
<script src="{{ url_for('static', filename='js/contextMenu.js') }}"></script>
<script src="{{ url_for('static', filename='js/resize.js') }}"></script>
<script src="{{ url_for('static', filename='js/notes.js') }}"></script>
<script src="{{ url_for('static', filename='js/images.js') }}"></script>
<script src="{{ url_for('static', filename='js/board.js') }}"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>
// contextMenu.js
function showContextMenu(element, id, x, y) {
const contextMenu = document.createElement('div');
contextMenu.className = 'context-menu';
contextMenu.style.position = 'absolute';
contextMenu.style.left = `${x}px`;
contextMenu.style.top = `${y}px`;
contextMenu.style.backgroundColor = 'white';
contextMenu.style.borderRadius = '4px';
contextMenu.style.border = '1px solid #ccc';
contextMenu.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';
contextMenu.style.zIndex = '1000';
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.style.padding = '5px 10px';
deleteButton.style.border = 'none';
deleteButton.style.backgroundColor = '#ff4d4d';
deleteButton.style.color = 'white';
deleteButton.style.borderRadius = '4px';
deleteButton.style.cursor = 'pointer';
deleteButton.style.width = '100%';
deleteButton.style.textAlign = 'left';
deleteButton.addEventListener('click', () => {
element.remove();
socket.emit('delete_element', { id: id, type: element.classList.contains('note') ? 'note' : 'image' });
contextMenu.remove();
});
contextMenu.appendChild(deleteButton);
document.body.appendChild(contextMenu);
// Close context menu when clicking outside
document.addEventListener('click', (e) => {
if (!contextMenu.contains(e.target)) {
contextMenu.remove();
}
}, { once: true });
}
Помоги мне. Мне надо чтобы на устройствах с тачпадом в контекстном меню изображения была еще кнопка "Move" и при нажатии на которую включался режим перемещения элемента. Пока на нее не нажать элемент не должен перемещаться. Это нужно только для тачпада, для мыши все оставляем как сейчас есть. Убедись что логика управления мышью не нарушенаASSISTANT
Добавление функционала для режима перемещения при помощи тачпада в вашем приложении требует создания дополнительных условий для активации режима перемещения. В этом случае, когда вы используете тачпад, появится кнопка "Move" в контекстном меню, активация которой позволит перемещать элемент. Вот как вы можете это реализовать:
1. Определить, используется ли тачпад, чтобы различать методы ввода.
2. Добавить кнопку "Move" в контекстном меню.
3. Активировать перемещение элемента после нажатия кнопки "Move".
### Изменения в `contextMenu.js`:
Добавьте в `showContextMenu` функционал проверки, если используется тачпад, и добавьте кнопку "Move":
```javascript
function showContextMenu(element, id, x, y) {
const contextMenu = document.createElement('div');
contextMenu.className = 'context-menu';
contextMenu.style.position = 'absolute';
contextMenu.style.left = `${x}px`;
contextMenu.style.top = `${y}px`;
contextMenu.style.backgroundColor = 'white';
contextMenu.style.borderRadius = '4px';
contextMenu.style.border = '1px solid #ccc';
contextMenu.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';
contextMenu.style.zIndex = '1000';
// Проверяем, используется ли тачпад
const isTouchpad = 'ontouchstart' in window || window.navigator.maxTouchPoints > 0;
if (isTouchpad) {
const moveButton = document.createElement('button');
moveButton.textContent = 'Move';
moveButton.style.padding = '5px 10px';
moveButton.style.border = 'none';
moveButton.style.backgroundColor = '#007bff';
moveButton.style.color = 'white';
moveButton.style.borderRadius = '4px';
moveButton.style.cursor = 'pointer';
moveButton.style.width = '100%';
moveButton.style.textAlign = 'left';
moveButton.addEventListener('click', () => {
contextMenu.remove();
enableMoveMode(element, id); // функция для активации режима перемещения
});
contextMenu.appendChild(moveButton);
}
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.style.padding = '5px 10px';
deleteButton.style.border = 'none';
deleteButton.style.backgroundColor = '#ff4d4d';
deleteButton.style.color = 'white';
deleteButton.style.borderRadius = '4px';
deleteButton.style.cursor = 'pointer';
deleteButton.style.width = '100%';
deleteButton.style.textAlign = 'left';
deleteButton.addEventListener('click', () => {
element.remove();
socket.emit('delete_element', { id: id, type: element.classList.contains('note') ? 'note' : 'image' });
contextMenu.remove();
});
contextMenu.appendChild(deleteButton);
document.body.appendChild(contextMenu);
// Закрытие контекстного меню, если кликнуть вне его
document.addEventListener('click', (e) => {
if (!contextMenu.contains(e.target)) {
contextMenu.remove();
}
}, { once: true });
}
// Функция для активации режима перемещения
function enableMoveMode(element, id) {
let isMoving = false;
function startMove(e) {
if (isMoving || e.touches && e.touches.length > 1) return;
isMoving = true;
const startX = e.clientX || e.touches[0].clientX;
const startY = e.clientY || e.touches[0].clientY;
const startLeft = parseFloat(element.style.left) || 0;
const startTop = parseFloat(element.style.top) || 0;
function move(e) {
if (!isMoving) return;
const currentX = e.clientX || e.touches[0].clientX;
const currentY = e.clientY || e.touches[0].clientY;
const deltaX = (currentX - startX) / scale;
const deltaY = (currentY - startY) / scale;
element.style.left = `${startLeft + deltaX}px`;
element.style.top = `${startTop + deltaY}px`;
const x = parseFloat(element.style.left) || 0;
const y = parseFloat(element.style.top) || 0;
const width = element.offsetWidth;
const height = element.offsetHeight;
socket.emit('update_image', { id, url: element.querySelector('img').src, x, y, width, height });
}
function stopMove() {
document.removeEventListener('mousemove', move);
document.removeEventListener('touchmove', move);
document.removeEventListener('mouseup', stopMove);
document.removeEventListener('touchend', stopMove);
isMoving = false;
}
document.addEventListener('mousemove', move);
document.addEventListener('touchmove', move);
document.addEventListener('mouseup', stopMove);
document.addEventListener('touchend', stopMove);
}
element.addEventListener('mousedown', startMove);
element.addEventListener('touchstart', startMove);
}
```
### Принцип работы:
1. **Идентификация устройства**: Используя проверку `ontouchstart` на окне или свойство `maxTouchPoints` у объекта `navigator`, мы можем определить, есть ли у устройства возможность тач-ввода, чтобы показывать или скрывать кнопку "Move".
2. **Создание контекстного меню**: Если устройство поддерживает тач-ввод, добавляем кнопку "Move" в контекстное меню.
3. **Режим перемещения**: При нажатии на кнопку "Move" создается функция `enableMoveMode`, которая управляет перемещением элемента. В этом режиме пользователи могут перемещать элемент путем перетаскивания.
Настоящие изменения позволят пользователям с тачпадом безопасно перемещать элементы, не беспокоясь о случайном перемещении во время использования других функций.