1
+// Message Interactions Handler for Touch/Pointer Devices
2
+import { getInputType } from "./device.js";
3
+
4
+// Track which messages currently have visible action buttons
5
+const visibleActionStates = new WeakSet();
6
+
7
+// Initialize interaction handlers for touch devices
8
+export function initializeMessageInteractions() {
9
+ const inputType = getInputType();
10
+
11
+ if (inputType === "touch") {
12
+ setupTouchInteractions();
13
+ }
14
+ // Pointer devices use hover states from CSS, no JS needed
15
+}
16
+
17
+// Set up touch device interactions
18
+function setupTouchInteractions() {
19
+ // Use event delegation on the chat history container
20
+ const chatHistory = document.getElementById("chat-history");
21
+ if (!chatHistory) return;
22
+
23
+ // Handle touch interactions for showing/hiding action buttons
24
+ chatHistory.addEventListener("click", handleTouchInteraction, { passive: false });
25
+
26
+ // Handle clicks outside messages to hide action buttons
27
+ document.addEventListener("click", handleOutsideClick, { passive: true });
28
+
29
+ // Prevent text selection when tapping action buttons area
30
+ chatHistory.addEventListener("selectstart", handleSelectStart, { passive: false });
31
+}
32
+
33
+// Handle touch interactions on messages
34
+function handleTouchInteraction(e) {
35
+ // Find the closest message container
36
+ const messageContainer = e.target.closest(".msg-content, .kvps-row, .message-text");
37
+ if (!messageContainer) return;
38
+
39
+ // If clicking on action buttons, let them handle their own events
40
+ if (e.target.closest(".action-buttons")) {
41
+ return;
42
+ }
43
+
44
+ // Toggle action buttons visibility
45
+ toggleActionButtons(messageContainer, e);
46
+}
47
+
48
+// Handle clicks outside of messages to hide action buttons
49
+function handleOutsideClick(e) {
50
+ // If clicking on a message or action button, don't hide
51
+ if (e.target.closest(".msg-content, .kvps-row, .message-text, .action-buttons")) {
52
+ return;
53
+ }
54
+
55
+ // Hide all visible action buttons
56
+ hideAllActionButtons();
57
+}
58
+
59
+// Prevent text selection when interacting with action area
60
+function handleSelectStart(e) {
61
+ // Allow text selection unless we're in the action buttons area
62
+ const actionButtons = e.target.closest(".action-buttons");
63
+ const messageContainer = e.target.closest(".msg-content, .kvps-row, .message-text");
64
+
65
+ if (actionButtons) {
66
+ e.preventDefault();
67
+ return false;
68
+ }
69
+
70
+ // If a message has visible action buttons, prevent selection to avoid conflicts
71
+ if (messageContainer && visibleActionStates.has(messageContainer)) {
72
+ // Allow selection after a short delay to distinguish from tap-to-show-buttons
73
+ setTimeout(() => {
74
+ if (visibleActionStates.has(messageContainer)) {
75
+ // Still visible, user might want to select text
76
+ return true;
77
+ }
78
+ }, 200);
79
+ }
80
+}
81
+
82
+// Toggle action buttons visibility for a message
83
+export function toggleActionButtons(messageContainer, event = null) {
84
+ if (!messageContainer) return;
85
+
86
+ const isCurrentlyVisible = visibleActionStates.has(messageContainer);
87
+
88
+ if (isCurrentlyVisible) {
89
+ hideActionButtons(messageContainer);
90
+ } else {
91
+ // Hide all other visible action buttons first
92
+ hideAllActionButtons();
93
+
94
+ // Show action buttons for this message
95
+ showActionButtons(messageContainer);
96
+ }
97
+
98
+ // Prevent event bubbling to avoid triggering document click handler
99
+ if (event) {
100
+ event.stopPropagation();
101
+ }
102
+}
103
+
104
+// Show action buttons for a message
105
+export function showActionButtons(messageContainer) {
106
+ if (!messageContainer) return;
107
+
108
+ messageContainer.classList.add("show-actions");
109
+ visibleActionStates.add(messageContainer);
110
+
111
+ // Add a slight delay to prevent immediate hiding from document click
112
+ setTimeout(() => {
113
+ messageContainer.setAttribute("data-actions-visible", "true");
114
+ }, 50);
115
+}
116
+
117
+// Hide action buttons for a message
118
+export function hideActionButtons(messageContainer) {
119
+ if (!messageContainer) return;
120
+
121
+ messageContainer.classList.remove("show-actions");
122
+ messageContainer.removeAttribute("data-actions-visible");
123
+ visibleActionStates.delete(messageContainer);
124
+}
125
+
126
+// Hide all visible action buttons
127
+export function hideAllActionButtons() {
128
+ const chatHistory = document.getElementById("chat-history");
129
+ if (!chatHistory) return;
130
+
131
+ // Find all messages with visible action buttons
132
+ const visibleMessages = chatHistory.querySelectorAll(".show-actions");
133
+ visibleMessages.forEach(message => {
134
+ hideActionButtons(message);
135
+ });
136
+}
137
+
138
+// Setup interaction for a specific message (called when message is created)
139
+export function setupMessageInteraction(messageContainer) {
140
+ if (!messageContainer) return;
141
+
142
+ const inputType = getInputType();
143
+
144
+ if (inputType === "touch") {
145
+ // For touch devices, add click handler
146
+ messageContainer.addEventListener("click", (e) => {
147
+ // Only handle if not clicking on action buttons
148
+ if (!e.target.closest(".action-buttons")) {
149
+ toggleActionButtons(messageContainer, e);
150
+ }
151
+ }, { passive: false });
152
+
153
+ // Prevent context menu on long press for action area
154
+ messageContainer.addEventListener("contextmenu", (e) => {
155
+ if (e.target.closest(".action-buttons")) {
156
+ e.preventDefault();
157
+ }
158
+ });
159
+ }
160
+
161
+ // For both touch and pointer devices, handle keyboard navigation
162
+ setupKeyboardNavigation(messageContainer);
163
+}
164
+
165
+// Set up keyboard navigation for accessibility
166
+function setupKeyboardNavigation(messageContainer) {
167
+ const actionButtons = messageContainer.querySelector(".action-buttons");
168
+ if (!actionButtons) return;
169
+
170
+ const buttons = actionButtons.querySelectorAll(".action-button");
171
+
172
+ buttons.forEach((button, index) => {
173
+ button.setAttribute("tabindex", "0");
174
+
175
+ button.addEventListener("keydown", (e) => {
176
+ switch (e.key) {
177
+ case "Enter":
178
+ case " ":
179
+ e.preventDefault();
180
+ button.click();
181
+ break;
182
+ case "ArrowLeft":
183
+ e.preventDefault();
184
+ const prevButton = buttons[index - 1] || buttons[buttons.length - 1];
185
+ prevButton.focus();
186
+ break;
187
+ case "ArrowRight":
188
+ e.preventDefault();
189
+ const nextButton = buttons[index + 1] || buttons[0];
190
+ nextButton.focus();
191
+ break;
192
+ case "Escape":
193
+ hideActionButtons(messageContainer);
194
+ messageContainer.focus();
195
+ break;
196
+ }
197
+ });
198
+ });
199
+}
200
+
201
+// Debounced scroll handler to manage viewport sticky buttons
202
+let scrollTimeout;
203
+export function handleScroll() {
204
+ clearTimeout(scrollTimeout);
205
+ scrollTimeout = setTimeout(() => {
206
+ // Check if any viewport-sticky buttons should be hidden
207
+ const stickyButtons = document.querySelectorAll(".action-buttons.viewport-sticky");
208
+ stickyButtons.forEach(buttons => {
209
+ const parent = buttons.closest(".msg-content, .kvps-row, .message-text");
210
+ if (parent) {
211
+ const rect = parent.getBoundingClientRect();
212
+ const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
213
+
214
+ if (isVisible) {
215
+ buttons.classList.remove("viewport-sticky");
216
+ }
217
+ }
218
+ });
219
+ }, 100);
220
+}
221
+
222
+// Initialize scroll handler
223
+export function initializeScrollHandler() {
224
+ window.addEventListener("scroll", handleScroll, { passive: true });
225
+ window.addEventListener("resize", handleScroll, { passive: true });
226
+}
227
+
228
+// Cleanup function
229
+export function cleanupMessageInteractions() {
230
+ window.removeEventListener("scroll", handleScroll);
231
+ window.removeEventListener("resize", handleScroll);
232
+
233
+ const chatHistory = document.getElementById("chat-history");
234
+ if (chatHistory) {
235
+ chatHistory.removeEventListener("click", handleTouchInteraction);
236
+ chatHistory.removeEventListener("selectstart", handleSelectStart);
237
+ }
238
+
239
+ document.removeEventListener("click", handleOutsideClick);
240
+
241
+ // Clear visible states
242
+ visibleActionStates.clear?.();
243
+}
\ No newline at end of file