@kerebron/extension-menu 0.4.27 → 0.4.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/CustomMenuPlugin.d.ts +117 -0
- package/esm/CustomMenuPlugin.d.ts.map +1 -0
- package/esm/CustomMenuPlugin.js +1634 -0
- package/esm/CustomMenuPlugin.js.map +1 -0
- package/esm/ExtensionCustomMenu.d.ts +11 -0
- package/esm/ExtensionCustomMenu.d.ts.map +1 -0
- package/esm/ExtensionCustomMenu.js +19 -0
- package/esm/ExtensionCustomMenu.js.map +1 -0
- package/esm/_dnt.shims.d.ts +2 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +58 -0
- package/esm/_dnt.shims.js.map +1 -0
- package/esm/buildMenu.d.ts +5 -0
- package/esm/buildMenu.d.ts.map +1 -0
- package/esm/buildMenu.js +447 -0
- package/esm/buildMenu.js.map +1 -0
- package/esm/icons.d.ts +15 -0
- package/esm/icons.d.ts.map +1 -0
- package/esm/icons.js +213 -0
- package/esm/icons.js.map +1 -0
- package/esm/menu.d.ts +83 -0
- package/esm/menu.d.ts.map +1 -0
- package/esm/menu.js +334 -0
- package/esm/menu.js.map +1 -0
- package/esm/mod.d.ts +3 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +3 -0
- package/esm/mod.js.map +1 -0
- package/esm/package.json +3 -0
- package/esm/prompt.d.ts +36 -0
- package/esm/prompt.d.ts.map +1 -0
- package/esm/prompt.js +165 -0
- package/esm/prompt.js.map +1 -0
- package/package.json +6 -3
- package/src/CustomMenuPlugin.ts +1996 -0
- package/src/ExtensionCustomMenu.ts +29 -0
- package/src/_dnt.shims.ts +60 -0
- package/src/buildMenu.ts +560 -0
- package/src/icons.ts +262 -0
- package/src/menu.ts +473 -0
- package/src/mod.ts +9 -0
- package/src/prompt.ts +205 -0
|
@@ -0,0 +1,1634 @@
|
|
|
1
|
+
// deno-lint-ignore-file no-window
|
|
2
|
+
import * as dntShim from "./_dnt.shims.js";
|
|
3
|
+
import { Plugin } from 'prosemirror-state';
|
|
4
|
+
import { getIcon } from './icons.js';
|
|
5
|
+
const CSS_PREFIX = 'kb-custom-menu';
|
|
6
|
+
const STORAGE_KEY = 'kb-custom-menu-order';
|
|
7
|
+
// Minimum width for overflow toggle button + some padding
|
|
8
|
+
const OVERFLOW_BUTTON_WIDTH = 48;
|
|
9
|
+
// Approximate width per toolbar item
|
|
10
|
+
const ITEM_WIDTH = 40;
|
|
11
|
+
// Delay before drag starts (ms) - user must hold for 2 seconds before dragging activates
|
|
12
|
+
const DRAG_START_DELAY = 2000;
|
|
13
|
+
export class CustomMenuView {
|
|
14
|
+
editorView;
|
|
15
|
+
editor;
|
|
16
|
+
content;
|
|
17
|
+
wrapper;
|
|
18
|
+
toolbar;
|
|
19
|
+
overflowMenu;
|
|
20
|
+
pinnedDropdownMenu = null;
|
|
21
|
+
tools = [];
|
|
22
|
+
root;
|
|
23
|
+
resizeHandle;
|
|
24
|
+
editorContainer;
|
|
25
|
+
closeOverflowHandler = null;
|
|
26
|
+
closePinnedDropdownHandler = null;
|
|
27
|
+
keydownHandler = null;
|
|
28
|
+
submenuStack = [];
|
|
29
|
+
pinnedDropdownStack = [];
|
|
30
|
+
resizeObserver = null;
|
|
31
|
+
// Drag and drop state
|
|
32
|
+
draggedItem = null;
|
|
33
|
+
dragStartTimer = null;
|
|
34
|
+
isDragging = false;
|
|
35
|
+
isDraggingFromOverflow = false;
|
|
36
|
+
dragPlaceholder = null;
|
|
37
|
+
dragGhost = null;
|
|
38
|
+
dragSourceWrapper = null;
|
|
39
|
+
// Current overflow tools (calculated during render)
|
|
40
|
+
currentOverflowTools = [];
|
|
41
|
+
// Default order of tools (stored on initialization)
|
|
42
|
+
defaultOrder = [];
|
|
43
|
+
// Currently focused toolbar item index for keyboard navigation
|
|
44
|
+
focusedToolbarIndex = -1;
|
|
45
|
+
constructor(editorView, editor, content) {
|
|
46
|
+
this.editorView = editorView;
|
|
47
|
+
this.editor = editor;
|
|
48
|
+
this.content = content;
|
|
49
|
+
this.root = editorView.root;
|
|
50
|
+
// Create wrapper
|
|
51
|
+
this.wrapper = document.createElement('div');
|
|
52
|
+
this.wrapper.classList.add(CSS_PREFIX + '__wrapper');
|
|
53
|
+
// Create toolbar
|
|
54
|
+
this.toolbar = document.createElement('div');
|
|
55
|
+
this.toolbar.classList.add(CSS_PREFIX);
|
|
56
|
+
this.toolbar.setAttribute('role', 'toolbar');
|
|
57
|
+
// Create editor container with resize handle
|
|
58
|
+
this.editorContainer = document.createElement('div');
|
|
59
|
+
this.editorContainer.classList.add(CSS_PREFIX + '__editor-container');
|
|
60
|
+
this.editorContainer.style.height = '50vh'; // Start at 50% height
|
|
61
|
+
// Create resize handle
|
|
62
|
+
this.resizeHandle = document.createElement('div');
|
|
63
|
+
this.resizeHandle.classList.add(CSS_PREFIX + '__resize-handle');
|
|
64
|
+
this.resizeHandle.innerHTML = '<div class="' + CSS_PREFIX +
|
|
65
|
+
'__resize-handle-bar"></div>';
|
|
66
|
+
// Mount toolbar before the editor DOM
|
|
67
|
+
this.wrapper.appendChild(this.toolbar);
|
|
68
|
+
if (editorView.dom.parentNode) {
|
|
69
|
+
editorView.dom.parentNode.replaceChild(this.wrapper, editorView.dom);
|
|
70
|
+
}
|
|
71
|
+
// Wrap editor in container and add resize handle
|
|
72
|
+
this.editorContainer.appendChild(editorView.dom);
|
|
73
|
+
this.editorContainer.appendChild(this.resizeHandle);
|
|
74
|
+
this.wrapper.appendChild(this.editorContainer);
|
|
75
|
+
// Create overflow menu (initially hidden)
|
|
76
|
+
this.overflowMenu = document.createElement('div');
|
|
77
|
+
this.overflowMenu.classList.add(CSS_PREFIX + '__overflow-menu');
|
|
78
|
+
this.overflowMenu.style.display = 'none';
|
|
79
|
+
this.wrapper.insertBefore(this.overflowMenu, this.editorContainer);
|
|
80
|
+
// Initialize tools from content
|
|
81
|
+
this.initializeTools();
|
|
82
|
+
// Load order state from localStorage
|
|
83
|
+
this.loadOrderState();
|
|
84
|
+
// Setup resize functionality
|
|
85
|
+
this.setupResize();
|
|
86
|
+
// Initial render
|
|
87
|
+
this.render();
|
|
88
|
+
// Use ResizeObserver to dynamically show/hide items based on available width
|
|
89
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
90
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
91
|
+
this.render();
|
|
92
|
+
});
|
|
93
|
+
this.resizeObserver.observe(this.toolbar);
|
|
94
|
+
}
|
|
95
|
+
// Setup keyboard navigation for accessibility
|
|
96
|
+
this.setupKeyboardNavigation();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Close all open menus (overflow menu and pinned dropdowns).
|
|
100
|
+
* Optionally returns focus to a specific element.
|
|
101
|
+
*/
|
|
102
|
+
closeAllMenus(returnFocusTo) {
|
|
103
|
+
// Close overflow menu
|
|
104
|
+
if (this.overflowMenu.style.display !== 'none') {
|
|
105
|
+
this.overflowMenu.style.display = 'none';
|
|
106
|
+
this.submenuStack = [];
|
|
107
|
+
const overflowToggle = this.toolbar.querySelector('.' + CSS_PREFIX + '__overflow-toggle');
|
|
108
|
+
if (overflowToggle) {
|
|
109
|
+
overflowToggle.setAttribute('aria-expanded', 'false');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Close pinned dropdown
|
|
113
|
+
if (this.pinnedDropdownMenu) {
|
|
114
|
+
this.pinnedDropdownMenu.remove();
|
|
115
|
+
this.pinnedDropdownMenu = null;
|
|
116
|
+
this.pinnedDropdownStack = [];
|
|
117
|
+
}
|
|
118
|
+
// Return focus if specified
|
|
119
|
+
if (returnFocusTo) {
|
|
120
|
+
returnFocusTo.focus();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get all focusable toolbar items (buttons).
|
|
125
|
+
*/
|
|
126
|
+
getToolbarButtons() {
|
|
127
|
+
const buttons = [];
|
|
128
|
+
const items = this.toolbar.querySelectorAll('.' + CSS_PREFIX + '__item');
|
|
129
|
+
items.forEach((item) => {
|
|
130
|
+
const button = item.querySelector('button');
|
|
131
|
+
if (button)
|
|
132
|
+
buttons.push(button);
|
|
133
|
+
});
|
|
134
|
+
// Add overflow toggle if present
|
|
135
|
+
const overflowToggle = this.toolbar.querySelector('.' + CSS_PREFIX + '__overflow-toggle');
|
|
136
|
+
if (overflowToggle)
|
|
137
|
+
buttons.push(overflowToggle);
|
|
138
|
+
return buttons;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Setup keyboard navigation for toolbar (WCAG toolbar pattern).
|
|
142
|
+
* - Left/Right arrows move between items
|
|
143
|
+
* - Home/End jump to first/last item
|
|
144
|
+
* - Escape closes menus
|
|
145
|
+
*/
|
|
146
|
+
setupKeyboardNavigation() {
|
|
147
|
+
this.keydownHandler = (e) => {
|
|
148
|
+
// Check if focus is within toolbar
|
|
149
|
+
const activeElement = document.activeElement;
|
|
150
|
+
const isInToolbar = this.toolbar.contains(activeElement);
|
|
151
|
+
const isInOverflowMenu = this.overflowMenu.contains(activeElement);
|
|
152
|
+
const isInPinnedDropdown = this.pinnedDropdownMenu?.contains(activeElement);
|
|
153
|
+
// Handle Escape key - close menus
|
|
154
|
+
if (e.key === 'Escape') {
|
|
155
|
+
if (isInPinnedDropdown || this.pinnedDropdownMenu) {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
const lastFocused = this.toolbar.querySelector('[data-last-focused="true"]');
|
|
158
|
+
this.closeAllMenus(lastFocused || undefined);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (isInOverflowMenu || this.overflowMenu.style.display !== 'none') {
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
const overflowToggle = this.toolbar.querySelector('.' + CSS_PREFIX + '__overflow-toggle');
|
|
164
|
+
this.closeAllMenus(overflowToggle || undefined);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Arrow key navigation within toolbar
|
|
169
|
+
if (isInToolbar && !isInOverflowMenu && !isInPinnedDropdown) {
|
|
170
|
+
const buttons = this.getToolbarButtons();
|
|
171
|
+
const currentIndex = buttons.indexOf(activeElement);
|
|
172
|
+
if (e.key === 'ArrowRight') {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
const nextIndex = (currentIndex + 1) % buttons.length;
|
|
175
|
+
buttons[nextIndex]?.focus();
|
|
176
|
+
}
|
|
177
|
+
else if (e.key === 'ArrowLeft') {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
const prevIndex = (currentIndex - 1 + buttons.length) %
|
|
180
|
+
buttons.length;
|
|
181
|
+
buttons[prevIndex]?.focus();
|
|
182
|
+
}
|
|
183
|
+
else if (e.key === 'Home') {
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
buttons[0]?.focus();
|
|
186
|
+
}
|
|
187
|
+
else if (e.key === 'End') {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
buttons[buttons.length - 1]?.focus();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Arrow key navigation within overflow menu
|
|
193
|
+
if (isInOverflowMenu) {
|
|
194
|
+
const focusableItems = Array.from(this.overflowMenu.querySelectorAll('button, [role="menuitem"], .' + CSS_PREFIX + '__overflow-item'));
|
|
195
|
+
const currentIndex = focusableItems.indexOf(activeElement);
|
|
196
|
+
if (e.key === 'ArrowDown') {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
const nextIndex = (currentIndex + 1) % focusableItems.length;
|
|
199
|
+
focusableItems[nextIndex]?.focus();
|
|
200
|
+
}
|
|
201
|
+
else if (e.key === 'ArrowUp') {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
const prevIndex = (currentIndex - 1 + focusableItems.length) %
|
|
204
|
+
focusableItems.length;
|
|
205
|
+
focusableItems[prevIndex]?.focus();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
document.addEventListener('keydown', this.keydownHandler);
|
|
210
|
+
}
|
|
211
|
+
initializeTools() {
|
|
212
|
+
let orderIndex = 0;
|
|
213
|
+
this.content.forEach((group, groupIndex) => {
|
|
214
|
+
group.forEach((element) => {
|
|
215
|
+
const { dom, update } = element.render(this.editorView);
|
|
216
|
+
// For dropdowns, get the label from the element's options directly
|
|
217
|
+
let label;
|
|
218
|
+
const dropdown = element;
|
|
219
|
+
if (dropdown.options && dropdown.options.label) {
|
|
220
|
+
label = dropdown.options.label;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
label = this.extractLabel(dom);
|
|
224
|
+
}
|
|
225
|
+
const id = this.generateStableId(label, dom);
|
|
226
|
+
this.tools.push({
|
|
227
|
+
id,
|
|
228
|
+
label,
|
|
229
|
+
element,
|
|
230
|
+
order: orderIndex++,
|
|
231
|
+
groupIndex,
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
// Store the default order (before any localStorage modifications)
|
|
236
|
+
this.defaultOrder = this.tools.map((t) => t.id);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Generate a stable ID from a label by converting it to a kebab-case slug.
|
|
240
|
+
* Falls back to a hash if the label is empty or contains only special characters.
|
|
241
|
+
*/
|
|
242
|
+
generateStableId(label, dom) {
|
|
243
|
+
// Try to use aria-label or data-id if available
|
|
244
|
+
const ariaLabel = dom.getAttribute('aria-label');
|
|
245
|
+
const dataId = dom.getAttribute('data-id');
|
|
246
|
+
if (dataId)
|
|
247
|
+
return `tool-${dataId}`;
|
|
248
|
+
const baseLabel = ariaLabel || label;
|
|
249
|
+
// Convert label to kebab-case slug
|
|
250
|
+
const slug = baseLabel
|
|
251
|
+
.toLowerCase()
|
|
252
|
+
.trim()
|
|
253
|
+
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
|
254
|
+
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
|
255
|
+
// If slug is empty, generate a simple hash from the label
|
|
256
|
+
if (!slug) {
|
|
257
|
+
const hash = this.simpleHash(baseLabel);
|
|
258
|
+
return `tool-${hash}`;
|
|
259
|
+
}
|
|
260
|
+
return `tool-${slug}`;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Simple hash function for generating stable IDs from strings.
|
|
264
|
+
*/
|
|
265
|
+
simpleHash(str) {
|
|
266
|
+
let hash = 0;
|
|
267
|
+
for (let i = 0; i < str.length; i++) {
|
|
268
|
+
const char = str.charCodeAt(i);
|
|
269
|
+
hash = ((hash << 5) - hash) + char;
|
|
270
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
271
|
+
}
|
|
272
|
+
return Math.abs(hash).toString(36);
|
|
273
|
+
}
|
|
274
|
+
extractLabel(dom) {
|
|
275
|
+
// For menu buttons, prioritize the visible label text over title
|
|
276
|
+
const buttonText = dom.querySelector('span')?.textContent?.trim();
|
|
277
|
+
if (buttonText)
|
|
278
|
+
return buttonText;
|
|
279
|
+
// For dropdowns, check the dropdown label
|
|
280
|
+
const dropdownLabel = dom.querySelector('.kb-dropdown__label')?.textContent
|
|
281
|
+
?.trim();
|
|
282
|
+
if (dropdownLabel)
|
|
283
|
+
return dropdownLabel;
|
|
284
|
+
// Try aria-label as fallback
|
|
285
|
+
const ariaLabel = dom.getAttribute('aria-label');
|
|
286
|
+
if (ariaLabel)
|
|
287
|
+
return ariaLabel;
|
|
288
|
+
// Try title attribute
|
|
289
|
+
const title = dom.getAttribute('title');
|
|
290
|
+
if (title)
|
|
291
|
+
return title;
|
|
292
|
+
// Fallback to looking at SVG title
|
|
293
|
+
const svgTitle = dom.querySelector('svg title')?.textContent?.trim();
|
|
294
|
+
if (svgTitle)
|
|
295
|
+
return svgTitle;
|
|
296
|
+
return 'Unknown Tool';
|
|
297
|
+
}
|
|
298
|
+
loadOrderState() {
|
|
299
|
+
try {
|
|
300
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
301
|
+
if (saved) {
|
|
302
|
+
const orderedIds = JSON.parse(saved);
|
|
303
|
+
// Apply saved order to tools
|
|
304
|
+
this.tools.forEach((tool) => {
|
|
305
|
+
const savedIndex = orderedIds.indexOf(tool.id);
|
|
306
|
+
if (savedIndex !== -1) {
|
|
307
|
+
tool.order = savedIndex;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
// New tools get placed at the end
|
|
311
|
+
tool.order = orderedIds.length + tool.order;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
// Sort tools by their order
|
|
315
|
+
this.tools.sort((a, b) => a.order - b.order);
|
|
316
|
+
}
|
|
317
|
+
// If no saved state, keep the default order from initialization
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
console.error('Failed to load order state:', e);
|
|
321
|
+
// Keep default order on error
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
saveOrderState() {
|
|
325
|
+
try {
|
|
326
|
+
const orderedIds = this.tools.map((t) => t.id);
|
|
327
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(orderedIds));
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
console.error('Failed to save order state:', e);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Check if the current order differs from the default order.
|
|
335
|
+
*/
|
|
336
|
+
hasCustomOrder() {
|
|
337
|
+
const currentOrder = this.tools.map((t) => t.id);
|
|
338
|
+
if (currentOrder.length !== this.defaultOrder.length)
|
|
339
|
+
return true;
|
|
340
|
+
return currentOrder.some((id, index) => id !== this.defaultOrder[index]);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Reset toolbar to default order, clearing any customizations.
|
|
344
|
+
*/
|
|
345
|
+
resetToDefault() {
|
|
346
|
+
// Clear localStorage
|
|
347
|
+
try {
|
|
348
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
console.error('Failed to clear order state:', e);
|
|
352
|
+
}
|
|
353
|
+
// Reset tool order based on default order
|
|
354
|
+
this.tools.forEach((tool) => {
|
|
355
|
+
const defaultIndex = this.defaultOrder.indexOf(tool.id);
|
|
356
|
+
if (defaultIndex !== -1) {
|
|
357
|
+
tool.order = defaultIndex;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
// Sort tools by their order
|
|
361
|
+
this.tools.sort((a, b) => a.order - b.order);
|
|
362
|
+
// Close overflow menu
|
|
363
|
+
this.closeAllMenus();
|
|
364
|
+
// Re-render
|
|
365
|
+
this.render();
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Create a reset button element.
|
|
369
|
+
* @param iconOnly - If true, show only the icon (for toolbar); if false, show icon + text (for overflow menu)
|
|
370
|
+
*/
|
|
371
|
+
createResetButton(iconOnly) {
|
|
372
|
+
const resetButton = document.createElement('button');
|
|
373
|
+
resetButton.type = 'button';
|
|
374
|
+
resetButton.classList.add(CSS_PREFIX + '__reset-button');
|
|
375
|
+
if (iconOnly) {
|
|
376
|
+
resetButton.classList.add(CSS_PREFIX + '__reset-button--icon-only');
|
|
377
|
+
// In toolbar, use button role
|
|
378
|
+
resetButton.setAttribute('role', 'button');
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
// In overflow menu, use menuitem role
|
|
382
|
+
resetButton.setAttribute('role', 'menuitem');
|
|
383
|
+
}
|
|
384
|
+
resetButton.setAttribute('tabindex', '0');
|
|
385
|
+
resetButton.setAttribute('aria-label', 'Reset toolbar to default order');
|
|
386
|
+
resetButton.title = 'Reset toolbar to default order';
|
|
387
|
+
const svg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
388
|
+
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
|
389
|
+
<path d="M3 3v5h5"/>
|
|
390
|
+
</svg>`;
|
|
391
|
+
if (iconOnly) {
|
|
392
|
+
resetButton.innerHTML = svg;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
resetButton.innerHTML = `${svg}<span>Reset to Default</span>`;
|
|
396
|
+
}
|
|
397
|
+
resetButton.addEventListener('click', (e) => {
|
|
398
|
+
e.preventDefault();
|
|
399
|
+
e.stopPropagation();
|
|
400
|
+
this.resetToDefault();
|
|
401
|
+
});
|
|
402
|
+
resetButton.addEventListener('keydown', (e) => {
|
|
403
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
e.stopPropagation();
|
|
406
|
+
this.resetToDefault();
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
return resetButton;
|
|
410
|
+
}
|
|
411
|
+
showSubmenu(tool) {
|
|
412
|
+
const dropdown = tool.element;
|
|
413
|
+
if (!dropdown.content)
|
|
414
|
+
return;
|
|
415
|
+
// Extract sub-items from dropdown
|
|
416
|
+
const subItems = dropdown.content.map((element, index) => {
|
|
417
|
+
const { dom } = element.render(this.editorView);
|
|
418
|
+
// For nested dropdowns, get the label from options directly
|
|
419
|
+
let label;
|
|
420
|
+
const nestedDropdown = element;
|
|
421
|
+
if (nestedDropdown.options && nestedDropdown.options.label) {
|
|
422
|
+
label = nestedDropdown.options.label;
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
label = this.extractLabel(dom);
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
id: `${tool.id}-sub-${index}`,
|
|
429
|
+
label,
|
|
430
|
+
element,
|
|
431
|
+
isPinned: false,
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
// Save current state to stack
|
|
435
|
+
this.submenuStack.push({
|
|
436
|
+
title: tool.label,
|
|
437
|
+
tools: subItems,
|
|
438
|
+
});
|
|
439
|
+
// Re-render to show submenu
|
|
440
|
+
this.renderOverflowMenu();
|
|
441
|
+
}
|
|
442
|
+
goBack() {
|
|
443
|
+
// Remove last submenu from stack
|
|
444
|
+
this.submenuStack.pop();
|
|
445
|
+
// Re-render
|
|
446
|
+
this.renderOverflowMenu();
|
|
447
|
+
}
|
|
448
|
+
showPinnedDropdown(tool, triggerElement) {
|
|
449
|
+
// Close overflow menu if open (ensure only one menu is open at a time)
|
|
450
|
+
if (this.overflowMenu.style.display !== 'none') {
|
|
451
|
+
this.overflowMenu.style.display = 'none';
|
|
452
|
+
this.submenuStack = [];
|
|
453
|
+
const overflowToggle = this.toolbar.querySelector('.' + CSS_PREFIX + '__overflow-toggle');
|
|
454
|
+
if (overflowToggle) {
|
|
455
|
+
overflowToggle.setAttribute('aria-expanded', 'false');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Close any existing pinned dropdown
|
|
459
|
+
if (this.pinnedDropdownMenu) {
|
|
460
|
+
this.pinnedDropdownMenu.remove();
|
|
461
|
+
this.pinnedDropdownMenu = null;
|
|
462
|
+
}
|
|
463
|
+
// Mark trigger as last focused for focus return on Escape
|
|
464
|
+
this.toolbar.querySelectorAll('[data-last-focused]').forEach((el) => {
|
|
465
|
+
el.removeAttribute('data-last-focused');
|
|
466
|
+
});
|
|
467
|
+
triggerElement.setAttribute('data-last-focused', 'true');
|
|
468
|
+
const dropdown = tool.element;
|
|
469
|
+
if (!dropdown.content)
|
|
470
|
+
return;
|
|
471
|
+
// Extract sub-items from dropdown
|
|
472
|
+
const subItems = dropdown.content.map((element, index) => {
|
|
473
|
+
const { dom } = element.render(this.editorView);
|
|
474
|
+
let label;
|
|
475
|
+
const nestedDropdown = element;
|
|
476
|
+
if (nestedDropdown.options && nestedDropdown.options.label) {
|
|
477
|
+
label = nestedDropdown.options.label;
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
label = this.extractLabel(dom);
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
id: `${tool.id}-sub-${index}`,
|
|
484
|
+
label,
|
|
485
|
+
element,
|
|
486
|
+
isPinned: false,
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
// Initialize stack with root menu
|
|
490
|
+
this.pinnedDropdownStack = [{
|
|
491
|
+
title: tool.label,
|
|
492
|
+
tools: subItems,
|
|
493
|
+
rootTool: tool,
|
|
494
|
+
}];
|
|
495
|
+
// Render the dropdown
|
|
496
|
+
this.renderPinnedDropdown(triggerElement);
|
|
497
|
+
}
|
|
498
|
+
pinnedDropdownGoBack() {
|
|
499
|
+
// Remove last item from stack
|
|
500
|
+
this.pinnedDropdownStack.pop();
|
|
501
|
+
// If stack is empty, close the dropdown
|
|
502
|
+
if (this.pinnedDropdownStack.length === 0) {
|
|
503
|
+
if (this.pinnedDropdownMenu) {
|
|
504
|
+
this.pinnedDropdownMenu.remove();
|
|
505
|
+
this.pinnedDropdownMenu = null;
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
// Re-render with previous menu
|
|
510
|
+
const triggerElement = this.toolbar.querySelector(`[data-tool-id="${this.pinnedDropdownStack[0].rootTool.id}"]`);
|
|
511
|
+
if (triggerElement) {
|
|
512
|
+
this.renderPinnedDropdown(triggerElement);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
pinnedDropdownShowSubmenu(tool, triggerElement) {
|
|
516
|
+
const dropdown = tool.element;
|
|
517
|
+
if (!dropdown.content)
|
|
518
|
+
return;
|
|
519
|
+
// Extract sub-items
|
|
520
|
+
const subItems = dropdown.content.map((element, index) => {
|
|
521
|
+
const { dom } = element.render(this.editorView);
|
|
522
|
+
let label;
|
|
523
|
+
const nestedDropdown = element;
|
|
524
|
+
if (nestedDropdown.options && nestedDropdown.options.label) {
|
|
525
|
+
label = nestedDropdown.options.label;
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
label = this.extractLabel(dom);
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
id: `${tool.id}-sub-${index}`,
|
|
532
|
+
label,
|
|
533
|
+
element,
|
|
534
|
+
isPinned: false,
|
|
535
|
+
};
|
|
536
|
+
});
|
|
537
|
+
// Add to stack
|
|
538
|
+
this.pinnedDropdownStack.push({
|
|
539
|
+
title: tool.label,
|
|
540
|
+
tools: subItems,
|
|
541
|
+
rootTool: this.pinnedDropdownStack[0].rootTool,
|
|
542
|
+
});
|
|
543
|
+
// Re-render
|
|
544
|
+
this.renderPinnedDropdown(triggerElement);
|
|
545
|
+
}
|
|
546
|
+
renderPinnedDropdown(triggerElement) {
|
|
547
|
+
// Remove existing dropdown
|
|
548
|
+
if (this.pinnedDropdownMenu) {
|
|
549
|
+
this.pinnedDropdownMenu.remove();
|
|
550
|
+
}
|
|
551
|
+
const currentLevel = this.pinnedDropdownStack[this.pinnedDropdownStack.length - 1];
|
|
552
|
+
const isSubmenu = this.pinnedDropdownStack.length > 1;
|
|
553
|
+
// Create dropdown menu
|
|
554
|
+
this.pinnedDropdownMenu = document.createElement('div');
|
|
555
|
+
this.pinnedDropdownMenu.classList.add(CSS_PREFIX + '__pinned-dropdown');
|
|
556
|
+
this.pinnedDropdownMenu.setAttribute('role', 'menu');
|
|
557
|
+
this.pinnedDropdownMenu.setAttribute('aria-label', currentLevel.title);
|
|
558
|
+
// Position it below the trigger element
|
|
559
|
+
const rect = triggerElement.getBoundingClientRect();
|
|
560
|
+
this.pinnedDropdownMenu.style.position = 'absolute';
|
|
561
|
+
this.pinnedDropdownMenu.style.top = `${rect.bottom + 4}px`;
|
|
562
|
+
this.pinnedDropdownMenu.style.left = `${rect.left}px`;
|
|
563
|
+
this.pinnedDropdownMenu.style.zIndex = '1000';
|
|
564
|
+
// Create scrollable content container
|
|
565
|
+
const overflowContent = document.createElement('div');
|
|
566
|
+
overflowContent.classList.add(CSS_PREFIX + '__overflow-content');
|
|
567
|
+
// Add back button if in submenu
|
|
568
|
+
if (isSubmenu) {
|
|
569
|
+
const header = document.createElement('div');
|
|
570
|
+
header.classList.add(CSS_PREFIX + '__overflow-submenu-header');
|
|
571
|
+
const backButton = document.createElement('button');
|
|
572
|
+
backButton.type = 'button';
|
|
573
|
+
backButton.classList.add(CSS_PREFIX + '__overflow-back-button');
|
|
574
|
+
backButton.setAttribute('aria-label', 'Go back to ' + currentLevel.title);
|
|
575
|
+
backButton.innerHTML =
|
|
576
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 19l-7-7 7-7"/></svg>';
|
|
577
|
+
backButton.addEventListener('click', (e) => {
|
|
578
|
+
e.preventDefault();
|
|
579
|
+
e.stopPropagation();
|
|
580
|
+
this.pinnedDropdownGoBack();
|
|
581
|
+
});
|
|
582
|
+
const title = document.createElement('span');
|
|
583
|
+
title.classList.add(CSS_PREFIX + '__overflow-submenu-title');
|
|
584
|
+
title.textContent = currentLevel.title;
|
|
585
|
+
header.appendChild(backButton);
|
|
586
|
+
header.appendChild(title);
|
|
587
|
+
overflowContent.appendChild(header);
|
|
588
|
+
}
|
|
589
|
+
// Render menu items
|
|
590
|
+
currentLevel.tools.forEach((subTool) => {
|
|
591
|
+
const isNestedDropdown = subTool.element.content !== undefined;
|
|
592
|
+
const wrapper = document.createElement('div');
|
|
593
|
+
wrapper.classList.add(CSS_PREFIX + '__overflow-item');
|
|
594
|
+
wrapper.setAttribute('data-tool-id', subTool.id);
|
|
595
|
+
wrapper.setAttribute('role', 'menuitem');
|
|
596
|
+
wrapper.setAttribute('tabindex', '0');
|
|
597
|
+
if (isNestedDropdown) {
|
|
598
|
+
// For nested dropdowns, show label and chevron
|
|
599
|
+
const label = document.createElement('span');
|
|
600
|
+
label.classList.add(CSS_PREFIX + '__overflow-item-label');
|
|
601
|
+
label.textContent = subTool.label;
|
|
602
|
+
wrapper.appendChild(label);
|
|
603
|
+
const chevron = document.createElement('span');
|
|
604
|
+
chevron.classList.add(CSS_PREFIX + '__overflow-item-chevron');
|
|
605
|
+
chevron.innerHTML =
|
|
606
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 5l7 7-7 7"/></svg>';
|
|
607
|
+
wrapper.appendChild(chevron);
|
|
608
|
+
wrapper.addEventListener('click', (e) => {
|
|
609
|
+
e.preventDefault();
|
|
610
|
+
e.stopPropagation();
|
|
611
|
+
this.pinnedDropdownShowSubmenu(subTool, triggerElement);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
// Regular menu item
|
|
616
|
+
const { dom } = subTool.element.render(this.editorView);
|
|
617
|
+
// Hide button's internal label
|
|
618
|
+
const spans = dom.querySelectorAll('span');
|
|
619
|
+
spans.forEach((span) => {
|
|
620
|
+
const isInsideIcon = span.closest('.kb-icon') !== null;
|
|
621
|
+
const isDirectChild = span.parentElement === dom;
|
|
622
|
+
if (span.textContent && span.textContent.trim() &&
|
|
623
|
+
!isInsideIcon &&
|
|
624
|
+
isDirectChild &&
|
|
625
|
+
!span.querySelector('svg') &&
|
|
626
|
+
!span.classList.contains('kb-icon')) {
|
|
627
|
+
span.style.display = 'none';
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
// Add label
|
|
631
|
+
const label = document.createElement('span');
|
|
632
|
+
label.classList.add(CSS_PREFIX + '__overflow-item-label');
|
|
633
|
+
label.textContent = subTool.label;
|
|
634
|
+
wrapper.appendChild(dom);
|
|
635
|
+
wrapper.appendChild(label);
|
|
636
|
+
// Make wrapper clickable
|
|
637
|
+
wrapper.addEventListener('mousedown', (e) => {
|
|
638
|
+
if (e.target !== dom) {
|
|
639
|
+
e.preventDefault();
|
|
640
|
+
const mousedownEvent = new MouseEvent('mousedown', {
|
|
641
|
+
bubbles: true,
|
|
642
|
+
cancelable: true,
|
|
643
|
+
view: dntShim.dntGlobalThis,
|
|
644
|
+
});
|
|
645
|
+
dom.dispatchEvent(mousedownEvent);
|
|
646
|
+
}
|
|
647
|
+
// Close dropdown after click
|
|
648
|
+
if (this.pinnedDropdownMenu) {
|
|
649
|
+
this.pinnedDropdownMenu.remove();
|
|
650
|
+
this.pinnedDropdownMenu = null;
|
|
651
|
+
this.pinnedDropdownStack = [];
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
overflowContent.appendChild(wrapper);
|
|
656
|
+
});
|
|
657
|
+
// Add content to dropdown
|
|
658
|
+
this.pinnedDropdownMenu.appendChild(overflowContent);
|
|
659
|
+
// Add to document
|
|
660
|
+
document.body.appendChild(this.pinnedDropdownMenu);
|
|
661
|
+
// Close on click outside
|
|
662
|
+
const doc = this.editorView.dom.ownerDocument || document;
|
|
663
|
+
setTimeout(() => {
|
|
664
|
+
if (this.closePinnedDropdownHandler) {
|
|
665
|
+
doc.removeEventListener('click', this.closePinnedDropdownHandler);
|
|
666
|
+
}
|
|
667
|
+
this.closePinnedDropdownHandler = (e) => {
|
|
668
|
+
const target = e.target;
|
|
669
|
+
if (this.pinnedDropdownMenu &&
|
|
670
|
+
!this.pinnedDropdownMenu.contains(target) &&
|
|
671
|
+
!triggerElement.contains(target)) {
|
|
672
|
+
this.pinnedDropdownMenu.remove();
|
|
673
|
+
this.pinnedDropdownMenu = null;
|
|
674
|
+
this.pinnedDropdownStack = [];
|
|
675
|
+
if (this.closePinnedDropdownHandler) {
|
|
676
|
+
doc.removeEventListener('click', this.closePinnedDropdownHandler);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
doc.addEventListener('click', this.closePinnedDropdownHandler);
|
|
681
|
+
}, 0);
|
|
682
|
+
}
|
|
683
|
+
renderOverflowMenu() {
|
|
684
|
+
// Clear overflow menu
|
|
685
|
+
this.overflowMenu.innerHTML = '';
|
|
686
|
+
// Set ARIA attributes for overflow menu
|
|
687
|
+
this.overflowMenu.setAttribute('role', 'menu');
|
|
688
|
+
this.overflowMenu.setAttribute('aria-label', 'More tools');
|
|
689
|
+
// Check if we're showing a submenu
|
|
690
|
+
const isSubmenu = this.submenuStack.length > 0;
|
|
691
|
+
const currentSubmenu = isSubmenu
|
|
692
|
+
? this.submenuStack[this.submenuStack.length - 1]
|
|
693
|
+
: null;
|
|
694
|
+
// Create scrollable content container - use grid layout for Google Docs style
|
|
695
|
+
const overflowContent = document.createElement('div');
|
|
696
|
+
overflowContent.classList.add(CSS_PREFIX + '__overflow-content');
|
|
697
|
+
// Apply flex-wrap style for Google Docs-like icon layout
|
|
698
|
+
if (!isSubmenu) {
|
|
699
|
+
overflowContent.classList.add(CSS_PREFIX + '__overflow-content--grid');
|
|
700
|
+
}
|
|
701
|
+
if (isSubmenu && currentSubmenu) {
|
|
702
|
+
// Render submenu header with back button
|
|
703
|
+
const header = document.createElement('div');
|
|
704
|
+
header.classList.add(CSS_PREFIX + '__overflow-submenu-header');
|
|
705
|
+
const backButton = document.createElement('button');
|
|
706
|
+
backButton.type = 'button';
|
|
707
|
+
backButton.classList.add(CSS_PREFIX + '__overflow-back-button');
|
|
708
|
+
backButton.setAttribute('aria-label', 'Go back to previous menu');
|
|
709
|
+
backButton.innerHTML =
|
|
710
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M15 19l-7-7 7-7"/></svg>';
|
|
711
|
+
backButton.addEventListener('click', (e) => {
|
|
712
|
+
e.preventDefault();
|
|
713
|
+
e.stopPropagation();
|
|
714
|
+
this.goBack();
|
|
715
|
+
});
|
|
716
|
+
const title = document.createElement('span');
|
|
717
|
+
title.classList.add(CSS_PREFIX + '__overflow-submenu-title');
|
|
718
|
+
title.textContent = currentSubmenu.title;
|
|
719
|
+
header.appendChild(backButton);
|
|
720
|
+
header.appendChild(title);
|
|
721
|
+
overflowContent.appendChild(header);
|
|
722
|
+
// Render submenu items
|
|
723
|
+
currentSubmenu.tools.forEach((tool) => {
|
|
724
|
+
// Check if this is a nested dropdown
|
|
725
|
+
const isDropdown = tool.element.content !== undefined;
|
|
726
|
+
const wrapper = document.createElement('div');
|
|
727
|
+
wrapper.classList.add(CSS_PREFIX + '__overflow-item');
|
|
728
|
+
wrapper.setAttribute('role', 'menuitem');
|
|
729
|
+
wrapper.setAttribute('tabindex', '0');
|
|
730
|
+
if (isDropdown) {
|
|
731
|
+
// For nested dropdowns, just show label and chevron (no icon/button)
|
|
732
|
+
wrapper.setAttribute('aria-haspopup', 'true');
|
|
733
|
+
// Add label
|
|
734
|
+
const label = document.createElement('span');
|
|
735
|
+
label.classList.add(CSS_PREFIX + '__overflow-item-label');
|
|
736
|
+
label.textContent = tool.label;
|
|
737
|
+
wrapper.appendChild(label);
|
|
738
|
+
// Add chevron to indicate submenu
|
|
739
|
+
const chevron = document.createElement('span');
|
|
740
|
+
chevron.classList.add(CSS_PREFIX + '__overflow-item-chevron');
|
|
741
|
+
chevron.setAttribute('aria-hidden', 'true');
|
|
742
|
+
chevron.innerHTML =
|
|
743
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 5l7 7-7 7"/></svg>';
|
|
744
|
+
wrapper.appendChild(chevron);
|
|
745
|
+
// Click and keyboard handler for nested dropdown - navigate deeper
|
|
746
|
+
const handleActivate = (e) => {
|
|
747
|
+
e.preventDefault();
|
|
748
|
+
e.stopPropagation();
|
|
749
|
+
this.showSubmenu(tool);
|
|
750
|
+
};
|
|
751
|
+
wrapper.addEventListener('click', handleActivate);
|
|
752
|
+
wrapper.addEventListener('keydown', (e) => {
|
|
753
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
754
|
+
handleActivate(e);
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
// Regular menu item
|
|
760
|
+
const { dom, update } = tool.element.render(this.editorView);
|
|
761
|
+
// Hide the button's internal label to avoid duplication
|
|
762
|
+
const internalLabel = dom.querySelector('span');
|
|
763
|
+
if (internalLabel) {
|
|
764
|
+
internalLabel.style.display = 'none';
|
|
765
|
+
}
|
|
766
|
+
// Add our own label to the item
|
|
767
|
+
const label = document.createElement('span');
|
|
768
|
+
label.classList.add(CSS_PREFIX + '__overflow-item-label');
|
|
769
|
+
label.textContent = tool.label;
|
|
770
|
+
// Restructure the DOM to show icon + label
|
|
771
|
+
wrapper.appendChild(dom);
|
|
772
|
+
wrapper.appendChild(label);
|
|
773
|
+
// Make the entire wrapper clickable by dispatching mousedown to the button
|
|
774
|
+
wrapper.addEventListener('mousedown', (e) => {
|
|
775
|
+
if (e.target !== dom) {
|
|
776
|
+
e.preventDefault();
|
|
777
|
+
const mousedownEvent = new MouseEvent('mousedown', {
|
|
778
|
+
bubbles: true,
|
|
779
|
+
cancelable: true,
|
|
780
|
+
view: dntShim.dntGlobalThis,
|
|
781
|
+
});
|
|
782
|
+
dom.dispatchEvent(mousedownEvent);
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
// Add keyboard support for Enter/Space
|
|
786
|
+
wrapper.addEventListener('keydown', (e) => {
|
|
787
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
788
|
+
e.preventDefault();
|
|
789
|
+
const mousedownEvent = new MouseEvent('mousedown', {
|
|
790
|
+
bubbles: true,
|
|
791
|
+
cancelable: true,
|
|
792
|
+
view: dntShim.dntGlobalThis,
|
|
793
|
+
});
|
|
794
|
+
dom.dispatchEvent(mousedownEvent);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
overflowContent.appendChild(wrapper);
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
// Render main overflow menu - use the stored overflow tools from render()
|
|
803
|
+
const allOverflowItems = this.currentOverflowTools;
|
|
804
|
+
// Render all overflow tools as icon grid (Google Docs style)
|
|
805
|
+
allOverflowItems.forEach((tool) => {
|
|
806
|
+
// Skip tools with empty or invalid labels
|
|
807
|
+
if (!tool.label || tool.label.trim() === '' ||
|
|
808
|
+
tool.label === 'Unknown Tool') {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
// Check if this is a dropdown with sub-items
|
|
812
|
+
const isDropdown = tool.element.content !== undefined;
|
|
813
|
+
const wrapper = document.createElement('div');
|
|
814
|
+
wrapper.classList.add(CSS_PREFIX + '__overflow-item');
|
|
815
|
+
wrapper.classList.add(CSS_PREFIX + '__overflow-item--grid');
|
|
816
|
+
wrapper.setAttribute('title', tool.label); // Tooltip on hover
|
|
817
|
+
wrapper.setAttribute('role', 'menuitem');
|
|
818
|
+
wrapper.setAttribute('tabindex', '0');
|
|
819
|
+
wrapper.setAttribute('aria-label', tool.label);
|
|
820
|
+
if (isDropdown) {
|
|
821
|
+
// For dropdowns, create a custom button with dropdown indicator
|
|
822
|
+
wrapper.classList.add(CSS_PREFIX + '__overflow-item--dropdown');
|
|
823
|
+
wrapper.setAttribute('aria-haspopup', 'true');
|
|
824
|
+
const button = document.createElement('button');
|
|
825
|
+
button.type = 'button';
|
|
826
|
+
button.classList.add('kb-menu__button');
|
|
827
|
+
button.setAttribute('tabindex', '-1'); // Parent is focusable
|
|
828
|
+
button.setAttribute('aria-hidden', 'true');
|
|
829
|
+
// Get the icon from the dropdown's options
|
|
830
|
+
const dropdown = tool.element;
|
|
831
|
+
if (dropdown.options?.icon) {
|
|
832
|
+
const icon = getIcon(this.root, dropdown.options.icon);
|
|
833
|
+
button.appendChild(icon);
|
|
834
|
+
}
|
|
835
|
+
wrapper.appendChild(button);
|
|
836
|
+
// Add small dropdown indicator
|
|
837
|
+
const indicator = document.createElement('span');
|
|
838
|
+
indicator.classList.add(CSS_PREFIX + '__overflow-item-indicator');
|
|
839
|
+
indicator.setAttribute('aria-hidden', 'true');
|
|
840
|
+
// CSS creates the triangle using borders
|
|
841
|
+
wrapper.appendChild(indicator);
|
|
842
|
+
// Click handler for dropdown - navigate to submenu
|
|
843
|
+
wrapper.addEventListener('click', (e) => {
|
|
844
|
+
e.preventDefault();
|
|
845
|
+
e.stopPropagation();
|
|
846
|
+
this.showSubmenu(tool);
|
|
847
|
+
});
|
|
848
|
+
// Add keyboard support for Enter/Space
|
|
849
|
+
wrapper.addEventListener('keydown', (e) => {
|
|
850
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
851
|
+
e.preventDefault();
|
|
852
|
+
e.stopPropagation();
|
|
853
|
+
this.showSubmenu(tool);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
// Regular menu item - show icon only with tooltip
|
|
859
|
+
const { dom, update } = tool.element.render(this.editorView);
|
|
860
|
+
// Hide any text labels, keep only icons
|
|
861
|
+
const spans = dom.querySelectorAll('span');
|
|
862
|
+
spans.forEach((span) => {
|
|
863
|
+
const isInsideIcon = span.closest('.kb-icon') !== null;
|
|
864
|
+
if (!isInsideIcon && !span.querySelector('svg')) {
|
|
865
|
+
span.style.display = 'none';
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
// Disable pointer events on the button itself - wrapper handles clicks
|
|
869
|
+
dom.style.pointerEvents = 'none';
|
|
870
|
+
wrapper.appendChild(dom);
|
|
871
|
+
// Store the button DOM for later use (click after drag timeout)
|
|
872
|
+
wrapper.dataset.hasButton = 'true';
|
|
873
|
+
wrapper._buttonDom =
|
|
874
|
+
dom;
|
|
875
|
+
// Add keyboard support for Enter/Space
|
|
876
|
+
wrapper.addEventListener('keydown', (e) => {
|
|
877
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
878
|
+
e.preventDefault();
|
|
879
|
+
// Close the overflow menu FIRST
|
|
880
|
+
this.closeAllMenus();
|
|
881
|
+
// Then trigger the action after menu is closed
|
|
882
|
+
requestAnimationFrame(() => {
|
|
883
|
+
const menuItem = tool.element;
|
|
884
|
+
if (menuItem.spec && typeof menuItem.spec.run === 'function') {
|
|
885
|
+
menuItem.spec.run(this.editorView.state, this.editorView.dispatch);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
// Add drag handlers for overflow items (only in main menu, not submenus)
|
|
892
|
+
if (!isSubmenu) {
|
|
893
|
+
this.setupOverflowDragHandlers(wrapper, tool);
|
|
894
|
+
}
|
|
895
|
+
overflowContent.appendChild(wrapper);
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
// Add the scrollable content to overflow menu
|
|
899
|
+
this.overflowMenu.appendChild(overflowContent);
|
|
900
|
+
// Add "Reset to Default" button if order has been customized (only in main menu, not submenus)
|
|
901
|
+
if (!isSubmenu && this.hasCustomOrder()) {
|
|
902
|
+
const footer = document.createElement('div');
|
|
903
|
+
footer.classList.add(CSS_PREFIX + '__overflow-footer');
|
|
904
|
+
const resetButton = this.createResetButton(false);
|
|
905
|
+
footer.appendChild(resetButton);
|
|
906
|
+
this.overflowMenu.appendChild(footer);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
render() {
|
|
910
|
+
// Skip render if currently dragging (to avoid interference)
|
|
911
|
+
if (this.isDragging)
|
|
912
|
+
return;
|
|
913
|
+
// Clear toolbar and overflow menu
|
|
914
|
+
this.toolbar.innerHTML = '';
|
|
915
|
+
this.overflowMenu.innerHTML = '';
|
|
916
|
+
// Get available width for toolbar items
|
|
917
|
+
const toolbarWidth = this.toolbar.offsetWidth || this.wrapper.offsetWidth;
|
|
918
|
+
const availableWidth = toolbarWidth - OVERFLOW_BUTTON_WIDTH - 16; // Reserve space for overflow button + padding
|
|
919
|
+
// First pass: render all items to measure their widths
|
|
920
|
+
const renderedItems = [];
|
|
921
|
+
this.tools.forEach((tool) => {
|
|
922
|
+
const wrapper = document.createElement('span');
|
|
923
|
+
wrapper.classList.add(CSS_PREFIX + '__item');
|
|
924
|
+
wrapper.setAttribute('data-tool-id', tool.id);
|
|
925
|
+
wrapper.setAttribute('draggable', 'false');
|
|
926
|
+
const isDropdown = tool.element.content !== undefined;
|
|
927
|
+
if (isDropdown) {
|
|
928
|
+
const { dom } = tool.element.render(this.editorView);
|
|
929
|
+
const dropdownMenu = dom.querySelector('.kb-dropdown__content');
|
|
930
|
+
if (dropdownMenu) {
|
|
931
|
+
dropdownMenu.style.display = 'none';
|
|
932
|
+
}
|
|
933
|
+
wrapper.appendChild(dom);
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
const { dom } = tool.element.render(this.editorView);
|
|
937
|
+
wrapper.appendChild(dom);
|
|
938
|
+
}
|
|
939
|
+
// Temporarily add to toolbar to measure
|
|
940
|
+
this.toolbar.appendChild(wrapper);
|
|
941
|
+
const width = wrapper.getBoundingClientRect().width;
|
|
942
|
+
renderedItems.push({ tool, wrapper, width });
|
|
943
|
+
});
|
|
944
|
+
// Second pass: determine which items fit
|
|
945
|
+
// Account for gaps between items (4px gap) and separators between groups
|
|
946
|
+
const GAP_SIZE = 4;
|
|
947
|
+
const SEPARATOR_WIDTH = 9; // 1px width + 4px margin on each side
|
|
948
|
+
let usedWidth = 0;
|
|
949
|
+
let previousGroupIndex = undefined;
|
|
950
|
+
const visibleItems = [];
|
|
951
|
+
const overflowItems = [];
|
|
952
|
+
for (const item of renderedItems) {
|
|
953
|
+
// Calculate extra width needed for gap and potentially a separator
|
|
954
|
+
let extraWidth = 0;
|
|
955
|
+
if (visibleItems.length > 0) {
|
|
956
|
+
extraWidth += GAP_SIZE; // Gap between items
|
|
957
|
+
// Add separator width if group is changing
|
|
958
|
+
if (previousGroupIndex !== undefined &&
|
|
959
|
+
item.tool.groupIndex !== previousGroupIndex) {
|
|
960
|
+
extraWidth += SEPARATOR_WIDTH + GAP_SIZE; // Separator + its gap
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (usedWidth + extraWidth + item.width <= availableWidth) {
|
|
964
|
+
visibleItems.push(item);
|
|
965
|
+
usedWidth += extraWidth + item.width;
|
|
966
|
+
previousGroupIndex = item.tool.groupIndex;
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
overflowItems.push(item);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// Clear toolbar again and re-render only visible items
|
|
973
|
+
this.toolbar.innerHTML = '';
|
|
974
|
+
// Track previous group index for inserting separators between groups
|
|
975
|
+
let prevGroupIndex = undefined;
|
|
976
|
+
// Render visible tools in toolbar with proper event handlers
|
|
977
|
+
visibleItems.forEach(({ tool, wrapper }) => {
|
|
978
|
+
// Insert separator if group changed (and not first item)
|
|
979
|
+
if (prevGroupIndex !== undefined &&
|
|
980
|
+
tool.groupIndex !== prevGroupIndex) {
|
|
981
|
+
const separator = document.createElement('div');
|
|
982
|
+
separator.classList.add(CSS_PREFIX + '__separator');
|
|
983
|
+
this.toolbar.appendChild(separator);
|
|
984
|
+
}
|
|
985
|
+
prevGroupIndex = tool.groupIndex;
|
|
986
|
+
const isDropdown = tool.element.content !== undefined;
|
|
987
|
+
if (isDropdown) {
|
|
988
|
+
const button = wrapper.querySelector('button');
|
|
989
|
+
const label = wrapper.querySelector('.kb-dropdown__label');
|
|
990
|
+
const clickHandler = (e) => {
|
|
991
|
+
// Don't open dropdown if we were dragging
|
|
992
|
+
if (this.isDragging) {
|
|
993
|
+
e.preventDefault();
|
|
994
|
+
e.stopPropagation();
|
|
995
|
+
e.stopImmediatePropagation();
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
e.preventDefault();
|
|
999
|
+
e.stopPropagation();
|
|
1000
|
+
e.stopImmediatePropagation();
|
|
1001
|
+
this.showPinnedDropdown(tool, wrapper);
|
|
1002
|
+
};
|
|
1003
|
+
// Mousedown handler - prevents built-in dropdown from opening
|
|
1004
|
+
// but allows our drag handler on the wrapper to work
|
|
1005
|
+
const mousedownHandler = (e) => {
|
|
1006
|
+
// Stop the built-in dropdown's mousedown handler from firing
|
|
1007
|
+
// This prevents the double-menu issue
|
|
1008
|
+
e.stopImmediatePropagation();
|
|
1009
|
+
// If dragging, also stop propagation
|
|
1010
|
+
if (this.isDragging) {
|
|
1011
|
+
e.stopPropagation();
|
|
1012
|
+
}
|
|
1013
|
+
// Note: We don't call preventDefault() so the wrapper's
|
|
1014
|
+
// capture-phase drag handler can still work
|
|
1015
|
+
};
|
|
1016
|
+
if (button) {
|
|
1017
|
+
button.addEventListener('click', clickHandler, { capture: true });
|
|
1018
|
+
button.addEventListener('mousedown', mousedownHandler, {
|
|
1019
|
+
capture: true,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
if (label) {
|
|
1023
|
+
label.addEventListener('click', clickHandler, { capture: true });
|
|
1024
|
+
label.addEventListener('mousedown', mousedownHandler, {
|
|
1025
|
+
capture: true,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
// Add drag-and-drop handlers
|
|
1030
|
+
this.setupDragHandlers(wrapper, tool);
|
|
1031
|
+
this.toolbar.appendChild(wrapper);
|
|
1032
|
+
});
|
|
1033
|
+
// Store overflow tools for the overflow menu
|
|
1034
|
+
this.currentOverflowTools = overflowItems.map((item) => item.tool);
|
|
1035
|
+
// Add separator before overflow button if there are overflow items
|
|
1036
|
+
if (this.currentOverflowTools.length > 0) {
|
|
1037
|
+
const separator = document.createElement('div');
|
|
1038
|
+
separator.classList.add(CSS_PREFIX + '__separator');
|
|
1039
|
+
this.toolbar.appendChild(separator);
|
|
1040
|
+
}
|
|
1041
|
+
// Add overflow toggle button if there are overflow items
|
|
1042
|
+
if (this.currentOverflowTools.length > 0) {
|
|
1043
|
+
const overflowToggle = document.createElement('button');
|
|
1044
|
+
overflowToggle.type = 'button';
|
|
1045
|
+
overflowToggle.className = CSS_PREFIX + '__overflow-toggle';
|
|
1046
|
+
overflowToggle.setAttribute('aria-haspopup', 'menu');
|
|
1047
|
+
overflowToggle.setAttribute('aria-expanded', 'false');
|
|
1048
|
+
overflowToggle.setAttribute('aria-label', 'More tools');
|
|
1049
|
+
overflowToggle.title = 'More tools';
|
|
1050
|
+
overflowToggle.innerHTML = `
|
|
1051
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
1052
|
+
<circle cx="5" cy="12" r="2"/>
|
|
1053
|
+
<circle cx="12" cy="12" r="2"/>
|
|
1054
|
+
<circle cx="19" cy="12" r="2"/>
|
|
1055
|
+
</svg>
|
|
1056
|
+
`;
|
|
1057
|
+
overflowToggle.addEventListener('click', (e) => {
|
|
1058
|
+
e.preventDefault();
|
|
1059
|
+
e.stopPropagation();
|
|
1060
|
+
// Close any open pinned dropdown first (only one menu open at a time)
|
|
1061
|
+
if (this.pinnedDropdownMenu) {
|
|
1062
|
+
this.pinnedDropdownMenu.remove();
|
|
1063
|
+
this.pinnedDropdownMenu = null;
|
|
1064
|
+
this.pinnedDropdownStack = [];
|
|
1065
|
+
}
|
|
1066
|
+
const isOpen = this.overflowMenu.style.display !== 'none';
|
|
1067
|
+
this.overflowMenu.style.display = isOpen ? 'none' : 'block';
|
|
1068
|
+
overflowToggle.setAttribute('aria-expanded', String(!isOpen));
|
|
1069
|
+
// Add/remove close handler based on open state
|
|
1070
|
+
const doc = this.editorView.dom.ownerDocument || document;
|
|
1071
|
+
if (!isOpen) {
|
|
1072
|
+
// Opening - reset submenu stack to show main menu
|
|
1073
|
+
this.submenuStack = [];
|
|
1074
|
+
this.renderOverflowMenu();
|
|
1075
|
+
// Opening - add close handler after a short delay
|
|
1076
|
+
setTimeout(() => {
|
|
1077
|
+
if (this.closeOverflowHandler) {
|
|
1078
|
+
doc.removeEventListener('click', this.closeOverflowHandler);
|
|
1079
|
+
}
|
|
1080
|
+
this.closeOverflowHandler = (e) => {
|
|
1081
|
+
// Don't interfere with editor clicks
|
|
1082
|
+
const target = e.target;
|
|
1083
|
+
const editorDom = this.editorView.dom;
|
|
1084
|
+
// Check if click is inside editor
|
|
1085
|
+
if (editorDom.contains(target)) {
|
|
1086
|
+
return; // Let editor handle it
|
|
1087
|
+
}
|
|
1088
|
+
if (!this.overflowMenu.contains(target) &&
|
|
1089
|
+
!this.toolbar.contains(target)) {
|
|
1090
|
+
this.overflowMenu.style.display = 'none';
|
|
1091
|
+
overflowToggle.setAttribute('aria-expanded', 'false');
|
|
1092
|
+
// Clear submenu stack when closing
|
|
1093
|
+
this.submenuStack = [];
|
|
1094
|
+
if (this.closeOverflowHandler) {
|
|
1095
|
+
doc.removeEventListener('click', this.closeOverflowHandler);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
doc.addEventListener('click', this.closeOverflowHandler);
|
|
1100
|
+
}, 0);
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
// Closing - remove close handler and clear submenu stack
|
|
1104
|
+
if (this.closeOverflowHandler) {
|
|
1105
|
+
doc.removeEventListener('click', this.closeOverflowHandler);
|
|
1106
|
+
}
|
|
1107
|
+
this.submenuStack = [];
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
this.toolbar.appendChild(overflowToggle);
|
|
1111
|
+
}
|
|
1112
|
+
else if (this.hasCustomOrder()) {
|
|
1113
|
+
// No overflow items but order has been customized - show reset button in toolbar
|
|
1114
|
+
const separator = document.createElement('div');
|
|
1115
|
+
separator.classList.add(CSS_PREFIX + '__separator');
|
|
1116
|
+
this.toolbar.appendChild(separator);
|
|
1117
|
+
const resetButton = this.createResetButton(true);
|
|
1118
|
+
this.toolbar.appendChild(resetButton);
|
|
1119
|
+
}
|
|
1120
|
+
// Render overflow menu content
|
|
1121
|
+
this.renderOverflowMenu();
|
|
1122
|
+
}
|
|
1123
|
+
setupDragHandlers(wrapper, tool) {
|
|
1124
|
+
let startX = 0;
|
|
1125
|
+
let startY = 0;
|
|
1126
|
+
const onMouseDown = (e) => {
|
|
1127
|
+
// Only handle left mouse button
|
|
1128
|
+
if (e.button !== 0)
|
|
1129
|
+
return;
|
|
1130
|
+
startX = e.clientX;
|
|
1131
|
+
startY = e.clientY;
|
|
1132
|
+
// Start a timer for delayed drag initiation
|
|
1133
|
+
this.dragStartTimer = setTimeout(() => {
|
|
1134
|
+
this.startDrag(wrapper, tool, e);
|
|
1135
|
+
}, DRAG_START_DELAY);
|
|
1136
|
+
// Listen for mouse up to cancel if released early
|
|
1137
|
+
const onMouseUp = () => {
|
|
1138
|
+
if (this.dragStartTimer) {
|
|
1139
|
+
clearTimeout(this.dragStartTimer);
|
|
1140
|
+
this.dragStartTimer = null;
|
|
1141
|
+
}
|
|
1142
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
1143
|
+
document.removeEventListener('mousemove', onEarlyMove);
|
|
1144
|
+
};
|
|
1145
|
+
// If mouse moves too much before timer, cancel drag start
|
|
1146
|
+
const onEarlyMove = (moveEvent) => {
|
|
1147
|
+
const dx = Math.abs(moveEvent.clientX - startX);
|
|
1148
|
+
const dy = Math.abs(moveEvent.clientY - startY);
|
|
1149
|
+
// Allow some tolerance for slight movement
|
|
1150
|
+
if (dx > 5 || dy > 5) {
|
|
1151
|
+
if (this.dragStartTimer) {
|
|
1152
|
+
clearTimeout(this.dragStartTimer);
|
|
1153
|
+
this.dragStartTimer = null;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
1158
|
+
document.addEventListener('mousemove', onEarlyMove);
|
|
1159
|
+
};
|
|
1160
|
+
// Use capture phase to ensure we get events from child elements (like dropdown buttons)
|
|
1161
|
+
wrapper.addEventListener('mousedown', onMouseDown, { capture: true });
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Set up drag handlers for overflow menu items.
|
|
1165
|
+
* Long-hold initiates drag to toolbar, short click activates the item.
|
|
1166
|
+
*/
|
|
1167
|
+
setupOverflowDragHandlers(wrapper, tool) {
|
|
1168
|
+
let startX = 0;
|
|
1169
|
+
let startY = 0;
|
|
1170
|
+
let dragInitiated = false;
|
|
1171
|
+
const onMouseDown = (e) => {
|
|
1172
|
+
// Only handle left mouse button
|
|
1173
|
+
if (e.button !== 0)
|
|
1174
|
+
return;
|
|
1175
|
+
startX = e.clientX;
|
|
1176
|
+
startY = e.clientY;
|
|
1177
|
+
dragInitiated = false;
|
|
1178
|
+
// Prevent default to stop button action from firing immediately
|
|
1179
|
+
e.preventDefault();
|
|
1180
|
+
// Start a timer for delayed drag initiation
|
|
1181
|
+
this.dragStartTimer = setTimeout(() => {
|
|
1182
|
+
dragInitiated = true;
|
|
1183
|
+
this.startOverflowDrag(wrapper, tool, e);
|
|
1184
|
+
}, DRAG_START_DELAY);
|
|
1185
|
+
// Listen for mouse up to cancel if released early (allows normal click)
|
|
1186
|
+
const onMouseUp = () => {
|
|
1187
|
+
if (this.dragStartTimer) {
|
|
1188
|
+
clearTimeout(this.dragStartTimer);
|
|
1189
|
+
this.dragStartTimer = null;
|
|
1190
|
+
}
|
|
1191
|
+
// If drag wasn't initiated, this was a short click - trigger the button action
|
|
1192
|
+
if (!dragInitiated) {
|
|
1193
|
+
// Close the overflow menu FIRST to prevent it from capturing modal events
|
|
1194
|
+
this.closeAllMenus();
|
|
1195
|
+
// Then trigger the action after the menu is closed
|
|
1196
|
+
// Use requestAnimationFrame to ensure DOM is updated
|
|
1197
|
+
requestAnimationFrame(() => {
|
|
1198
|
+
// Call the spec's run method directly instead of dispatching DOM events
|
|
1199
|
+
const menuItem = tool.element;
|
|
1200
|
+
if (menuItem.spec && typeof menuItem.spec.run === 'function') {
|
|
1201
|
+
menuItem.spec.run(this.editorView.state, this.editorView.dispatch);
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
1206
|
+
document.removeEventListener('mousemove', onEarlyMove);
|
|
1207
|
+
};
|
|
1208
|
+
// If mouse moves too much before timer, cancel drag start
|
|
1209
|
+
const onEarlyMove = (moveEvent) => {
|
|
1210
|
+
const dx = Math.abs(moveEvent.clientX - startX);
|
|
1211
|
+
const dy = Math.abs(moveEvent.clientY - startY);
|
|
1212
|
+
// Allow some tolerance for slight movement
|
|
1213
|
+
if (dx > 5 || dy > 5) {
|
|
1214
|
+
if (this.dragStartTimer) {
|
|
1215
|
+
clearTimeout(this.dragStartTimer);
|
|
1216
|
+
this.dragStartTimer = null;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
1221
|
+
document.addEventListener('mousemove', onEarlyMove);
|
|
1222
|
+
};
|
|
1223
|
+
// Use capture phase and stop propagation to prevent other handlers
|
|
1224
|
+
wrapper.addEventListener('mousedown', onMouseDown, { capture: true });
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Start dragging an item from the overflow menu to the toolbar.
|
|
1228
|
+
*/
|
|
1229
|
+
startOverflowDrag(wrapper, tool, e) {
|
|
1230
|
+
this.isDragging = true;
|
|
1231
|
+
this.isDraggingFromOverflow = true;
|
|
1232
|
+
this.draggedItem = tool;
|
|
1233
|
+
this.dragSourceWrapper = wrapper;
|
|
1234
|
+
// Add dragging class to wrapper
|
|
1235
|
+
wrapper.classList.add(CSS_PREFIX + '__overflow-item--dragging');
|
|
1236
|
+
this.wrapper.classList.add(CSS_PREFIX + '__wrapper--dragging');
|
|
1237
|
+
// Close the overflow menu to allow dropping on toolbar
|
|
1238
|
+
this.closeOverflowMenu();
|
|
1239
|
+
// Create a ghost element for visual feedback
|
|
1240
|
+
const rect = wrapper.getBoundingClientRect();
|
|
1241
|
+
this.dragGhost = wrapper.cloneNode(true);
|
|
1242
|
+
this.dragGhost.classList.add(CSS_PREFIX + '__drag-ghost');
|
|
1243
|
+
this.dragGhost.style.position = 'fixed';
|
|
1244
|
+
this.dragGhost.style.left = `${rect.left}px`;
|
|
1245
|
+
this.dragGhost.style.top = `${rect.top}px`;
|
|
1246
|
+
this.dragGhost.style.width = `${rect.width}px`;
|
|
1247
|
+
this.dragGhost.style.height = `${rect.height}px`;
|
|
1248
|
+
this.dragGhost.style.pointerEvents = 'none';
|
|
1249
|
+
this.dragGhost.style.zIndex = '10000';
|
|
1250
|
+
document.body.appendChild(this.dragGhost);
|
|
1251
|
+
// Create a placeholder to show where the item will be dropped
|
|
1252
|
+
this.dragPlaceholder = document.createElement('span');
|
|
1253
|
+
this.dragPlaceholder.classList.add(CSS_PREFIX + '__drop-placeholder');
|
|
1254
|
+
const onMouseMove = (moveEvent) => {
|
|
1255
|
+
if (!this.isDragging || !this.dragGhost)
|
|
1256
|
+
return;
|
|
1257
|
+
// Update ghost position
|
|
1258
|
+
const ghostRect = this.dragGhost.getBoundingClientRect();
|
|
1259
|
+
this.dragGhost.style.left = `${moveEvent.clientX - ghostRect.width / 2}px`;
|
|
1260
|
+
this.dragGhost.style.top = `${moveEvent.clientY - ghostRect.height / 2}px`;
|
|
1261
|
+
// Check if we're over the toolbar area
|
|
1262
|
+
const toolbarRect = this.toolbar.getBoundingClientRect();
|
|
1263
|
+
const isOverToolbar = moveEvent.clientY >= toolbarRect.top &&
|
|
1264
|
+
moveEvent.clientY <= toolbarRect.bottom &&
|
|
1265
|
+
moveEvent.clientX >= toolbarRect.left &&
|
|
1266
|
+
moveEvent.clientX <= toolbarRect.right;
|
|
1267
|
+
// Find the toolbar item we're hovering over
|
|
1268
|
+
const toolbarItems = Array.from(this.toolbar.querySelectorAll('.' + CSS_PREFIX + '__item'));
|
|
1269
|
+
let insertBefore = null;
|
|
1270
|
+
if (isOverToolbar) {
|
|
1271
|
+
for (let i = 0; i < toolbarItems.length; i++) {
|
|
1272
|
+
const item = toolbarItems[i];
|
|
1273
|
+
const itemRect = item.getBoundingClientRect();
|
|
1274
|
+
const itemCenter = itemRect.left + itemRect.width / 2;
|
|
1275
|
+
if (moveEvent.clientX < itemCenter && insertBefore === null) {
|
|
1276
|
+
insertBefore = item;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
// Remove existing placeholder
|
|
1281
|
+
if (this.dragPlaceholder && this.dragPlaceholder.parentNode) {
|
|
1282
|
+
this.dragPlaceholder.remove();
|
|
1283
|
+
}
|
|
1284
|
+
// Only show placeholder if over toolbar
|
|
1285
|
+
if (isOverToolbar && this.dragPlaceholder) {
|
|
1286
|
+
if (insertBefore) {
|
|
1287
|
+
insertBefore.parentNode?.insertBefore(this.dragPlaceholder, insertBefore);
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
// Insert at end (before separator or overflow toggle)
|
|
1291
|
+
const separator = this.toolbar.querySelector('.' + CSS_PREFIX + '__separator');
|
|
1292
|
+
if (separator) {
|
|
1293
|
+
separator.parentNode?.insertBefore(this.dragPlaceholder, separator);
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1296
|
+
const overflowToggle = this.toolbar.querySelector('.' + CSS_PREFIX + '__overflow-toggle');
|
|
1297
|
+
if (overflowToggle) {
|
|
1298
|
+
overflowToggle.parentNode?.insertBefore(this.dragPlaceholder, overflowToggle);
|
|
1299
|
+
}
|
|
1300
|
+
else {
|
|
1301
|
+
this.toolbar.appendChild(this.dragPlaceholder);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
const onMouseUp = (_upEvent) => {
|
|
1308
|
+
if (!this.isDragging)
|
|
1309
|
+
return;
|
|
1310
|
+
// Calculate new position based on placeholder
|
|
1311
|
+
if (this.dragPlaceholder && this.dragPlaceholder.parentNode) {
|
|
1312
|
+
// Count toolbar items before the placeholder to determine insert position
|
|
1313
|
+
let insertAtIndex = 0;
|
|
1314
|
+
for (let i = 0; i < this.toolbar.children.length; i++) {
|
|
1315
|
+
const child = this.toolbar.children[i];
|
|
1316
|
+
if (child === this.dragPlaceholder)
|
|
1317
|
+
break;
|
|
1318
|
+
if (child.classList.contains(CSS_PREFIX + '__item')) {
|
|
1319
|
+
insertAtIndex++;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
// Find the tool's current index in the tools array
|
|
1323
|
+
const currentToolIndex = this.tools.indexOf(tool);
|
|
1324
|
+
if (currentToolIndex !== -1) {
|
|
1325
|
+
// Remove the tool from its current position
|
|
1326
|
+
this.tools.splice(currentToolIndex, 1);
|
|
1327
|
+
// Insert at new position (at the target index)
|
|
1328
|
+
this.tools.splice(insertAtIndex, 0, tool);
|
|
1329
|
+
// Update order values
|
|
1330
|
+
this.tools.forEach((t, i) => {
|
|
1331
|
+
t.order = i;
|
|
1332
|
+
});
|
|
1333
|
+
// Save to localStorage
|
|
1334
|
+
this.saveOrderState();
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
// Clean up
|
|
1338
|
+
this.cleanupOverflowDrag();
|
|
1339
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
1340
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
1341
|
+
// Re-render after a short delay
|
|
1342
|
+
setTimeout(() => {
|
|
1343
|
+
this.render();
|
|
1344
|
+
}, 0);
|
|
1345
|
+
};
|
|
1346
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
1347
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Close the overflow menu programmatically.
|
|
1351
|
+
*/
|
|
1352
|
+
closeOverflowMenu() {
|
|
1353
|
+
if (this.overflowMenu) {
|
|
1354
|
+
this.overflowMenu.style.display = 'none';
|
|
1355
|
+
}
|
|
1356
|
+
// Update the overflow toggle button state
|
|
1357
|
+
const overflowToggle = this.toolbar.querySelector('.' + CSS_PREFIX + '__overflow-toggle');
|
|
1358
|
+
if (overflowToggle) {
|
|
1359
|
+
overflowToggle.setAttribute('aria-expanded', 'false');
|
|
1360
|
+
}
|
|
1361
|
+
// Clear submenu stack
|
|
1362
|
+
this.submenuStack = [];
|
|
1363
|
+
// Remove close handler
|
|
1364
|
+
const doc = this.editorView.dom.ownerDocument || document;
|
|
1365
|
+
if (this.closeOverflowHandler) {
|
|
1366
|
+
doc.removeEventListener('click', this.closeOverflowHandler);
|
|
1367
|
+
this.closeOverflowHandler = null;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
cleanupOverflowDrag() {
|
|
1371
|
+
this.isDragging = false;
|
|
1372
|
+
this.isDraggingFromOverflow = false;
|
|
1373
|
+
this.draggedItem = null;
|
|
1374
|
+
if (this.dragSourceWrapper) {
|
|
1375
|
+
this.dragSourceWrapper.classList.remove(CSS_PREFIX + '__overflow-item--dragging');
|
|
1376
|
+
this.dragSourceWrapper = null;
|
|
1377
|
+
}
|
|
1378
|
+
this.wrapper.classList.remove(CSS_PREFIX + '__wrapper--dragging');
|
|
1379
|
+
if (this.dragGhost) {
|
|
1380
|
+
this.dragGhost.remove();
|
|
1381
|
+
this.dragGhost = null;
|
|
1382
|
+
}
|
|
1383
|
+
// Also clean up any orphaned ghost elements (fallback)
|
|
1384
|
+
const orphanedGhost = document.querySelector('.' + CSS_PREFIX + '__drag-ghost');
|
|
1385
|
+
if (orphanedGhost) {
|
|
1386
|
+
orphanedGhost.remove();
|
|
1387
|
+
}
|
|
1388
|
+
if (this.dragPlaceholder) {
|
|
1389
|
+
this.dragPlaceholder.remove();
|
|
1390
|
+
this.dragPlaceholder = null;
|
|
1391
|
+
}
|
|
1392
|
+
if (this.dragStartTimer) {
|
|
1393
|
+
clearTimeout(this.dragStartTimer);
|
|
1394
|
+
this.dragStartTimer = null;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
startDrag(wrapper, tool, e) {
|
|
1398
|
+
this.isDragging = true;
|
|
1399
|
+
this.draggedItem = tool;
|
|
1400
|
+
// Add dragging class to wrapper
|
|
1401
|
+
wrapper.classList.add(CSS_PREFIX + '__item--dragging');
|
|
1402
|
+
this.wrapper.classList.add(CSS_PREFIX + '__wrapper--dragging');
|
|
1403
|
+
// Create a ghost element for visual feedback
|
|
1404
|
+
const rect = wrapper.getBoundingClientRect();
|
|
1405
|
+
this.dragGhost = wrapper.cloneNode(true);
|
|
1406
|
+
this.dragGhost.classList.add(CSS_PREFIX + '__drag-ghost');
|
|
1407
|
+
this.dragGhost.style.position = 'fixed';
|
|
1408
|
+
this.dragGhost.style.left = `${rect.left}px`;
|
|
1409
|
+
this.dragGhost.style.top = `${rect.top}px`;
|
|
1410
|
+
this.dragGhost.style.width = `${rect.width}px`;
|
|
1411
|
+
this.dragGhost.style.height = `${rect.height}px`;
|
|
1412
|
+
this.dragGhost.style.pointerEvents = 'none';
|
|
1413
|
+
this.dragGhost.style.zIndex = '10000';
|
|
1414
|
+
document.body.appendChild(this.dragGhost);
|
|
1415
|
+
// Create a placeholder to show where the item will be dropped
|
|
1416
|
+
this.dragPlaceholder = document.createElement('span');
|
|
1417
|
+
this.dragPlaceholder.classList.add(CSS_PREFIX + '__drop-placeholder');
|
|
1418
|
+
const onMouseMove = (moveEvent) => {
|
|
1419
|
+
if (!this.isDragging || !this.dragGhost)
|
|
1420
|
+
return;
|
|
1421
|
+
// Update ghost position
|
|
1422
|
+
const ghostRect = this.dragGhost.getBoundingClientRect();
|
|
1423
|
+
this.dragGhost.style.left = `${moveEvent.clientX - ghostRect.width / 2}px`;
|
|
1424
|
+
this.dragGhost.style.top = `${moveEvent.clientY - ghostRect.height / 2}px`;
|
|
1425
|
+
// Find the toolbar item we're hovering over
|
|
1426
|
+
const toolbarItems = Array.from(this.toolbar.querySelectorAll('.' + CSS_PREFIX + '__item'));
|
|
1427
|
+
let insertBefore = null;
|
|
1428
|
+
let insertIndex = -1;
|
|
1429
|
+
for (let i = 0; i < toolbarItems.length; i++) {
|
|
1430
|
+
const item = toolbarItems[i];
|
|
1431
|
+
if (item === wrapper)
|
|
1432
|
+
continue; // Skip the dragged item itself
|
|
1433
|
+
const itemRect = item.getBoundingClientRect();
|
|
1434
|
+
const itemCenter = itemRect.left + itemRect.width / 2;
|
|
1435
|
+
if (moveEvent.clientX < itemCenter && insertBefore === null) {
|
|
1436
|
+
insertBefore = item;
|
|
1437
|
+
insertIndex = i;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
// Remove existing placeholder
|
|
1441
|
+
if (this.dragPlaceholder && this.dragPlaceholder.parentNode) {
|
|
1442
|
+
this.dragPlaceholder.remove();
|
|
1443
|
+
}
|
|
1444
|
+
// Insert placeholder at the appropriate position
|
|
1445
|
+
if (insertBefore && this.dragPlaceholder) {
|
|
1446
|
+
insertBefore.parentNode?.insertBefore(this.dragPlaceholder, insertBefore);
|
|
1447
|
+
}
|
|
1448
|
+
else if (this.dragPlaceholder) {
|
|
1449
|
+
// Insert at end (before separator or overflow toggle)
|
|
1450
|
+
const separator = this.toolbar.querySelector('.' + CSS_PREFIX + '__separator');
|
|
1451
|
+
if (separator) {
|
|
1452
|
+
separator.parentNode?.insertBefore(this.dragPlaceholder, separator);
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
const overflowToggle = this.toolbar.querySelector('.' + CSS_PREFIX + '__overflow-toggle');
|
|
1456
|
+
if (overflowToggle) {
|
|
1457
|
+
overflowToggle.parentNode?.insertBefore(this.dragPlaceholder, overflowToggle);
|
|
1458
|
+
}
|
|
1459
|
+
else {
|
|
1460
|
+
this.toolbar.appendChild(this.dragPlaceholder);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
const onMouseUp = (upEvent) => {
|
|
1466
|
+
if (!this.isDragging)
|
|
1467
|
+
return;
|
|
1468
|
+
// Calculate new position based on placeholder
|
|
1469
|
+
if (this.dragPlaceholder && this.dragPlaceholder.parentNode) {
|
|
1470
|
+
const toolbarItems = Array.from(this.toolbar.querySelectorAll('.' + CSS_PREFIX + '__item'));
|
|
1471
|
+
const placeholderIndex = Array.from(this.toolbar.children).indexOf(this.dragPlaceholder);
|
|
1472
|
+
const draggedIndex = toolbarItems.indexOf(wrapper);
|
|
1473
|
+
// Calculate target index in visible items
|
|
1474
|
+
let visibleTargetIndex = 0;
|
|
1475
|
+
for (let i = 0; i < this.toolbar.children.length; i++) {
|
|
1476
|
+
const child = this.toolbar.children[i];
|
|
1477
|
+
if (child === this.dragPlaceholder)
|
|
1478
|
+
break;
|
|
1479
|
+
if (child.classList.contains(CSS_PREFIX + '__item')) {
|
|
1480
|
+
visibleTargetIndex++;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
// Find the tool's current index in the tools array
|
|
1484
|
+
const currentToolIndex = this.tools.indexOf(tool);
|
|
1485
|
+
if (currentToolIndex !== -1 && visibleTargetIndex !== currentToolIndex) {
|
|
1486
|
+
// Remove the tool from its current position
|
|
1487
|
+
this.tools.splice(currentToolIndex, 1);
|
|
1488
|
+
// Insert at new position
|
|
1489
|
+
const insertAtIndex = visibleTargetIndex > currentToolIndex
|
|
1490
|
+
? visibleTargetIndex - 1
|
|
1491
|
+
: visibleTargetIndex;
|
|
1492
|
+
this.tools.splice(insertAtIndex, 0, tool);
|
|
1493
|
+
// Update order values
|
|
1494
|
+
this.tools.forEach((t, i) => {
|
|
1495
|
+
t.order = i;
|
|
1496
|
+
});
|
|
1497
|
+
// Save to localStorage
|
|
1498
|
+
this.saveOrderState();
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
// Clean up
|
|
1502
|
+
this.cleanupDrag(wrapper);
|
|
1503
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
1504
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
1505
|
+
// Re-render after a short delay
|
|
1506
|
+
setTimeout(() => {
|
|
1507
|
+
this.render();
|
|
1508
|
+
}, 0);
|
|
1509
|
+
};
|
|
1510
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
1511
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
1512
|
+
}
|
|
1513
|
+
cleanupDrag(wrapper) {
|
|
1514
|
+
this.isDragging = false;
|
|
1515
|
+
this.draggedItem = null;
|
|
1516
|
+
wrapper.classList.remove(CSS_PREFIX + '__item--dragging');
|
|
1517
|
+
this.wrapper.classList.remove(CSS_PREFIX + '__wrapper--dragging');
|
|
1518
|
+
if (this.dragGhost) {
|
|
1519
|
+
this.dragGhost.remove();
|
|
1520
|
+
this.dragGhost = null;
|
|
1521
|
+
}
|
|
1522
|
+
// Also clean up any orphaned ghost elements (fallback)
|
|
1523
|
+
const orphanedGhost = document.querySelector('.' + CSS_PREFIX + '__drag-ghost');
|
|
1524
|
+
if (orphanedGhost) {
|
|
1525
|
+
orphanedGhost.remove();
|
|
1526
|
+
}
|
|
1527
|
+
if (this.dragPlaceholder) {
|
|
1528
|
+
this.dragPlaceholder.remove();
|
|
1529
|
+
this.dragPlaceholder = null;
|
|
1530
|
+
}
|
|
1531
|
+
if (this.dragStartTimer) {
|
|
1532
|
+
clearTimeout(this.dragStartTimer);
|
|
1533
|
+
this.dragStartTimer = null;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
setupResize() {
|
|
1537
|
+
let isResizing = false;
|
|
1538
|
+
let startY = 0;
|
|
1539
|
+
let startHeight = 0;
|
|
1540
|
+
const onMouseDown = (e) => {
|
|
1541
|
+
isResizing = true;
|
|
1542
|
+
startY = e.clientY;
|
|
1543
|
+
startHeight = this.editorContainer.offsetHeight;
|
|
1544
|
+
// Add resizing class for visual feedback
|
|
1545
|
+
this.wrapper.classList.add(CSS_PREFIX + '__wrapper--resizing');
|
|
1546
|
+
// Prevent text selection while dragging
|
|
1547
|
+
e.preventDefault();
|
|
1548
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
1549
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
1550
|
+
};
|
|
1551
|
+
const onMouseMove = (e) => {
|
|
1552
|
+
if (!isResizing)
|
|
1553
|
+
return;
|
|
1554
|
+
const deltaY = e.clientY - startY;
|
|
1555
|
+
const newHeight = startHeight + deltaY;
|
|
1556
|
+
// Set minimum and maximum heights
|
|
1557
|
+
const minHeight = 200;
|
|
1558
|
+
const maxHeight = globalThis.innerHeight - 100;
|
|
1559
|
+
if (newHeight >= minHeight && newHeight <= maxHeight) {
|
|
1560
|
+
this.editorContainer.style.height = newHeight + 'px';
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
const onMouseUp = () => {
|
|
1564
|
+
if (!isResizing)
|
|
1565
|
+
return;
|
|
1566
|
+
isResizing = false;
|
|
1567
|
+
this.wrapper.classList.remove(CSS_PREFIX + '__wrapper--resizing');
|
|
1568
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
1569
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
1570
|
+
};
|
|
1571
|
+
this.resizeHandle.addEventListener('mousedown', onMouseDown);
|
|
1572
|
+
}
|
|
1573
|
+
update(view, prevState) {
|
|
1574
|
+
// Re-render tools to update their state (original approach)
|
|
1575
|
+
// Note: This is less efficient but more reliable than storing update functions
|
|
1576
|
+
this.tools.forEach((tool) => {
|
|
1577
|
+
tool.element.render(this.editorView);
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
destroy() {
|
|
1581
|
+
// Clean up event listeners
|
|
1582
|
+
const doc = this.editorView.dom.ownerDocument || document;
|
|
1583
|
+
if (this.closeOverflowHandler) {
|
|
1584
|
+
doc.removeEventListener('click', this.closeOverflowHandler);
|
|
1585
|
+
this.closeOverflowHandler = null;
|
|
1586
|
+
}
|
|
1587
|
+
if (this.closePinnedDropdownHandler) {
|
|
1588
|
+
doc.removeEventListener('click', this.closePinnedDropdownHandler);
|
|
1589
|
+
this.closePinnedDropdownHandler = null;
|
|
1590
|
+
}
|
|
1591
|
+
// Clean up keyboard handler
|
|
1592
|
+
if (this.keydownHandler) {
|
|
1593
|
+
document.removeEventListener('keydown', this.keydownHandler);
|
|
1594
|
+
this.keydownHandler = null;
|
|
1595
|
+
}
|
|
1596
|
+
// Clean up ResizeObserver
|
|
1597
|
+
if (this.resizeObserver) {
|
|
1598
|
+
this.resizeObserver.disconnect();
|
|
1599
|
+
this.resizeObserver = null;
|
|
1600
|
+
}
|
|
1601
|
+
// Clean up pinned dropdown
|
|
1602
|
+
if (this.pinnedDropdownMenu) {
|
|
1603
|
+
this.pinnedDropdownMenu.remove();
|
|
1604
|
+
this.pinnedDropdownMenu = null;
|
|
1605
|
+
}
|
|
1606
|
+
// Clean up drag state
|
|
1607
|
+
if (this.dragGhost) {
|
|
1608
|
+
this.dragGhost.remove();
|
|
1609
|
+
this.dragGhost = null;
|
|
1610
|
+
}
|
|
1611
|
+
if (this.dragPlaceholder) {
|
|
1612
|
+
this.dragPlaceholder.remove();
|
|
1613
|
+
this.dragPlaceholder = null;
|
|
1614
|
+
}
|
|
1615
|
+
if (this.dragStartTimer) {
|
|
1616
|
+
clearTimeout(this.dragStartTimer);
|
|
1617
|
+
this.dragStartTimer = null;
|
|
1618
|
+
}
|
|
1619
|
+
// Clean up DOM
|
|
1620
|
+
if (this.wrapper.parentNode) {
|
|
1621
|
+
this.wrapper.parentNode.replaceChild(this.editorView.dom, this.wrapper);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
export class CustomMenuPlugin extends Plugin {
|
|
1626
|
+
constructor(editor, options) {
|
|
1627
|
+
super({
|
|
1628
|
+
view(editorView) {
|
|
1629
|
+
return new CustomMenuView(editorView, editor, options.content);
|
|
1630
|
+
},
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
//# sourceMappingURL=CustomMenuPlugin.js.map
|