mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Improve context menu
This commit is contained in:
+1
-1
@@ -321,7 +321,7 @@
|
||||
</div>
|
||||
</mdui-dialog>
|
||||
|
||||
<div id="message-context-menu">
|
||||
<div id="message-context-menu" class="context-menu">
|
||||
<div class="context-menu-item" data-action="reply">
|
||||
<span class="material-symbols">reply</span>
|
||||
Reply
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,3 +28,87 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -89,25 +90,27 @@ export function show(message: Message, x: number, y: number): void {
|
||||
deleteItem.style.display = 'none';
|
||||
}
|
||||
|
||||
// Calculate menu position
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
// Position the menu properly
|
||||
menu.style.display = 'block';
|
||||
|
||||
// Default menu size estimates
|
||||
const menuWidth = 150;
|
||||
const menuHeight = 120;
|
||||
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<void> {
|
||||
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<void> {
|
||||
const preview = replyDialog.querySelector('#reply-preview') as HTMLElement;
|
||||
if (preview) {
|
||||
preview.innerHTML = `
|
||||
<div class="reply-preview-content">
|
||||
<strong>${message.username}</strong>: ${message.content}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
preview.innerHTML = `
|
||||
<div class="reply-preview-content">
|
||||
<strong>${message.username}</strong>: ${message.content}
|
||||
</div>
|
||||
`;
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user