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>
|
</div>
|
||||||
</mdui-dialog>
|
</mdui-dialog>
|
||||||
|
|
||||||
<div id="message-context-menu">
|
<div id="message-context-menu" class="context-menu">
|
||||||
<div class="context-menu-item" data-action="reply">
|
<div class="context-menu-item" data-action="reply">
|
||||||
<span class="material-symbols">reply</span>
|
<span class="material-symbols">reply</span>
|
||||||
Reply
|
Reply
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ export function addMessage(message: Message, isAuthor: boolean): void {
|
|||||||
profileImg.src = message.profile_picture || defaultAvatar;
|
profileImg.src = message.profile_picture || defaultAvatar;
|
||||||
profileImg.alt = message.username;
|
profileImg.alt = message.username;
|
||||||
|
|
||||||
let errorLock = false
|
let errorLock = false;
|
||||||
|
|
||||||
profileImg.addEventListener("error", () => {
|
profileImg.addEventListener("error", () => {
|
||||||
if (!errorLock) {
|
if (!errorLock) {
|
||||||
profileImg.src = defaultAvatar;
|
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 styles
|
||||||
.reply-preview {
|
.reply-preview {
|
||||||
background-color: $color-dark-surface;
|
background-color: $color-dark-surface;
|
||||||
|
|||||||
@@ -28,3 +28,87 @@
|
|||||||
button, input {
|
button, input {
|
||||||
font: inherit;
|
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 { websocket } from "./websocket";
|
||||||
import type { Message, WebSocketMessage } from "./types";
|
import type { Message, WebSocketMessage } from "./types";
|
||||||
import { showSuccess, showError } from "./utils/notification";
|
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 menu = document.getElementById("message-context-menu")!;
|
||||||
let editDialog = document.getElementById("edit-message-dialog");
|
let editDialog = document.getElementById("edit-message-dialog") as Dialog;
|
||||||
let replyDialog = document.getElementById("reply-message-dialog");
|
let replyDialog = document.getElementById("reply-message-dialog") as Dialog;
|
||||||
let currentMessage: Message | null = null;
|
let currentMessage: Message | null = null;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
@@ -73,8 +76,6 @@ function bindEvents(): void {
|
|||||||
* @param {number} y - Y coordinate
|
* @param {number} y - Y coordinate
|
||||||
*/
|
*/
|
||||||
export function show(message: Message, x: number, y: number): void {
|
export function show(message: Message, x: number, y: number): void {
|
||||||
if (!menu) return;
|
|
||||||
|
|
||||||
currentMessage = message;
|
currentMessage = message;
|
||||||
|
|
||||||
// Only show edit and delete for own messages
|
// 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';
|
deleteItem.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate menu position
|
// Position the menu properly
|
||||||
const viewportWidth = window.innerWidth;
|
menu.style.display = 'block';
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
|
|
||||||
// Default menu size estimates
|
let menuWidth = menu.offsetWidth;
|
||||||
const menuWidth = 150;
|
let menuHeight = menu.offsetHeight;
|
||||||
const menuHeight = 120;
|
|
||||||
|
|
||||||
let adjustedX = x;
|
let adjustedX = x;
|
||||||
let adjustedY = y;
|
let adjustedY = y;
|
||||||
|
let vertical = "top";
|
||||||
|
let horizontal = "right";
|
||||||
|
|
||||||
// Adjust horizontal position if menu would go off-screen
|
// Adjust horizontal position if menu would go off-screen
|
||||||
if (x + menuWidth > viewportWidth) {
|
if (x + menuWidth > window.innerWidth) {
|
||||||
adjustedX = x - menuWidth;
|
adjustedX = x - menuWidth;
|
||||||
|
horizontal = "left";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust vertical position if menu would go off-screen
|
// Adjust vertical position if menu would go off-screen
|
||||||
if (y + menuHeight > viewportHeight) {
|
if (y + menuHeight > window.innerHeight) {
|
||||||
adjustedY = y - menuHeight;
|
adjustedY = y - menuHeight;
|
||||||
|
vertical = "bottom";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure menu doesn't go off the left or top edges
|
// 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.left = `${adjustedX}px`;
|
||||||
menu.style.top = `${adjustedY}px`;
|
menu.style.top = `${adjustedY}px`;
|
||||||
menu.style.display = 'block';
|
menu.classList.add(`pos-${vertical}-${horizontal}`, "open");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hides the context menu
|
* Hides the context menu
|
||||||
*/
|
*/
|
||||||
export function hide(): void {
|
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;
|
currentMessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,21 +162,16 @@ function handleAction(action: string, message: Message): void {
|
|||||||
* @param {Message} message - The message to edit
|
* @param {Message} message - The message to edit
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function showEditDialog(message: Message): void {
|
async function showEditDialog(message: Message): Promise<void> {
|
||||||
if (!editDialog) return;
|
const textField = editDialog.querySelector('#edit-message-input') as TextField;
|
||||||
|
textField.value = message.content;
|
||||||
const textField = editDialog.querySelector('#edit-message-input') as any;
|
|
||||||
if (textField) {
|
|
||||||
textField.value = message.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentMessage = message;
|
currentMessage = message;
|
||||||
(editDialog as any).open = true;
|
editDialog.open = true;
|
||||||
|
|
||||||
// Focus the text field
|
// Focus the text field
|
||||||
setTimeout(() => {
|
await delay(100);
|
||||||
textField?.focus();
|
textField?.focus();
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,9 +179,7 @@ function showEditDialog(message: Message): void {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function hideEditDialog(): void {
|
function hideEditDialog(): void {
|
||||||
if (editDialog) {
|
editDialog.open = false;
|
||||||
(editDialog as any).open = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,9 +187,9 @@ function hideEditDialog(): void {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function saveEdit(): void {
|
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() || '';
|
const newContent = textField?.value?.trim() || '';
|
||||||
|
|
||||||
if (!newContent) {
|
if (!newContent) {
|
||||||
@@ -231,26 +230,21 @@ function saveEdit(): void {
|
|||||||
* @param {Message} message - The message to reply to
|
* @param {Message} message - The message to reply to
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function showReplyDialog(message: Message): void {
|
async function showReplyDialog(message: Message): Promise<void> {
|
||||||
if (!replyDialog) return;
|
|
||||||
|
|
||||||
const preview = replyDialog.querySelector('#reply-preview') as HTMLElement;
|
const preview = replyDialog.querySelector('#reply-preview') as HTMLElement;
|
||||||
if (preview) {
|
preview.innerHTML = `
|
||||||
preview.innerHTML = `
|
<div class="reply-preview-content">
|
||||||
<div class="reply-preview-content">
|
<strong>${message.username}</strong>: ${message.content}
|
||||||
<strong>${message.username}</strong>: ${message.content}
|
</div>
|
||||||
</div>
|
`;
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentMessage = message;
|
currentMessage = message;
|
||||||
(replyDialog as any).open = true;
|
replyDialog.open = true;
|
||||||
|
|
||||||
// Focus the text field
|
// Focus the text field
|
||||||
setTimeout(() => {
|
await delay(100);
|
||||||
const textField = replyDialog?.querySelector('#reply-message-input') as any;
|
const textField = replyDialog?.querySelector('#reply-message-input') as TextField;
|
||||||
textField?.focus();
|
textField?.focus();
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,9 +252,7 @@ function showReplyDialog(message: Message): void {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function hideReplyDialog(): void {
|
function hideReplyDialog(): void {
|
||||||
if (replyDialog) {
|
replyDialog.open = false;
|
||||||
(replyDialog as any).open = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -268,9 +260,9 @@ function hideReplyDialog(): void {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function sendReply(): void {
|
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() || '';
|
const content = textField?.value?.trim() || '';
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import 'mdui/components/text-field';
|
|||||||
import 'mdui/components/button-icon';
|
import 'mdui/components/button-icon';
|
||||||
import 'mdui/components/top-app-bar';
|
import 'mdui/components/top-app-bar';
|
||||||
import 'mdui/components/top-app-bar-title';
|
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';
|
import { setColorScheme } from 'mdui/functions/setColorScheme.js';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user