From b9390671146eff0735b0615e89df0be45d659bad Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Sat, 23 Aug 2025 20:32:22 +0300 Subject: [PATCH] Improve context menu --- frontend/index.html | 2 +- frontend/src/chat.ts | 4 +- frontend/src/css/_chat.scss | 55 ------------- frontend/src/css/common/_components.scss | 84 ++++++++++++++++++++ frontend/src/message-context-menu.ts | 98 +++++++++++------------- frontend/src/utils/material.ts | 3 + 6 files changed, 135 insertions(+), 111 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 5f5a6e8..5a1549b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -321,7 +321,7 @@ -
+
reply Reply diff --git a/frontend/src/chat.ts b/frontend/src/chat.ts index 2808973..7cede95 100644 --- a/frontend/src/chat.ts +++ b/frontend/src/chat.ts @@ -45,12 +45,12 @@ export function addMessage(message: Message, isAuthor: boolean): void { profileImg.src = message.profile_picture || defaultAvatar; profileImg.alt = message.username; - let errorLock = false + let errorLock = false; profileImg.addEventListener("error", () => { if (!errorLock) { profileImg.src = defaultAvatar; - errorLock = true + errorLock = true; } }); diff --git a/frontend/src/css/_chat.scss b/frontend/src/css/_chat.scss index ccdb043..91f667a 100644 --- a/frontend/src/css/_chat.scss +++ b/frontend/src/css/_chat.scss @@ -319,61 +319,6 @@ } } -// Message Context Menu -#message-context-menu { - position: fixed; - background-color: $color-dark-surface-container; - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - padding: 0.5rem 0; - z-index: 1000; - display: none; - min-width: 150px; - max-width: 200px; - white-space: nowrap; - - .context-menu-item { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - cursor: pointer; - color: $color-dark-on-surface; - transition: background-color 0.2s ease; - font-size: 0.9rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &:hover { - background-color: rgba(255, 255, 255, 0.1); - } - - .material-symbols { - font-size: 1.1rem; - flex-shrink: 0; - } - } -} - -// Dialog content styles -.dialog-content { - padding: 1.5rem; - - h3 { - margin: 0 0 1rem 0; - color: $color-dark-on-surface; - font-size: 1.2rem; - } - - .dialog-actions { - display: flex; - gap: 0.75rem; - justify-content: flex-end; - margin-top: 1.5rem; - } -} - // Reply preview styles .reply-preview { background-color: $color-dark-surface; diff --git a/frontend/src/css/common/_components.scss b/frontend/src/css/common/_components.scss index 982ba92..230ed3e 100644 --- a/frontend/src/css/common/_components.scss +++ b/frontend/src/css/common/_components.scss @@ -27,4 +27,88 @@ button, input { font: inherit; +} + +.context-menu { + position: fixed; + background-color: $color-dark-surface-container; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + padding: 0.5rem 0; + z-index: 1000; + display: none; + min-width: 150px; + max-width: 200px; + white-space: nowrap; + user-select: none; + + .context-menu-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + cursor: pointer; + color: $color-dark-on-surface; + transition: background-color 0.2s ease; + font-size: 0.9rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } + + .material-symbols { + font-size: 1.1rem; + flex-shrink: 0; + } + } + + &.pos-top-left { + transform-origin: top right; + } + &.pos-top-right { + transform-origin: top left; + } + &.pos-bottom-left { + transform-origin: bottom right; + } + &.pos-bottom-right { + transform-origin: bottom left; + } + + &.open { + animation: context-menu-open 0.25s ease; + } + + @keyframes context-menu-open { + 0% { + opacity: 0; + transform: scale(0.5); + } + + 100% { + opacity: 1; + transform: scale(1); + } + } +} + +// Dialog content styles +.dialog-content { + padding: 1.5rem; + + h3 { + margin: 0 0 1rem 0; + color: $color-dark-on-surface; + font-size: 1.2rem; + } + + .dialog-actions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; + margin-top: 1.5rem; + } } \ No newline at end of file diff --git a/frontend/src/message-context-menu.ts b/frontend/src/message-context-menu.ts index aece9f8..8375c47 100644 --- a/frontend/src/message-context-menu.ts +++ b/frontend/src/message-context-menu.ts @@ -9,11 +9,14 @@ import { currentUser, authToken } from "./auth"; import { websocket } from "./websocket"; import type { Message, WebSocketMessage } from "./types"; import { showSuccess, showError } from "./utils/notification"; +import { delay } from "./utils/utils"; +import type { Dialog } from "mdui/components/dialog"; +import type { TextField } from "mdui/components/text-field"; let menu = document.getElementById("message-context-menu")!; -let editDialog = document.getElementById("edit-message-dialog"); -let replyDialog = document.getElementById("reply-message-dialog"); +let editDialog = document.getElementById("edit-message-dialog") as Dialog; +let replyDialog = document.getElementById("reply-message-dialog") as Dialog; let currentMessage: Message | null = null; function init() { @@ -73,8 +76,6 @@ function bindEvents(): void { * @param {number} y - Y coordinate */ export function show(message: Message, x: number, y: number): void { - if (!menu) return; - currentMessage = message; // Only show edit and delete for own messages @@ -88,26 +89,28 @@ export function show(message: Message, x: number, y: number): void { editItem.style.display = 'none'; deleteItem.style.display = 'none'; } - - // Calculate menu position - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - // Default menu size estimates - const menuWidth = 150; - const menuHeight = 120; + // Position the menu properly + menu.style.display = 'block'; + + let menuWidth = menu.offsetWidth; + let menuHeight = menu.offsetHeight; let adjustedX = x; let adjustedY = y; + let vertical = "top"; + let horizontal = "right"; // Adjust horizontal position if menu would go off-screen - if (x + menuWidth > viewportWidth) { + if (x + menuWidth > window.innerWidth) { adjustedX = x - menuWidth; + horizontal = "left"; } // Adjust vertical position if menu would go off-screen - if (y + menuHeight > viewportHeight) { + if (y + menuHeight > window.innerHeight) { adjustedY = y - menuHeight; + vertical = "bottom"; } // Ensure menu doesn't go off the left or top edges @@ -116,16 +119,19 @@ export function show(message: Message, x: number, y: number): void { menu.style.left = `${adjustedX}px`; menu.style.top = `${adjustedY}px`; - menu.style.display = 'block'; + menu.classList.add(`pos-${vertical}-${horizontal}`, "open"); } /** * Hides the context menu */ export function hide(): void { - if (menu) { - menu.style.display = 'none'; - } + menu.style.display = 'none'; + menu.classList.forEach((name) => { + if (name.match(/pos-\w+-\w+/)) { + menu.classList.remove(name); + } + }) currentMessage = null; } @@ -156,21 +162,16 @@ function handleAction(action: string, message: Message): void { * @param {Message} message - The message to edit * @private */ -function showEditDialog(message: Message): void { - if (!editDialog) return; - - const textField = editDialog.querySelector('#edit-message-input') as any; - if (textField) { - textField.value = message.content; - } +async function showEditDialog(message: Message): Promise { + const textField = editDialog.querySelector('#edit-message-input') as TextField; + textField.value = message.content; currentMessage = message; - (editDialog as any).open = true; + editDialog.open = true; // Focus the text field - setTimeout(() => { - textField?.focus(); - }, 100); + await delay(100); + textField?.focus(); } /** @@ -178,9 +179,7 @@ function showEditDialog(message: Message): void { * @private */ function hideEditDialog(): void { - if (editDialog) { - (editDialog as any).open = false; - } + editDialog.open = false; } /** @@ -188,9 +187,9 @@ function hideEditDialog(): void { * @private */ function saveEdit(): void { - if (!currentMessage || !editDialog) return; + if (!currentMessage) return; - const textField = editDialog.querySelector('#edit-message-input') as any; + const textField = editDialog.querySelector('#edit-message-input') as TextField; const newContent = textField?.value?.trim() || ''; if (!newContent) { @@ -231,26 +230,21 @@ function saveEdit(): void { * @param {Message} message - The message to reply to * @private */ -function showReplyDialog(message: Message): void { - if (!replyDialog) return; - +async function showReplyDialog(message: Message): Promise { const preview = replyDialog.querySelector('#reply-preview') as HTMLElement; - if (preview) { - preview.innerHTML = ` -
- ${message.username}: ${message.content} -
- `; - } + preview.innerHTML = ` +
+ ${message.username}: ${message.content} +
+ `; currentMessage = message; - (replyDialog as any).open = true; + replyDialog.open = true; // Focus the text field - setTimeout(() => { - const textField = replyDialog?.querySelector('#reply-message-input') as any; - textField?.focus(); - }, 100); + await delay(100); + const textField = replyDialog?.querySelector('#reply-message-input') as TextField; + textField?.focus(); } /** @@ -258,9 +252,7 @@ function showReplyDialog(message: Message): void { * @private */ function hideReplyDialog(): void { - if (replyDialog) { - (replyDialog as any).open = false; - } + replyDialog.open = false; } /** @@ -268,9 +260,9 @@ function hideReplyDialog(): void { * @private */ function sendReply(): void { - if (!currentMessage || !replyDialog) return; + if (!currentMessage) return; - const textField = replyDialog.querySelector('#reply-message-input') as any; + const textField = replyDialog.querySelector('#reply-message-input') as TextField; const content = textField?.value?.trim() || ''; if (!content) { diff --git a/frontend/src/utils/material.ts b/frontend/src/utils/material.ts index 265d42b..5f220df 100644 --- a/frontend/src/utils/material.ts +++ b/frontend/src/utils/material.ts @@ -19,6 +19,9 @@ import 'mdui/components/text-field'; import 'mdui/components/button-icon'; import 'mdui/components/top-app-bar'; import 'mdui/components/top-app-bar-title'; +import 'mdui/components/dropdown.js'; +import 'mdui/components/menu.js'; +import 'mdui/components/menu-item.js'; import { setColorScheme } from 'mdui/functions/setColorScheme.js';