PageSourceSearch

https://pupwife.com/projects/chatroom/scripts/contextMenu.js

js pupwife.com collected 2026-09-30 15:07:06 UTC 9,291 bytes, 284 lines download raw bytes

1/**
2 * Context Menu System (Discord-inspired)
3 * Right-click context menu for messages
4 */
5
6window.ChatroomContextMenu = (function() {
7    'use strict';
8
9    let currentMenu = null;
10    let currentMessage = null;
11
12    function menuIcon(name) {
13        if (window.ChatroomIcons && window.ChatroomIcons.icon) {
14            return window.ChatroomIcons.icon(name);
15        }
16        return '';
17    }
18
19    function menuItem(action, iconName, label, extraClass) {
20        return (
21            '<button type="button" class="context-menu-item' + (extraClass ? ' ' + extraClass : '') + '" data-action="' + action + '">' +
22            '<span class="cr-menu-icon">' + menuIcon(iconName) + '</span>' +
23            '<span class="cr-menu-label">' + label + '</span>' +
24            '</button>'
25        );
26    }
27
28    /**
29     * Initialize context menu
30     */
31    function init() {
32        if (window.ChatroomIcons && window.ChatroomIcons.ensure) {
33            window.ChatroomIcons.ensure();
34        }
35
36        // Listen for right-clicks - prevent default context menu everywhere
37        document.addEventListener('contextmenu', (e) => {
38            e.preventDefault();
39
40            const message = e.target.closest('.message');
41            if (message) {
42                showContextMenu(e, message);
43            } else {
44                hideContextMenu();
45            }
46        });
47
48        document.addEventListener('click', (e) => {
49            if (currentMenu && !currentMenu.contains(e.target)) {
50                hideContextMenu();
51            }
52        });
53
54        document.addEventListener('scroll', hideContextMenu, true);
55    }
56
57    /**
58     * Show context menu
59     */
60    function getMessageUsername(message) {
61        const el = message.querySelector('.message-username');
62        if (!el) return '';
63        return el.textContent.replace(/👑/g, '').trim();
64    }
65
66    function showContextMenu(event, message) {
67        hideContextMenu();
68
69        currentMessage = message;
70        const messageId = message.dataset.messageId;
71        const username = getMessageUsername(message);
72        const currentUser = window.__sharedUsername ? window.__sharedUsername.get() : '';
73        const isModerator = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator();
74        const isOwnMessage = username && username === currentUser;
75
76        const menu = document.createElement('div');
77        menu.className = 'context-menu';
78        menu.style.position = 'fixed';
79        menu.style.left = event.clientX + 'px';
80        menu.style.top = event.clientY + 'px';
81        menu.style.zIndex = '3000';
82
83        let menuItems = '';
84
85        menuItems += menuItem('copy', 'copy', 'Copy Message');
86        if (isModerator) {
87            menuItems += menuItem('copy-id', 'key', 'Copy Message ID');
88        }
89        menuItems += menuItem('add-reaction', 'smile', 'Add Reaction');
90        menuItems += menuItem('reply', 'reply', 'Reply');
91        if (isModerator) {
92            menuItems += menuItem('pin', 'thumbtack', 'Pin Message');
93        }
94        if (isOwnMessage) {
95            menuItems += menuItem('edit', 'pen', 'Edit Message');
96        }
97        if (isOwnMessage || isModerator) {
98            menuItems += menuItem('delete', 'trash', 'Delete Message', 'context-menu-danger');
99        }
100
101        menuItems += '<div class="context-menu-divider"></div>';
102        menuItems += menuItem('profile', 'user', 'View Profile');
103        menuItems += menuItem('dm', 'envelope', 'Send Message');
104
105        menu.innerHTML = menuItems;
106        document.body.appendChild(menu);
107        currentMenu = menu;
108
109        const rect = menu.getBoundingClientRect();
110        let left = event.clientX;
111        let top = event.clientY;
112        if (rect.right > window.innerWidth) {
113            left = event.clientX - rect.width;
114        }
115        if (rect.bottom > window.innerHeight) {
116            top = event.clientY - rect.height;
117        }
118        menu.style.left = Math.max(8, left) + 'px';
119        menu.style.top = Math.max(8, top) + 'px';
120
121        menu.querySelectorAll('.context-menu-item').forEach(item => {
122            item.addEventListener('click', () => {
123                handleContextAction(item.dataset.action, message);
124                hideContextMenu();
125            });
126        });
127    }
128
129    /**
130     * Handle context menu action
131     */
132    function handleContextAction(action, message) {
133        const messageId = message.dataset.messageId;
134        const username = getMessageUsername(message);
135        const messageText = message.querySelector('.message-text')?.textContent || '';
136
137        switch (action) {
138            case 'copy':
139                navigator.clipboard.writeText(messageText).then(() => {
140                    showToast('Message copied to clipboard!');
141                });
142                break;
143
144            case 'copy-id':
145                navigator.clipboard.writeText(messageId).then(() => {
146                    showToast('Message ID copied!');
147                });
148                break;
149
150            case 'add-reaction':
151                if (window.ChatroomReactions) {
152                    window.ChatroomReactions.showReactionPicker(messageId);
153                }
154                break;
155
156            case 'reply':
157                if (window.ChatroomReplies) {
158                    window.ChatroomReplies.setPending(message);
159                }
160                break;
161
162            case 'delete':
163                if (window.ChatroomModerator) {
164                    window.ChatroomModerator.deleteMessage(messageId);
165                }
166                break;
167
168            case 'edit':
169                if (window.ChatroomMessageMeta && window.ChatroomMessageMeta.startEdit) {
170                    window.ChatroomMessageMeta.startEdit(message);
171                } else {
172                    editMessage(message);
173                }
174                break;
175
176            case 'profile':
177                if (window.ChatroomUserProfile) {
178                    window.ChatroomUserProfile.showProfile(username);
179                }
180                break;
181
182            case 'dm':
183                if (window.ChatroomDMs && username) {
184                    window.ChatroomDMs.openDM(username);
185                }
186                break;
187
188            case 'pin':
189                if (window.ChatroomMessageMeta && window.ChatroomMessageMeta.pinMessage) {
190                    window.ChatroomMessageMeta.pinMessage(message);
191                }
192                break;
193        }
194    }
195
196    /**
197     * Edit message
198     */
199    function editMessage(messageElement) {
200        const messageText = messageElement.querySelector('.message-text');
201        const originalText = messageText.textContent;
202
203        const input = document.createElement('textarea');
204        input.value = originalText;
205        input.className = 'message-edit-input';
206        input.style.width = '100%';
207        input.style.minHeight = '60px';
208        input.style.padding = '8px';
209        input.style.border = '2px solid var(--accent-2)';
210        input.style.borderRadius = '8px';
211        input.style.background = 'var(--bg-2)';
212        input.style.color = 'var(--text-2)';
213        input.style.fontFamily = 'inherit';
214        input.style.fontSize = 'inherit';
215
216        messageText.replaceWith(input);
217        input.focus();
218        input.select();
219
220        const saveEdit = () => {
221            const newText = input.value.trim();
222            if (newText && newText !== originalText) {
223                showToast('Message editing coming soon!');
224            }
225            input.replaceWith(messageText);
226        };
227
228        input.addEventListener('blur', saveEdit);
229        input.addEventListener('keydown', (e) => {
230            if (e.key === 'Enter' && !e.shiftKey) {
231                e.preventDefault();
232                saveEdit();
233            }
234            if (e.key === 'Escape') {
235                input.replaceWith(messageText);
236            }
237        });
238    }
239
240    function hideContextMenu() {
241        if (currentMenu) {
242            currentMenu.remove();
243            currentMenu = null;
244            currentMessage = null;
245        }
246    }
247
248    function showToast(message) {
249        const toast = document.createElement('div');
250        toast.className = 'toast';
251        toast.textContent = message;
252        document.body.appendChild(toast);
253
254        setTimeout(() => {
255            toast.classList.add('show');
256        }, 10);
257
258        setTimeout(() => {
259            toast.classList.remove('show');
260            setTimeout(() => toast.remove(), 300);
261        }, 2000);
262    }
263
264    if (document.readyState === 'loading') {
265        document.addEventListener('DOMContentLoaded', init);
266    } else {
267        init();
268    }
269
270    function showContextMenuAt(coords, message) {
271        showContextMenu({
272            clientX: coords.clientX,
273            clientY: coords.clientY,
274            preventDefault: function () {}
275        }, message);
276    }
277
278    return {
279        showContextMenu,
280        showContextMenuAt,
281        hideContextMenu,
282        init
283    };
284})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.