leksy-editor 2.6.0 → 3.0.0

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/utilities.js DELETED
@@ -1,4359 +0,0 @@
1
- import { BLOCK_FORMAT_TAGS, CLASSES, FONT_SIZES, FONTS, FORMATS, GIPHY_POWERED_IMAGE, REGEX, RESIZER_PLUGINS, SOCIAL_MEDIA_BASEURLS, SOCIAL_MEDIA_PATTERNS, SVG, TABLE_PLUGINS, TAB_CATEGORIES, RESIZE_MARGIN, LIST_STYLES_BY_LEVEL, UNORDERED_LIST_STYLES_BY_LEVEL } from "./constant";
2
- import { DiffDOM } from 'diff-dom';
3
- import { v4 as uuidv4 } from 'uuid';
4
- import { findTab } from "./tab";
5
- import { applyOrderList, applyUnorderedList } from "./plugin";
6
-
7
- const dd = new DiffDOM();
8
-
9
- const removeBlockedStyles = (node) => {
10
- if (!node?.style) return;
11
- const styleProperties = Array.from(node.style);
12
-
13
- styleProperties.forEach((propertyName) => {
14
- if (REGEX.BLOCKED_STYLES.test(propertyName)) {
15
- node.style.removeProperty(propertyName);
16
- }
17
- });
18
-
19
- if (!node.getAttribute('style')?.trim()) {
20
- node.removeAttribute('style');
21
- }
22
- }
23
-
24
- const traverseAndClean = (element, options, core) => {
25
- if (!element?.childNodes) return;
26
-
27
- // Iterate through child nodes
28
- const childNodes = element.childNodes;
29
- for (let i = childNodes.length - 1; i >= 0; i--) {
30
- const node = childNodes[i];
31
-
32
- // Recursively process child elements
33
- if (node.nodeType === Node.ELEMENT_NODE) {
34
- // Check if element tag is allowed
35
- if (!REGEX.WHILE_LISTED_TAG.test(node.tagName)) {
36
- // Remove the element if not allowed
37
- node.parentNode.removeChild(node);
38
- } else {
39
- // Remove attributes not in the whitelist
40
- const attributesToRemove = [];
41
- for (const attr of node.attributes) {
42
- if (!REGEX.WHILE_LISTED_ATTRIBUTES.test(attr.name) || attr.value.includes('var(')) {
43
- attributesToRemove.push(attr.name);
44
- }
45
- }
46
- if (options?.disablePastedColorStyles) {
47
- const nodeStyles = node.style
48
- let bgColor = nodeStyles.backgroundColor;
49
- let textColor = nodeStyles.color;
50
- let currentNode = core?.state?.range?.startContainer;
51
- let bodyBg = getEffectiveBackgroundColor(currentNode);
52
- if (bodyBg && bodyBg?.startsWith('rgb')) {
53
- bodyBg = rgbToHex(bodyBg)
54
- }
55
- if (bgColor && bgColor?.startsWith('rgb')) {
56
- bgColor = rgbToHex(bgColor)
57
- }
58
- if (textColor && textColor?.startsWith('rgb')) {
59
- textColor = rgbToHex(textColor)
60
- }
61
-
62
- if (bodyBg === bgColor) node.style.removeProperty('background-color');
63
- if (bodyBg === textColor) node.style.removeProperty('color');
64
- }
65
- if (options?.removeColorsWhilePasting) {
66
- node.style.removeProperty('background-color');
67
- node.style.removeProperty('color');
68
- }
69
- removeBlockedStyles(node);
70
- attributesToRemove.forEach(attr => node.removeAttribute(attr));
71
-
72
- // Process child nodes
73
- traverseAndClean(node, options, core);
74
- }
75
- }
76
- }
77
- }
78
-
79
- const getEffectiveBackgroundColor = (element) => {
80
- if (!(element instanceof Element)) return null;
81
- let el = element;
82
- while (el) {
83
- const bgColor = window?.getComputedStyle(el)?.backgroundColor;
84
-
85
- if (bgColor !== 'rgba(0, 0, 0, 0)' &&
86
- bgColor !== 'transparent' &&
87
- bgColor !== '') {
88
- return bgColor;
89
- }
90
-
91
- el = el !== document.body ? el.parentElement : null;
92
- }
93
- const bodyBg = window?.getComputedStyle(document.body)?.backgroundColor;
94
- return bodyBg
95
- }
96
-
97
- const cleanHTML = (html, options, core) => {
98
- // Create a temporary div to manipulate the HTML
99
- const tempDiv = document.createElement('div');
100
- tempDiv.innerHTML = html;
101
-
102
- // Traverse through all elements and clean attributes
103
- traverseAndClean(tempDiv, options, core);
104
-
105
- // Return manipulated HTML
106
- return tempDiv.innerHTML;
107
- }
108
-
109
- const debounce = (fn, delay) => {
110
- let timeout = null;
111
-
112
- return (...args) => {
113
- if (timeout) {
114
- clearTimeout(timeout);
115
- }
116
-
117
- timeout = setTimeout(async () => {
118
- fn(...args);
119
- timeout = null; // Clear the timeout reference after execution
120
- }, delay);
121
- }
122
- }
123
-
124
- function asyncDebounce(fn, delay) {
125
- let timeout = null;
126
- let isRunning = false;
127
- let callNextFn = false;
128
- let newArgs = [];
129
-
130
- function call(...args) {
131
- if (isRunning) {
132
- newArgs = args;
133
- callNextFn = true;
134
- return;
135
- } else {
136
- clearTimeout(timeout);
137
- newArgs = [];
138
- callNextFn = false;
139
- }
140
-
141
- return new Promise((resolve) => {
142
- timeout = setTimeout(async () => {
143
- isRunning = true;
144
- await fn(...args);
145
- resolve();
146
- isRunning = false;
147
- if (callNextFn) call(...newArgs);
148
- }, delay);
149
- });
150
- }
151
-
152
- return call;
153
- }
154
-
155
- function getDeviceName() {
156
- const userAgent = navigator.userAgent;
157
-
158
- if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
159
- return "IOS";
160
- } else if (/Android/.test(userAgent)) {
161
- return "ANDROID";
162
- } else if (/Macintosh/.test(userAgent)) {
163
- return "MAC";
164
- } else if (/Windows/.test(userAgent)) {
165
- return "WINDOWS";
166
- } else if (/Linux/.test(userAgent)) {
167
- return "LINUX";
168
- } else {
169
- return "Unknown";
170
- }
171
- }
172
-
173
- function getScrollParent(element) {
174
- let parent = element.parentElement;
175
- while (parent && parent !== document.body) {
176
- const style = getComputedStyle(parent);
177
- const overflowX = style.overflowX;
178
- if (overflowX === 'auto' || overflowX === 'scroll' || overflowX === 'hidden') {
179
- return parent;
180
- }
181
- parent = parent.parentElement;
182
- }
183
- return document.body;
184
- }
185
-
186
- const showTooltip = (options) => {
187
- document.addEventListener('mouseover', (e) => {
188
- const el = e.target.closest(`.${options.classPrefix}${CLASSES.TOOLBAR_ITEM}`);
189
- if (!el) return;
190
-
191
- const rect = el.getBoundingClientRect();
192
-
193
- let top = rect.bottom + 2;
194
- let left = rect.left + rect.width / 2;
195
-
196
- left = Math.max(68, Math.min(left, window.innerWidth - 68));
197
-
198
- el.style.setProperty('--leksy-editor-top', `${top}px`);
199
- el.style.setProperty('--leksy-editor-left', `${left}px`);
200
- });
201
- };
202
-
203
- const getTooltip = (plugin, options) => {
204
- return options?.customTooltip?.[plugin.pluginId] ?? plugin.title
205
- }
206
-
207
- const getToolbarIcon = (plugin, options) => {
208
- return options?.customIcon?.[plugin.pluginId] ?? plugin.icon
209
- }
210
-
211
- const makeToolbarButton = (_plugin, options, core) => {
212
- const pluginButton = document.createElement('button');
213
- pluginButton.type = "button"
214
- pluginButton.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEM} ${_plugin.type === 'reset-color' ? 'reset-color' : ''}`
215
-
216
- pluginButton.dataset.title = getTooltip(_plugin, options)
217
- pluginButton.dataset.type = 'button'
218
- pluginButton.onclick = (e) => {
219
- e.preventDefault();
220
- _plugin.click(e, core, options);
221
- };
222
-
223
- const pluginIcon = document.createElement('div');
224
- pluginIcon.innerHTML = getToolbarIcon(_plugin, options);
225
- pluginButton.appendChild(pluginIcon)
226
- return pluginButton
227
- }
228
-
229
-
230
- const makeToolbarButtonSelect = (_plugin, options, core) => {
231
- const pluginButton = document.createElement('button');
232
- pluginButton.type = "button"
233
- pluginButton.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEM} ${_plugin.type === 'button-select' ? CLASSES.TOOLBAR_ITEM_BUTTON_SELECT : ''}`
234
-
235
- pluginButton.dataset.title = getTooltip(_plugin, options)
236
- pluginButton.dataset.type = 'button'
237
- pluginButton.onclick = (e) => {
238
- e.preventDefault();
239
- _plugin.mainClick(e, core, options);
240
- };
241
-
242
- const pluginIcon = document.createElement('div');
243
- pluginIcon.innerHTML = getToolbarIcon(_plugin, options);
244
- pluginButton.appendChild(pluginIcon)
245
- const pluginSelect = makeToolbarSelect({ ..._plugin, icon: '' }, options, core);
246
-
247
- const pluginButtonSelect = document.createElement('div');
248
- pluginButtonSelect.dataset.type = 'button-select'
249
- pluginButtonSelect.style.display = 'flex'
250
- pluginButtonSelect.appendChild(pluginButton);
251
- pluginButtonSelect.appendChild(pluginSelect);
252
- return pluginButtonSelect
253
- }
254
-
255
-
256
- const makeToolbarColor = (_plugin, options, core) => {
257
- const pluginContainer = document.createElement('div');
258
- pluginContainer.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEM} ${CLASSES.TOOLBAR_ITEM_COLOR}`
259
- pluginContainer.dataset.title = getTooltip(_plugin, options)
260
- pluginContainer.dataset.type = 'color'
261
-
262
- const pluginButton = document.createElement('input');
263
- pluginButton.type = "color"
264
- pluginButton.onclick = () => { if (getDeviceName() !== 'IOS') core.elements.editor.focus() }
265
-
266
- pluginButton.onchange = (e) => {
267
- e.preventDefault();
268
- _plugin.change(e, core, options);
269
- };
270
-
271
- const pluginIcon = document.createElement('div');
272
- pluginIcon.innerHTML = getToolbarIcon(_plugin, options);
273
- pluginIcon.onclick = () => pluginButton.click()
274
- pluginContainer.append(pluginButton, pluginIcon)
275
- return pluginContainer
276
- }
277
-
278
- const makeToolbarDropdown = (_plugin, options, core) => {
279
- const handleOutsideClick = (e, forceFully) => {
280
- if (!forceFully && (e.target.getAttribute('data-toolbar-option') || dropdownPlugin.contains(e.target))) return
281
- dropdownContent.style.display = 'none'
282
- document.removeEventListener('mousedown', handleOutsideClick, false);
283
- core.elements.editor.removeEventListener('click', handleOutsideClick, false);
284
- }
285
- const renderGalary = (images) => {
286
- if ((core.state.page == 1 && !core.state.next) || (!core.state.page && !core.state.next)) container.innerHTML = ''
287
- images.forEach(image => {
288
- const imageElement = document.createElement('img');
289
- imageElement.src = image.src;
290
- imageElement.style.width = '120px';
291
- imageElement.style.height = '120px';
292
- imageElement.style.cursor = 'pointer';
293
- imageElement.style.margin = '2px';
294
- imageElement.onclick = (e) => {
295
- const img = document.createElement('img');
296
- img.src = image.src
297
- if (image?.url) { // for video
298
- const linkNode = document.createElement('a');
299
- linkNode.href = image.url;
300
- linkNode.target = '_blank';
301
- linkNode.appendChild(img);
302
- core.insertNode(linkNode);
303
- } else {
304
- core.insertNode(img);
305
- }
306
- core.updateCaretPosition()
307
- core.elements.editor.focus()
308
- options.closePluginOnClick && handleOutsideClick(e, true)
309
- };
310
- container.appendChild(imageElement);
311
- })
312
- }
313
- const renderMentions = (value) => {
314
- container.innerHTML = ''
315
- const labels = JSON.parse(JSON.stringify(options.labels))
316
- .map(label => {
317
- label.fields = label.fields.filter(field => field.name.toLowerCase().includes(value.toLowerCase()))
318
- return label
319
- })
320
- .filter(label => label.fields.length)
321
- labels.forEach(category => {
322
- const categoryElement = document.createElement('div')
323
- categoryElement.textContent = category.name
324
- container.append(categoryElement)
325
- category.fields.forEach(field => {
326
- const labelElement = document.createElement('button')
327
- labelElement.type = "button"
328
- labelElement.textContent = field.name
329
- labelElement.onclick = (e) => {
330
- core.elements.editor.focus();
331
- const span = document.createElement('span');
332
- span.spellcheck = false
333
- span.contentEditable = false
334
- span.innerText = field.name
335
- span.setAttribute('data-id', field.value);
336
-
337
- core.insertNode(span)
338
- core.updateCaretPosition()
339
- options.closePluginOnClick && handleOutsideClick(e, true)
340
- }
341
- container.append(labelElement)
342
- })
343
- })
344
- }
345
-
346
- const dropdownPlugin = document.createElement('div');
347
- dropdownPlugin.setAttribute('data-type', 'dropdown')
348
-
349
- const dropdownButton = document.createElement('button');
350
- dropdownButton.type = "button"
351
- dropdownButton.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEM}`
352
- dropdownButton.setAttribute('data-title', getTooltip(_plugin, options))
353
-
354
- const pluginIcon = document.createElement('div');
355
- pluginIcon.innerHTML = getToolbarIcon(_plugin, options);
356
- dropdownButton.appendChild(pluginIcon)
357
-
358
- const dropdownContent = document.createElement('div');
359
- dropdownContent.className = `${options.classPrefix}${CLASSES.DROPDOWN_CONTENT} ${_plugin.type}`
360
-
361
- const container = document.createElement('div');
362
-
363
- if (_plugin.type === 'dropdown') {
364
- _plugin.options.forEach(option => {
365
- const optionDiv = document.createElement('button');
366
- optionDiv.type = "button"
367
- optionDiv.textContent = option.label;
368
- optionDiv.setAttribute('data-value', option.value); // Set custom attribute for value
369
- optionDiv.setAttribute('data-toolbar-option', true); // Set custom attribute for value
370
-
371
- // Event listener to handle selection
372
- optionDiv.addEventListener('click', function (e) {
373
- dropdownContent.style.display = 'none';
374
- _plugin.click(e, core, options)
375
- });
376
-
377
- // Append option to the dropdown content div
378
- dropdownContent.appendChild(optionDiv);
379
- });
380
- } else if (_plugin.type === 'gallery') {
381
- const search = document.createElement('input');
382
- search.placeholder = 'Search...'
383
- const fetchAndRenderImages = async (searchQuery = '', page) => {
384
- let res;
385
- if (searchQuery) {
386
- res = await _plugin.search(options, searchQuery, page);
387
- } else {
388
- res = await _plugin.suggestions(options, page);
389
- }
390
- if (res.totalPages) core.state.totalPages = res.totalPages; // Update total pages if available
391
- if (res.next) core.state.next = res.next; // Update next page if available
392
- renderGalary(res.images); // Pass search query and append flag
393
- };
394
-
395
- const debounce = asyncDebounce(async (value) => {
396
- core.state.page = 1; // Reset page on new search
397
- core.state.next = "";
398
- core.state.currentSearch = value; // Store the search term
399
- if (_plugin.title == "Tenor") {
400
- await fetchAndRenderImages(value, "");
401
- } else {
402
- await fetchAndRenderImages(value, 1);
403
- }
404
- }, 300);
405
-
406
- search.oninput = (e) => debounce(e.target.value)
407
-
408
- dropdownContent.addEventListener("scroll", async () => {
409
- if (core.state.isFetching) return; // Prevent multiple calls while fetching
410
-
411
- if (dropdownContent.scrollTop + dropdownContent.clientHeight >= dropdownContent.scrollHeight - 10) {
412
- if (core.state.next) {
413
- core.state.isFetching = true; // Set fetching state
414
- await fetchAndRenderImages(core.state.currentSearch, core.state.next); // Fetch new images
415
- core.state.isFetching = false; // Reset fetching state
416
- }
417
- if (core.state.totalPages && core.state.page < core.state.totalPages) {
418
- core.state.isFetching = true; // Set fetching state
419
- core.state.page += 1; // Increment page
420
- await fetchAndRenderImages(core.state.currentSearch, core.state.page); // Fetch new images
421
- core.state.isFetching = false; // Reset fetching state
422
- }
423
- }
424
- });
425
- const footer = document.createElement('div');
426
- footer.className = 'footer';
427
- if (_plugin.title === "GIPHY") {
428
- const img = document.createElement('img');
429
-
430
- img.src = `data:image/png;base64,${GIPHY_POWERED_IMAGE}`;
431
- img.alt = "Powered By GIPHY";
432
- footer.append(img)
433
- } else {
434
- footer.innerHTML = `<span>Powered By ${_plugin.title}</span>`;
435
- }
436
- footer.style.position = 'sticky';
437
- footer.style.bottom = '0';
438
- footer.style.width = '100%';
439
- footer.style.background = '#f1f1f1';
440
- footer.style.textAlign = 'center';
441
- footer.style.height = '44px';
442
- footer.style.display = 'flex';
443
- footer.style.alignItems = 'center';
444
- footer.style.justifyContent = 'center';
445
- footer.style.overflow = 'hidden';
446
-
447
- dropdownContent.append(search, container, footer)
448
- } else if (_plugin.type === 'category') {
449
- _plugin.categories.forEach(category => {
450
- const categoryElement = document.createElement('div')
451
- categoryElement.textContent = category.label
452
- dropdownContent.append(categoryElement)
453
- category.icons.forEach(emoji => {
454
- const emojiElement = document.createElement('span')
455
- emojiElement.textContent = emoji
456
- emojiElement.onclick = (e) => {
457
- core.elements.editor.focus();
458
- core.elements.iframeWindow.execCommand('insertText', false, emoji)
459
- options.closePluginOnClick && handleOutsideClick(e, true)
460
- }
461
- dropdownContent.append(emojiElement)
462
- })
463
- })
464
- } else if (_plugin.type === 'mention') {
465
- const search = document.createElement('input');
466
- search.placeholder = 'Search...'
467
- search.oninput = (e) => renderMentions(e.target.value)
468
- dropdownContent.append(search, container)
469
- renderMentions('')
470
- } else if (_plugin.type === 'table') {
471
- const totalRows = 10;
472
- const totalCols = 10;
473
-
474
- // Create wrapper for table and checkbox
475
- const wrapper = document.createElement('div');
476
- wrapper.className = 'toolbar-table-wrapper'
477
-
478
- // Create header checkbox
479
- const headerOption = document.createElement('label');
480
- headerOption.style.display = 'block';
481
- headerOption.style.marginBottom = '8px';
482
-
483
- const checkbox = document.createElement('input');
484
- checkbox.type = 'checkbox';
485
- checkbox.checked = true; // Default to include header
486
- checkbox.style.marginRight = '6px';
487
-
488
- headerOption.appendChild(checkbox);
489
- headerOption.appendChild(document.createTextNode('Include header row'));
490
-
491
- wrapper.appendChild(headerOption);
492
-
493
- const table = document.createElement('div');
494
- table.className = `${options.classPrefix}${CLASSES.TOOLBAR_TABLE}`
495
-
496
- // Create table selector grid
497
- for (let i = 0; i < totalRows; i++) {
498
- for (let j = 0; j < totalCols; j++) {
499
-
500
- const cell = document.createElement('div');
501
- cell.className = 'cell'
502
-
503
- cell.setAttribute('data-row', i);
504
- cell.setAttribute('data-col', j);
505
- if (j === 0) {
506
- cell.setAttribute('tabIndex', '0');
507
- }
508
-
509
- cell.addEventListener('mouseover', () => {
510
- const cells = table.querySelectorAll('.cell');
511
- cells.forEach((cell) => {
512
- const cellRow = parseInt(cell.getAttribute('data-row'));
513
- const cellCol = parseInt(cell.getAttribute('data-col'));
514
-
515
- if (cellRow <= i && cellCol <= j) {
516
- cell.classList.add('selected');
517
- } else {
518
- cell.classList.remove('selected');
519
- }
520
- });
521
- });
522
- cell.addEventListener('click', () => {
523
- _plugin.create(core, options, { rows: i, cols: j, header: checkbox.checked })
524
- dropdownContent.style.display = 'none'
525
- });
526
-
527
- table.appendChild(cell);
528
- }
529
- }
530
- wrapper.appendChild(table);
531
- dropdownContent.appendChild(wrapper);
532
- }
533
-
534
- let isSuggestionLoaded = false;
535
- dropdownButton.onclick = () => {
536
- if (dropdownContent.style.display === 'block') {
537
- dropdownContent.style.display = 'none'
538
- document.removeEventListener('mousedown', handleOutsideClick, false);
539
- core.elements.editor.removeEventListener('click', handleOutsideClick, false);
540
- return
541
- }
542
- dropdownContent.style.display = 'block'
543
- const container = getScrollParent(dropdownContent);
544
- const containerRect = container.getBoundingClientRect();
545
- const popoverRect = dropdownContent.getBoundingClientRect();
546
-
547
- const overflowRight = popoverRect.right > containerRect.right;
548
- const overflowLeft = popoverRect.left < containerRect.left;
549
-
550
- if (overflowRight) {
551
- dropdownContent.style.right = '10px';
552
- } else if (overflowLeft) {
553
- dropdownContent.style.left = '10px';
554
- }
555
-
556
- if (!isSuggestionLoaded && _plugin.type === 'gallery') {
557
- _plugin.suggestions(options)
558
- .then(res => {
559
- isSuggestionLoaded = true
560
- if (res.totalPages) {
561
- core.state.totalPages = res.totalPages;
562
- core.state.next = ""
563
- } // Update total pages if available
564
- if (res.next) {
565
- core.state.next = res.next;
566
- core.state.totalPages = null
567
- } // Update next page if available
568
- renderGalary(res.images)
569
- })
570
- }
571
-
572
- document.addEventListener('mousedown', handleOutsideClick);
573
- core.elements.editor.addEventListener('click', handleOutsideClick);
574
- }
575
-
576
- dropdownPlugin.appendChild(dropdownButton)
577
- dropdownPlugin.appendChild(dropdownContent)
578
- return dropdownPlugin
579
- }
580
-
581
- const makeToolbarSelect = (_plugin, options, core) => {
582
- const handleOutsideClick = (e) => {
583
- if (e.target.getAttribute('data-toolbar-option') || selectPlugin.contains(e.target)) return
584
- dropdownContent.style.display = 'none'
585
- document.removeEventListener('mousedown', handleOutsideClick, false);
586
- core.elements.editor.removeEventListener('click', handleOutsideClick, false);
587
- }
588
-
589
- const selectPlugin = document.createElement('div');
590
- selectPlugin.dataset.type = 'select'
591
-
592
- const dropdownButton = document.createElement('button');
593
- dropdownButton.type = "button"
594
- dropdownButton.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEM} ${CLASSES.TOOLBAR_ITEM_SELECT} ${_plugin.type === 'button-select' ? CLASSES.TOOLBAR_ITEM_BUTTON_SELECT : ''}`
595
- dropdownButton.dataset.title = _plugin.type == 'button-select' ? `Options` : getTooltip(_plugin, options)
596
-
597
- const pluginIcon = document.createElement('div');
598
- pluginIcon.style.width = _plugin.width
599
- pluginIcon.style.textAlign = 'left'
600
- pluginIcon.style.display = 'flex'
601
- pluginIcon.style.justifyContent = 'space-between'
602
-
603
- const iconName = document.createElement('span');
604
- iconName.innerHTML = getToolbarIcon(_plugin, options);
605
-
606
- const arrow = document.createElement('span');
607
- if (_plugin.type == 'button-select') {
608
- arrow.innerHTML = SVG.ARROW_DROP_DOWN_FILL;
609
- } else {
610
- arrow.innerHTML = SVG.ARROW_DOWN;
611
- arrow.style.paddingLeft = '8px';
612
- }
613
- pluginIcon.append(iconName, arrow)
614
-
615
- dropdownButton.append(pluginIcon)
616
-
617
- const dropdownContent = document.createElement('div');
618
- dropdownContent.className = `${options.classPrefix}${CLASSES.DROPDOWN_CONTENT}`
619
- _plugin.options.forEach(option => {
620
- const optionDiv = document.createElement('button');
621
- optionDiv.type = "button"
622
- optionDiv.innerHTML = option.label;
623
- optionDiv.setAttribute('data-value', option.value); // Set custom attribute for value
624
- optionDiv.setAttribute('data-toolbar-option', true); // Set custom attribute for value
625
-
626
- // Event listener to handle selection
627
- optionDiv.addEventListener('click', function (e) {
628
- dropdownContent.style.display = 'none';
629
- _plugin.click(option.value, core, options)
630
- });
631
-
632
- // Append option to the dropdown content div
633
- dropdownContent.appendChild(optionDiv);
634
- });
635
-
636
- dropdownButton.onclick = () => {
637
- if (dropdownContent.style.display === 'block') {
638
- dropdownContent.style.display = 'none'
639
- document.removeEventListener('mousedown', handleOutsideClick, false);
640
- core.elements.editor.removeEventListener('click', handleOutsideClick, false);
641
- return
642
- }
643
- dropdownContent.style.display = 'block'
644
- const container = getScrollParent(dropdownContent);
645
- const containerRect = container.getBoundingClientRect();
646
- const popoverRect = dropdownContent.getBoundingClientRect();
647
-
648
- const overflowRight = popoverRect.right > containerRect.right;
649
- const overflowLeft = popoverRect.left < containerRect.left;
650
-
651
- if (overflowRight) {
652
- dropdownContent.style.right = '10px';
653
- } else if (overflowLeft) {
654
- dropdownContent.style.left = '10px';
655
- }
656
- document.addEventListener('mousedown', handleOutsideClick);
657
- core.elements.editor.addEventListener('click', handleOutsideClick);
658
- }
659
-
660
- selectPlugin.appendChild(dropdownButton)
661
- selectPlugin.appendChild(dropdownContent)
662
- return selectPlugin
663
- }
664
-
665
- const makeEditToolbar = (options, core, { type, td, image, updateImage, updateTable }) => {
666
- const handleOutsideClick = (e) => {
667
- if (popoverPlugin.contains(e.target)) return
668
- popoverContent.style.display = 'none'
669
- document.removeEventListener('mousedown', handleOutsideClick, false);
670
- core.elements.editor.removeEventListener('click', handleOutsideClick, false);
671
- }
672
-
673
- const popoverPlugin = document.createElement('div');
674
- popoverPlugin.setAttribute('data-type', 'popover')
675
-
676
- const popoverButton = document.createElement('button');
677
- popoverButton.type = "button"
678
- popoverButton.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEM}`
679
- popoverButton.setAttribute('data-title', `Edit ${type === 'td' ? 'Cell' : type === 'image' ? 'Image' : "Embed"}`)
680
-
681
- const pluginIcon = document.createElement('div');
682
- pluginIcon.innerHTML = SVG.EDIT;
683
- popoverButton.appendChild(pluginIcon)
684
-
685
- const popoverContent = document.createElement('div');
686
- popoverContent.className = `${options.classPrefix}${CLASSES.POPOVER_CONTENT}`
687
-
688
- if (type === 'image' || type === 'figure') {
689
- const resizeByIds = {
690
- byPercentage: Math.random(),
691
- byPixels: Math.random(),
692
- percentageWidth: Math.random(),
693
- percentageHeight: Math.random(),
694
- pixelsWidth: Math.random(),
695
- pixelsHeight: Math.random(),
696
- percentage: Math.random(),
697
- pixels: Math.random(),
698
- aspectRatio: Math.random(),
699
- altText: Math.random(),
700
- submit: Math.random(),
701
- }
702
-
703
- popoverContent.innerHTML = (`
704
- <div style="border-bottom: 1px solid gray; padding-bottom: 4px;">
705
- <div>
706
- By:
707
- <input type="radio" value="percentage" id="${resizeByIds.byPercentage}" checked/>
708
- <label for="${resizeByIds.byPercentage}">Percentage</label>
709
- <input type="radio" value="pixels" id="${resizeByIds.byPixels}" />
710
- <label for="${resizeByIds.byPixels}">Pixels</label>
711
- </div>
712
- <div id="${resizeByIds.percentage}">
713
- <div style="display: flex; justify-content: space-between; margin-top: 4px">
714
- <label for="${resizeByIds.percentageWidth}">Width (%)</label>
715
- <input style="width: 100px" type="number" name='width' placeholder="Width" id="${resizeByIds.percentageWidth}" />
716
- </div>
717
- <div style="display: flex; justify-content: space-between; margin-top: 4px">
718
- <label for="${resizeByIds.percentageHeight}">Height (%)</label>
719
- <input style="width: 100px" type="number" name='height' placeholder="Height" id="${resizeByIds.percentageHeight}" />
720
- </div>
721
- </div>
722
- <div id="${resizeByIds.pixels}" style="display: none">
723
- <div style="display: flex; justify-content: space-between; margin-top: 4px">
724
- <label for="${resizeByIds.pixelsWidth}">Width (px)</label>
725
- <input style="width: 100px" type="number" name='width' placeholder="Width" id="${resizeByIds.pixelsWidth}" />
726
- </div>
727
- <div style="display: flex; justify-content: space-between; margin-top: 4px">
728
- <label for="${resizeByIds.pixelsHeight}">Height (px)</label>
729
- <input style="width: 100px" type="number" name='height' placeholder="Height" id="${resizeByIds.pixelsHeight}" />
730
- </div>
731
- </div>
732
- <div style="margin-top: 4px;">
733
- <input type="checkbox" id="${resizeByIds.aspectRatio}" checked/>
734
- <label for="${resizeByIds.aspectRatio}">Maintain Aspect Ratio</label>
735
- </div>
736
- ${type === "image" ? `
737
- <div style="display: flex; justify-content: space-between; margin-top: 4px">
738
- <label for="${resizeByIds.altText}">Alt Text</label>
739
- <input style="width: 100px" type="text" id="${resizeByIds.altText}" placeholder="Enter alt text" style="width: 100%;" />
740
- </div>` : ""}
741
- <div style="display: flex; justify-content: end; margin-top: 4px;">
742
- <button style="border: 1px solid gray; padding: 2px 4px; border-radius: 4px; color: white; background-color: hsl(160, 100%, 40%);" type="button" id="${resizeByIds.submit}">Update</button>
743
- </div>
744
- </div >
745
- `)
746
-
747
- const pluginSet = document.createElement('div')
748
- pluginSet.className = `${options.classPrefix}${CLASSES.RESIZE_ITEMS} `
749
- RESIZER_PLUGINS.forEach(plugin => {
750
- const pluginButton = document.createElement('button');
751
- pluginButton.type = "button"
752
- pluginButton.className = `${options.classPrefix}${CLASSES.RESIZE_ITEM} `
753
-
754
- pluginButton.setAttribute('data-title', plugin.title)
755
- pluginButton.setAttribute('data-type', 'button')
756
- pluginButton.onclick = (e) => {
757
- e.preventDefault();
758
- updateImage(plugin.event);
759
- };
760
-
761
- const pluginIcon = document.createElement('div');
762
- pluginIcon.innerHTML = plugin.icon;
763
- pluginButton.appendChild(pluginIcon)
764
- pluginSet.appendChild(pluginButton)
765
- })
766
- popoverContent.appendChild(pluginSet)
767
- popoverButton.onclick = () => {
768
- const onInputKeydown = (event) => {
769
- if (event.key === 'Enter') {
770
- event.preventDefault()
771
- resizeBy.submit.click();
772
- }
773
- }
774
-
775
- if (popoverContent.style.display === 'block') {
776
- popoverContent.style.display = 'none'
777
- document.removeEventListener('mousedown', handleOutsideClick, false);
778
- core.elements.editor.removeEventListener('click', handleOutsideClick, false);
779
- return
780
- }
781
- popoverContent.style.display = 'block'
782
- const container = getScrollParent(popoverContent);
783
- const containerRect = container.getBoundingClientRect();
784
- const popoverRect = popoverContent.getBoundingClientRect();
785
-
786
- const overflowRight = popoverRect.right > containerRect.right;
787
- const overflowLeft = popoverRect.left < containerRect.left;
788
-
789
- if (overflowRight) {
790
- popoverContent.style.right = '10px';
791
- } else if (overflowLeft) {
792
- popoverContent.style.left = '10px';
793
- }
794
- const resizeBy = {
795
- byPercentage: document.getElementById(resizeByIds.byPercentage),
796
- byPixels: document.getElementById(resizeByIds.byPixels),
797
- percentageWidth: document.getElementById(resizeByIds.percentageWidth),
798
- percentageHeight: document.getElementById(resizeByIds.percentageHeight),
799
- pixelsWidth: document.getElementById(resizeByIds.pixelsWidth),
800
- pixelsHeight: document.getElementById(resizeByIds.pixelsHeight),
801
- percentage: document.getElementById(resizeByIds.percentage),
802
- pixels: document.getElementById(resizeByIds.pixels),
803
- aspectRatio: document.getElementById(resizeByIds.aspectRatio),
804
- submit: document.getElementById(resizeByIds.submit),
805
- }
806
- if (type == "image") resizeBy.altText = document.getElementById(resizeByIds.altText)
807
-
808
- const imageClientRect = image.getBoundingClientRect()
809
- resizeBy.percentageWidth.value = (imageClientRect.width / image.naturalWidth) * 100
810
- resizeBy.percentageHeight.value = (imageClientRect.height / image.naturalHeight) * 100
811
- resizeBy.pixelsWidth.value = imageClientRect.width
812
- resizeBy.pixelsHeight.value = imageClientRect.height
813
- if (type == "image") resizeBy.altText.value = image.alt || "";
814
-
815
- resizeBy.byPercentage.onchange = (e) => {
816
- if (e.target.checked) {
817
- resizeBy.pixels.style.display = 'none'
818
- resizeBy.percentage.style.display = 'block'
819
- resizeBy.byPixels.checked = false
820
- }
821
- }
822
- resizeBy.byPixels.onchange = (e) => {
823
- if (e.target.checked) {
824
- resizeBy.percentage.style.display = 'none'
825
- resizeBy.pixels.style.display = 'block'
826
- resizeBy.byPercentage.checked = false
827
- }
828
- }
829
-
830
- const handlePercentage = (e) => {
831
- let value = parseInt(e.target.value)
832
- if (value) {
833
- if (value > 100) value = 100;
834
- if (resizeBy.aspectRatio.checked) {
835
- resizeBy.percentageWidth.value = value
836
- resizeBy.percentageHeight.value = value
837
- } else {
838
- e.target.value = value
839
- }
840
- }
841
- }
842
- resizeBy.percentageWidth.oninput = handlePercentage
843
- resizeBy.percentageHeight.oninput = handlePercentage
844
-
845
- const handlePixel = (e) => {
846
- let value = parseInt(e.target.value)
847
- if (value) {
848
- if (resizeBy.aspectRatio.checked) {
849
- let width, height;
850
- if (e.target.name === 'width') {
851
- width = value
852
- const delta = image.naturalWidth / value
853
- height = image.naturalHeight / delta
854
- } else {
855
- height = value
856
- const delta = image.naturalHeight / value
857
- width = image.naturalWidth / delta
858
- }
859
-
860
- resizeBy.pixelsWidth.value = width
861
- resizeBy.pixelsHeight.value = height
862
- } else {
863
- e.target.value = value
864
- }
865
- }
866
- }
867
- resizeBy.pixelsWidth.oninput = handlePixel
868
- resizeBy.pixelsHeight.oninput = handlePixel
869
-
870
- resizeBy.percentageWidth.onkeydown = onInputKeydown
871
- resizeBy.percentageHeight.onkeydown = onInputKeydown
872
- resizeBy.pixelsWidth.onkeydown = onInputKeydown
873
- resizeBy.pixelsHeight.onkeydown = onInputKeydown
874
-
875
- resizeBy.aspectRatio.onchange = (e) => {
876
- if (e.target.checked) {
877
- if (resizeBy.byPixels.checked) {
878
- const delta = image.naturalWidth / parseInt(resizeBy.pixelsWidth.value)
879
- const height = image.naturalHeight / delta
880
- resizeBy.pixelsHeight.value = height
881
- } else {
882
- resizeBy.percentageHeight.value = resizeBy.percentageWidth.value
883
- }
884
- }
885
- }
886
-
887
- resizeBy.submit.onclick = (e) => {
888
- if (resizeBy.byPixels.checked) {
889
- if (parseInt(resizeBy.pixelsWidth.value) && parseInt(resizeBy.pixelsHeight.value))
890
- updateImage('update', { width: resizeBy.pixelsWidth.value + 'px', height: resizeBy.pixelsHeight.value + 'px', })
891
- } else if (resizeBy.byPercentage.checked) {
892
- if (parseInt(resizeBy.percentageWidth.value) && parseInt(resizeBy.percentageHeight.value))
893
- updateImage('update', { width: resizeBy.percentageWidth.value + '%', height: resizeBy.percentageHeight.value + '%', })
894
- }
895
- if (type == "image") image.alt = resizeBy.altText.value;
896
- }
897
-
898
- document.addEventListener('mousedown', handleOutsideClick);
899
- core.elements.editor.addEventListener('click', handleOutsideClick);
900
- }
901
- } else if (type === 'td') {
902
- popoverContent.style.padding = '0';
903
-
904
- const tabsContainer = document.createElement('div');
905
- tabsContainer.className = `${options.classPrefix}${CLASSES.POPOVER_TABS}`
906
-
907
- const tabContentContainer = document.createElement('div');
908
- tabContentContainer.style.padding = '0px 2px';
909
-
910
- popoverContent.innerHTML = ''; // Clear existing content
911
- popoverContent.append(tabsContainer, tabContentContainer);
912
-
913
- const createPluginItem = (plugin) => {
914
- const isColorInput =
915
- plugin.event === 'border-color' ||
916
- plugin.event === 'background-color' ||
917
- plugin.event === 'row-background-color' ||
918
- plugin.event === 'column-background-color';
919
- const isDimensionInput = ['cell-width', 'cell-height', 'cell-padding'].includes(plugin.event);
920
-
921
- let pluginElement;
922
-
923
- if (isColorInput) {
924
- pluginElement = document.createElement('label');
925
- pluginElement.className = `${options.classPrefix}${CLASSES.RESIZE_ITEM}`;
926
- pluginElement.style.display = 'flex';
927
- pluginElement.style.gap = '6px';
928
- pluginElement.style.alignItems = 'center';
929
- pluginElement.style.width = '100%';
930
- pluginElement.style.cursor = 'pointer';
931
- pluginElement.style.padding = '4px 8px';
932
- pluginElement.onmouseenter = () => { pluginElement.style.backgroundColor = '#ddd'; };
933
- pluginElement.onmouseleave = () => { pluginElement.style.backgroundColor = 'transparent'; };
934
-
935
- const icon = document.createElement('span');
936
- icon.innerHTML = plugin.icon;
937
-
938
- const title = document.createElement('span');
939
- title.textContent = plugin.title;
940
-
941
- const colorInput = document.createElement('input');
942
- colorInput.type = "color";
943
- colorInput.style.width = '24px';
944
- colorInput.style.height = '24px';
945
- colorInput.style.padding = '0px';
946
- colorInput.style.border = 'none';
947
- colorInput.style.background = 'transparent';
948
- colorInput.style.cursor = 'pointer';
949
- colorInput.oninput = (e) => {
950
- updateTable(plugin.event, e.target.value);
951
- };
952
-
953
- pluginElement.append(icon, title, colorInput);
954
- } else if (isDimensionInput) {
955
- pluginElement = document.createElement('label');
956
- pluginElement.className = `${options.classPrefix}${CLASSES.RESIZE_ITEM}`;
957
- pluginElement.style.display = 'flex';
958
- pluginElement.style.gap = '8px';
959
- pluginElement.style.alignItems = 'center';
960
- pluginElement.style.width = '100%';
961
- pluginElement.style.cursor = 'pointer';
962
- pluginElement.style.padding = '4px 8px';
963
- pluginElement.onmouseenter = () => { pluginElement.style.backgroundColor = '#ddd'; };
964
- pluginElement.onmouseleave = () => { pluginElement.style.backgroundColor = 'transparent'; };
965
-
966
- const icon = document.createElement('span');
967
- icon.innerHTML = plugin.icon;
968
-
969
- const title = document.createElement('span');
970
- title.textContent = plugin.title;
971
-
972
- const input = document.createElement('input');
973
- input.type = "number";
974
- input.placeholder = "auto";
975
- input.style.width = '60px';
976
- input.style.padding = '2px';
977
- input.style.border = '1px solid #ccc';
978
- input.style.borderRadius = '4px';
979
- if (plugin.event === 'cell-width') {
980
- input.value = toPx(td.style.width) || '';
981
- } else if (plugin.event === 'cell-height') {
982
- input.value = toPx(td.parentElement.style.height) || '';
983
- } else if (plugin.event === 'cell-padding') {
984
- input.value = toPx(td.style.padding) || '';
985
- }
986
- input.onchange = (e) => updateTable(plugin.event, (Number.parseInt(e.target.value) || 0) + 'px');
987
- input.onclick = (e) => e.stopPropagation();
988
- pluginElement.append(icon, title, input);
989
- } else {
990
- pluginElement = document.createElement('button');
991
- pluginElement.type = "button";
992
- pluginElement.className = `${options.classPrefix}${CLASSES.RESIZE_ITEM}`;
993
- pluginElement.style.display = 'flex';
994
- pluginElement.style.alignItems = 'center';
995
- pluginElement.style.gap = '8px';
996
- pluginElement.style.width = '100%';
997
- pluginElement.style.padding = '4px 8px';
998
- pluginElement.style.border = 'none';
999
- pluginElement.style.background = 'transparent';
1000
- pluginElement.style.textAlign = 'left';
1001
- pluginElement.onmouseenter = () => { pluginElement.style.backgroundColor = '#ddd'; };
1002
- pluginElement.onmouseleave = () => { pluginElement.style.backgroundColor = 'transparent'; };
1003
-
1004
- pluginElement.onclick = (e) => {
1005
- e.preventDefault();
1006
- updateTable(plugin.event);
1007
- };
1008
-
1009
- const icon = document.createElement('div');
1010
- icon.innerHTML = plugin.icon;
1011
-
1012
- const title = document.createElement('span');
1013
- title.textContent = plugin.title;
1014
-
1015
- pluginElement.append(icon, title);
1016
- }
1017
- return pluginElement;
1018
- };
1019
-
1020
- Object.keys(TAB_CATEGORIES).forEach((categoryName, index) => {
1021
- const tabButton = document.createElement('button');
1022
- tabButton.type = 'button'
1023
- tabButton.textContent = categoryName;
1024
- tabButton.className = `${options.classPrefix}${CLASSES.POPOVER_TAB}`
1025
-
1026
- const tabId = categoryName.replaceAll(' ', '-');
1027
- tabButton.dataset.tab = tabId;
1028
-
1029
- const tabContent = document.createElement('div');
1030
- tabContent.id = tabId;
1031
- tabContent.className = `${options.classPrefix}${CLASSES.POPOVER_TAB_CONTENT}`;
1032
- tabContent.style.display = 'none';
1033
-
1034
- if (index === 0) {
1035
- tabButton.classList.add('active');
1036
- tabContent.style.display = 'block';
1037
- }
1038
-
1039
- const subCategories = TAB_CATEGORIES[categoryName];
1040
- Object.keys(subCategories).forEach(subCategoryName => {
1041
- if (subCategoryName) {
1042
- const subHeader = document.createElement('div');
1043
- subHeader.style.padding = '4px';
1044
- subHeader.textContent = subCategoryName;
1045
- tabContent.appendChild(subHeader);
1046
- }
1047
-
1048
- const pluginListContainer = document.createElement('div');
1049
- pluginListContainer.className = `${options.classPrefix}${CLASSES.RESIZE_ITEMS}`;
1050
- pluginListContainer.style.width = '100%';
1051
-
1052
- subCategories[subCategoryName].forEach(pluginEvent => {
1053
- const plugin = TABLE_PLUGINS.find(p => p.event === pluginEvent);
1054
- if (plugin) {
1055
- const item = createPluginItem(plugin);
1056
- pluginListContainer.appendChild(item);
1057
- }
1058
- });
1059
- tabContent.appendChild(pluginListContainer);
1060
- });
1061
-
1062
- tabsContainer.appendChild(tabButton);
1063
- tabContentContainer.appendChild(tabContent);
1064
-
1065
- tabButton.onclick = (e) => {
1066
- tabsContainer.querySelectorAll(`.${options.classPrefix}${CLASSES.POPOVER_TAB}`).forEach(btn => {
1067
- btn.classList.remove('active');
1068
- });
1069
- tabContentContainer.querySelectorAll(`.${options.classPrefix}${CLASSES.POPOVER_TAB_CONTENT}`).forEach(content => {
1070
- content.style.display = 'none';
1071
- });
1072
- e.currentTarget.classList.add('active');
1073
- tabContentContainer.querySelector(`#${e.currentTarget.getAttribute('data-tab')}`).style.display = 'block';
1074
- };
1075
- });
1076
-
1077
- popoverButton.onclick = () => {
1078
- if (popoverContent.style.display === 'block') {
1079
- popoverContent.style.display = 'none'
1080
- document.removeEventListener('mousedown', handleOutsideClick, false);
1081
- core.elements.editor.removeEventListener('click', handleOutsideClick, false);
1082
- return
1083
- }
1084
- popoverContent.style.display = 'block'
1085
- const container = getScrollParent(popoverContent);
1086
- const containerRect = container.getBoundingClientRect();
1087
- const popoverRect = popoverContent.getBoundingClientRect();
1088
-
1089
- const overflowRight = popoverRect.right > containerRect.right;
1090
- const overflowLeft = popoverRect.left < containerRect.left;
1091
-
1092
- if (overflowRight) {
1093
- popoverContent.style.right = '20px';
1094
- } else if (overflowLeft) {
1095
- popoverContent.style.left = '20px';
1096
- }
1097
- document.addEventListener('mousedown', handleOutsideClick);
1098
- core.elements.editor.addEventListener('click', handleOutsideClick);
1099
- }
1100
- }
1101
-
1102
- popoverPlugin.appendChild(popoverButton)
1103
- popoverPlugin.appendChild(popoverContent)
1104
- core.elements.toolbarContainer.appendChild(popoverPlugin)
1105
- core.elements.resizerPlugin = popoverPlugin
1106
- }
1107
-
1108
- const openModal = ({ title, bodyNode, footerNode }, core, options) => {
1109
- const modalContainer = document.createElement('div');
1110
- modalContainer.className = `${options.classPrefix}${CLASSES.MODAL} `
1111
-
1112
- const modalContent = document.createElement('div');
1113
- modalContent.className = `${options.classPrefix}${CLASSES.MODAL_CONTENT} `
1114
-
1115
- const modalHeader = document.createElement('div');
1116
- modalHeader.className = `${options.classPrefix}${CLASSES.MODAL_HEADER} `
1117
- const modalTitle = document.createElement('span');
1118
- modalTitle.innerText = title ?? ''
1119
- const modalCloseButton = document.createElement('button');
1120
- modalCloseButton.className = 'modal-close-btn';
1121
- modalCloseButton.innerHTML = SVG.CLOSE;
1122
- modalCloseButton.type = 'button'
1123
- modalCloseButton.onclick = () => {
1124
- core.elements.base.removeChild(modalContainer)
1125
- }
1126
- modalHeader.append(modalTitle, modalCloseButton)
1127
-
1128
- const modalBody = document.createElement('div');
1129
- modalBody.className = `${options.classPrefix}${CLASSES.MODAL_BODY} `
1130
- modalBody.appendChild(bodyNode)
1131
-
1132
- const modalFooter = document.createElement('div');
1133
- modalFooter.className = `${options.classPrefix}${CLASSES.MODAL_FOOTER} `
1134
- modalFooter.appendChild(footerNode)
1135
-
1136
- modalContent.appendChild(modalHeader);
1137
- modalContent.appendChild(modalBody);
1138
- modalContent.appendChild(modalFooter);
1139
-
1140
- modalContainer.appendChild(modalContent)
1141
- core.elements.base.appendChild(modalContainer)
1142
-
1143
- return {
1144
- close: () => { core.elements.base.removeChild(modalContainer) }
1145
- }
1146
- }
1147
-
1148
- const openDraggableModal = ({ title, bodyNode, footerNode, onClose }, core, options) => {
1149
- const modalContainer = document.createElement('div');
1150
- modalContainer.className = `${options.classPrefix}${CLASSES.MODAL} ${options.classPrefix}${CLASSES.DRAGGABLE_MODAL} `
1151
-
1152
- const modalContent = document.createElement('div');
1153
- modalContent.className = `${options.classPrefix}${CLASSES.MODAL_CONTENT} `
1154
-
1155
- const modalHeader = document.createElement('div');
1156
- modalHeader.className = `${options.classPrefix}${CLASSES.MODAL_HEADER} `
1157
- if (title instanceof HTMLElement) {
1158
- modalHeader.appendChild(title);
1159
- } else {
1160
- const modalTitle = document.createElement('span');
1161
- modalTitle.innerText = title ?? ''
1162
- modalHeader.appendChild(modalTitle);
1163
- }
1164
- const modalCloseButton = document.createElement('button');
1165
- modalCloseButton.className = 'modal-close-btn';
1166
- modalCloseButton.innerHTML = SVG.CLOSE;
1167
- modalCloseButton.type = 'button'
1168
- modalCloseButton.onclick = () => {
1169
- modalContainer.remove();
1170
- if (typeof onClose === 'function') onClose()
1171
- }
1172
- modalHeader.appendChild(modalCloseButton)
1173
-
1174
- let isDragging = false;
1175
- let currentX;
1176
- let currentY;
1177
- let initialX;
1178
- let initialY;
1179
- let xOffset = 0;
1180
- let yOffset = 0;
1181
-
1182
- const dragStart = (e) => {
1183
- if (e.target.closest('button')) return;
1184
-
1185
- if (e.type === "touchstart") {
1186
- initialX = e.touches[0].clientX - xOffset;
1187
- initialY = e.touches[0].clientY - yOffset;
1188
- } else {
1189
- initialX = e.clientX - xOffset;
1190
- initialY = e.clientY - yOffset;
1191
- }
1192
-
1193
- isDragging = true;
1194
-
1195
- document.addEventListener('mouseup', dragEnd);
1196
- document.addEventListener('touchend', dragEnd);
1197
- core.elements.iframeWindow.addEventListener('mouseup', dragEnd);
1198
- core.elements.iframeWindow.addEventListener('touchend', dragEnd);
1199
- document.addEventListener('mousemove', drag);
1200
- document.addEventListener('touchmove', drag);
1201
- };
1202
-
1203
- const dragEnd = () => {
1204
- isDragging = false;
1205
- document.removeEventListener('mouseup', dragEnd, false);
1206
- document.removeEventListener('touchend', dragEnd, false);
1207
- core.elements.iframeWindow.removeEventListener('mouseup', dragEnd, false);
1208
- core.elements.iframeWindow.removeEventListener('touchend', dragEnd, false);
1209
- document.removeEventListener('mousemove', drag, false);
1210
- document.removeEventListener('touchmove', drag, false);
1211
- };
1212
-
1213
- const drag = (e) => {
1214
- if (isDragging) {
1215
- e.preventDefault();
1216
- currentX = (e.type === "touchmove" ? e.touches[0].clientX : e.clientX) - initialX;
1217
- currentY = (e.type === "touchmove" ? e.touches[0].clientY : e.clientY) - initialY;
1218
-
1219
- xOffset = currentX;
1220
- yOffset = currentY;
1221
-
1222
- modalContainer.style.transform = `translate(-50%, -50%) translate3d(${currentX}px, ${currentY}px, 0)`;
1223
- }
1224
- };
1225
-
1226
- modalHeader.addEventListener('mousedown', dragStart, false);
1227
- modalHeader.addEventListener('touchstart', dragStart, false);
1228
-
1229
- const modalBody = document.createElement('div');
1230
- modalBody.className = `${options.classPrefix}${CLASSES.MODAL_BODY} `
1231
- modalBody.appendChild(bodyNode)
1232
-
1233
- const modalFooter = document.createElement('div');
1234
- modalFooter.className = `${options.classPrefix}${CLASSES.MODAL_FOOTER} `
1235
- modalFooter.appendChild(footerNode)
1236
-
1237
- modalContent.appendChild(modalHeader);
1238
- modalContent.appendChild(modalBody);
1239
- modalContent.appendChild(modalFooter);
1240
-
1241
- modalContainer.appendChild(modalContent)
1242
- core.elements.base.appendChild(modalContainer)
1243
-
1244
- return {
1245
- close: () => {
1246
- modalContainer.remove();
1247
- if (typeof onClose === 'function') onClose()
1248
- }
1249
- }
1250
- }
1251
-
1252
-
1253
- const initImageResizer = (type, image, options, core) => {
1254
- destroyImageResizer(options, core)
1255
- changeAllToolbarState(core, 'disabled', ['link', 'align_justify', 'align_left', 'align_right', 'align_center'])
1256
- core.elements.editor.blur() // to remove focus from the editor
1257
-
1258
- const imageClientRect = image.getBoundingClientRect();
1259
- const resizer = document.createElement('div');
1260
- resizer.className = `resizer`
1261
- resizer.style.width = imageClientRect.width + 'px';
1262
- resizer.style.height = imageClientRect.height + 'px';
1263
- resizer.style.top = imageClientRect.top + 'px';
1264
- resizer.style.left = imageClientRect.left + 'px';
1265
-
1266
- // Full-screen button (only for images)
1267
- if (type === 'image') {
1268
- const fullscreenButton = document.createElement('span');
1269
- fullscreenButton.innerHTML = SVG.FULLSCREEN_IMAGE
1270
- fullscreenButton.style.position = 'absolute';
1271
- fullscreenButton.style.top = '5px';
1272
- fullscreenButton.style.right = '5px';
1273
- fullscreenButton.style.background = 'rgba(0, 0, 0, 0.6)';
1274
- fullscreenButton.style.color = 'white';
1275
- fullscreenButton.style.border = 'none';
1276
- fullscreenButton.style.cursor = 'pointer';
1277
- fullscreenButton.style.borderRadius = '4px';
1278
- fullscreenButton.style.fontSize = '14px';
1279
- fullscreenButton.style.transition = '0.3s ease-in-out';
1280
- fullscreenButton.style.width = '28px';
1281
- fullscreenButton.style.height = '28px';
1282
- fullscreenButton.style.display = 'flex';
1283
- fullscreenButton.style.alignItems = 'center';
1284
- fullscreenButton.style.justifyContent = 'center';
1285
-
1286
- fullscreenButton.addEventListener('click', () => {
1287
- const anchor = image.closest('a');
1288
- let isVideo = false;
1289
- let src = image.src;
1290
-
1291
- if (anchor?.dataset?.leksyPreview === 'video-url') {
1292
- const href = anchor.getAttribute('href');
1293
- if (href) {
1294
- isVideo = true;
1295
- src = href;
1296
- }
1297
- }
1298
- if (options.onFullScreen instanceof Function) {
1299
- const type = isVideo ? 'video/mp4' : 'image/png';
1300
- options.onFullScreen(src, type);
1301
- }
1302
- else createPreviewModal(src, core, options, isVideo);
1303
- });
1304
-
1305
- resizer.appendChild(fullscreenButton);
1306
- }
1307
-
1308
- let firstX, firstY, direction, aspectRatio, resizerClientRect;
1309
- const handleMouseDown = (e) => {
1310
- resizerClientRect = resizer.getBoundingClientRect();
1311
- direction = e.target.getAttribute('data-resize')
1312
- firstX = e.clientX || e.touches[0].clientX;
1313
- firstY = e.clientY || e.touches[0].clientY;
1314
- aspectRatio = resizer.offsetWidth / resizer.offsetHeight;
1315
- core.elements.iframeWindow.addEventListener('mousemove', handleMouseMove)
1316
- core.elements.iframeWindow.addEventListener('mouseup', handleMouseUp)
1317
- core.elements.iframeWindow.addEventListener('touchmove', handleMouseMove)
1318
- core.elements.iframeWindow.addEventListener('touchend', handleMouseUp)
1319
- }
1320
-
1321
- const handleMouseMove = (e) => {
1322
- const position = {
1323
- clientX: e.clientX || e.touches[0].clientX,
1324
- clientY: e.clientY || e.touches[0].clientY,
1325
- }
1326
- // Calculate the new dimensions
1327
- if (direction === 'up') {
1328
- let top = resizerClientRect.top + (position.clientY - firstY);
1329
- let height = resizerClientRect.height - (position.clientY - firstY);
1330
- if (height < 0) {
1331
- top = resizerClientRect.bottom;
1332
- height = 0
1333
- }
1334
- resizer.style.top = top + 'px';
1335
- resizer.style.height = height + 'px';
1336
- } else if (direction === 'bottom') {
1337
- let height = resizerClientRect.height + (position.clientY - firstY);
1338
- if (height < 0) height = 0
1339
- resizer.style.height = height + 'px';
1340
- } else if (direction === 'left') {
1341
- let left = resizerClientRect.left + (position.clientX - firstX);
1342
- let width = resizerClientRect.width - (position.clientX - firstX);
1343
- if (width < 0) {
1344
- left = resizerClientRect.right;
1345
- width = 0
1346
- }
1347
- resizer.style.left = left + 'px';
1348
- resizer.style.width = width + 'px';
1349
- } else if (direction === 'right') {
1350
- let width = resizerClientRect.width + (position.clientX - firstX);
1351
- if (width < 0) width = 0
1352
- resizer.style.width = width + 'px';
1353
- } else if (direction === 'bottom-right') {
1354
- const distanceX = position.clientX - firstX
1355
- const distanceY = position.clientY - firstY
1356
- const delta = Math.sign(distanceX) === -1 || Math.sign(distanceY) === -1
1357
- ? Math.min(distanceX, distanceY)
1358
- : Math.max(distanceX, distanceY)
1359
- const width = resizerClientRect.width + delta;
1360
- const height = width / aspectRatio;
1361
- resizer.style.width = width + 'px';
1362
- resizer.style.height = height + 'px';
1363
- } else if (direction === 'bottom-left') {
1364
- const distanceX = position.clientX - firstX
1365
- const distanceY = position.clientY - firstY
1366
- const delta = distanceX > 0 ? Math.max(distanceX, distanceY) : distanceX
1367
- let width = resizerClientRect.width - delta;
1368
- if (width < 0) width = 0
1369
- let left = resizerClientRect.left + resizerClientRect.width - width;
1370
- const height = width / aspectRatio;
1371
- resizer.style.width = width + 'px';
1372
- resizer.style.left = left + 'px';
1373
- resizer.style.height = height + 'px';
1374
- } else if (direction === 'up-right') {
1375
- const distanceX = position.clientX - firstX
1376
- const distanceY = position.clientY - firstY
1377
- const delta = distanceX > 0 ? Math.max(distanceX, distanceY) : distanceX
1378
-
1379
- let width = resizerClientRect.width + delta;
1380
- if (width < 0) width = 0
1381
- const height = width / aspectRatio;
1382
- const top = resizerClientRect.top + (resizerClientRect.height - height)
1383
- resizer.style.top = top + 'px';
1384
- resizer.style.width = width + 'px';
1385
- resizer.style.height = height + 'px';
1386
- } else if (direction === 'up-left') {
1387
- const distanceX = position.clientX - firstX
1388
- const distanceY = position.clientY - firstY
1389
- const delta = distanceX > 0 ? Math.max(distanceX, distanceY) : distanceX
1390
- let width = resizerClientRect.width - delta;
1391
- if (width < 0) width = 0
1392
- const height = width / aspectRatio;
1393
- const top = resizerClientRect.top + (resizerClientRect.height - height)
1394
- const left = resizerClientRect.left + (resizerClientRect.width - width)
1395
- resizer.style.top = top + 'px';
1396
- resizer.style.width = width + 'px';
1397
- resizer.style.height = height + 'px';
1398
- resizer.style.left = left + 'px';
1399
- }
1400
- position.textContent = `${parseInt(resizer.style.width)}x${parseInt(resizer.style.height)} `;
1401
- }
1402
-
1403
- const handleMouseUp = (e) => {
1404
- image.style.width = parseInt(resizer.style.width) + 'px'
1405
- image.style.height = parseInt(resizer.style.height) + 'px'
1406
- const imageClientRect = image.getBoundingClientRect();
1407
- resizer.style.width = imageClientRect.width + 'px';
1408
- resizer.style.height = imageClientRect.height + 'px';
1409
- resizer.style.top = imageClientRect.top + 'px';
1410
- resizer.style.left = imageClientRect.left + 'px';
1411
- core.elements.iframeWindow.removeEventListener('mousemove', handleMouseMove, false)
1412
- core.elements.iframeWindow.removeEventListener('mouseup', handleMouseUp, false)
1413
- core.elements.iframeWindow.removeEventListener('touchmove', handleMouseMove, false)
1414
- core.elements.iframeWindow.removeEventListener('touchend', handleMouseUp, false)
1415
- }
1416
-
1417
- const dot1 = document.createElement('button');
1418
- dot1.type = 'button'
1419
- dot1.className = 'dot up-left'
1420
- dot1.setAttribute('data-resize', 'up-left')
1421
- dot1.addEventListener('mousedown', handleMouseDown)
1422
- dot1.addEventListener('touchstart', handleMouseDown)
1423
-
1424
- const dot2 = document.createElement('button');
1425
- dot2.type = 'button'
1426
- dot2.className = 'dot up'
1427
- dot2.setAttribute('data-resize', 'up')
1428
- dot2.addEventListener('mousedown', handleMouseDown)
1429
- dot2.addEventListener('touchstart', handleMouseDown)
1430
-
1431
- const dot3 = document.createElement('button');
1432
- dot3.type = 'button'
1433
- dot3.className = 'dot up-right'
1434
- dot3.setAttribute('data-resize', 'up-right')
1435
- dot3.addEventListener('mousedown', handleMouseDown)
1436
- dot3.addEventListener('touchstart', handleMouseDown)
1437
-
1438
- const dot4 = document.createElement('button');
1439
- dot4.type = 'button'
1440
- dot4.className = 'dot right'
1441
- dot4.setAttribute('data-resize', 'right')
1442
- dot4.addEventListener('mousedown', handleMouseDown)
1443
- dot4.addEventListener('touchstart', handleMouseDown)
1444
-
1445
- const dot5 = document.createElement('button');
1446
- dot5.type = 'button'
1447
- dot5.className = 'dot bottom-right'
1448
- dot5.setAttribute('data-resize', 'bottom-right')
1449
- dot5.addEventListener('mousedown', handleMouseDown)
1450
- dot5.addEventListener('touchstart', handleMouseDown)
1451
-
1452
- const dot6 = document.createElement('button');
1453
- dot6.type = 'button'
1454
- dot6.className = 'dot bottom'
1455
- dot6.setAttribute('data-resize', 'bottom')
1456
- dot6.addEventListener('mousedown', handleMouseDown)
1457
- dot6.addEventListener('touchstart', handleMouseDown)
1458
-
1459
- const dot7 = document.createElement('button');
1460
- dot7.type = 'button'
1461
- dot7.className = 'dot bottom-left'
1462
- dot7.setAttribute('data-resize', 'bottom-left')
1463
- dot7.addEventListener('mousedown', handleMouseDown)
1464
- dot7.addEventListener('touchstart', handleMouseDown)
1465
-
1466
- const dot8 = document.createElement('button');
1467
- dot8.type = 'button'
1468
- dot8.className = 'dot left'
1469
- dot8.setAttribute('data-resize', 'left')
1470
- dot8.addEventListener('mousedown', handleMouseDown)
1471
- dot8.addEventListener('touchstart', handleMouseDown)
1472
-
1473
- const position = document.createElement('div')
1474
- position.className = 'position';
1475
- position.textContent = `${parseInt(imageClientRect.width)}x${parseInt(imageClientRect.height)} `;
1476
-
1477
- resizer.append(dot1, dot2, dot3, dot4, dot5, dot6, dot7, dot8, position)
1478
-
1479
- const updateImage = (event, data) => {
1480
- switch (event) {
1481
- case 'update':
1482
- image.style.width = data.width;
1483
- image.style.height = data.height;
1484
- break;
1485
- case '100%':
1486
- case '75%':
1487
- case '50%':
1488
- case 'auto':
1489
- image.style.width = event;
1490
- image.style.height = 'unset';
1491
- break;
1492
- case 'wrap-left':
1493
- image.style.float = 'left';
1494
- image.style.margin = '0 10px 10px 0';
1495
- break;
1496
- case 'wrap-right':
1497
- image.style.float = 'right';
1498
- image.style.margin = '0 0 10px 10px';
1499
- break;
1500
- case 'revert':
1501
- image.style.width = 'unset';
1502
- image.style.height = 'unset';
1503
- image.style.float = 'none';
1504
- image.style.margin = '';
1505
- break;
1506
- case 'delete':
1507
- image.remove()
1508
- destroyImageResizer(options, core)
1509
- break;
1510
- }
1511
- const imageClientRect = image.getBoundingClientRect();
1512
- resizer.style.width = imageClientRect.width + 'px';
1513
- resizer.style.height = imageClientRect.height + 'px';
1514
- resizer.style.top = imageClientRect.top + 'px';
1515
- resizer.style.left = imageClientRect.left + 'px';
1516
- position.textContent = `${parseInt(resizer.style.width)}x${parseInt(resizer.style.height)} `;
1517
- }
1518
-
1519
- makeEditToolbar(options, core, { type, image, updateImage })
1520
-
1521
- core.elements.resizerImage = image
1522
- core.elements.resizer = resizer
1523
- core.elements.iframeWindow.body.appendChild(resizer)
1524
- core.resizerHandler = (e) => updateImageResizerPosition(e, core);
1525
- core.deleteImageHandler = (e) => {
1526
- if (e.key === 'Backspace' || e.key === 'Delete') updateImage('delete')
1527
- }
1528
- core.elements.iframeWindow.addEventListener('scroll', core.resizerHandler)
1529
- core.elements.iframeWindow.addEventListener('keydown', core.deleteImageHandler)
1530
- }
1531
-
1532
- const createPreviewModal = (src, core, options, isVideo = false) => {
1533
- // Create overlay
1534
- const overlay = document.createElement('div');
1535
- overlay.className = `${options.classPrefix}${CLASSES.PREVIEW_MODAL_OVERLAY} `
1536
-
1537
- // Create modal content
1538
- const modalContent = document.createElement('div');
1539
- modalContent.className = `${options.classPrefix}${CLASSES.PREVIEW_MODAL_CONTENT} `
1540
-
1541
- // Close button
1542
- const closeButton = document.createElement('span');
1543
- closeButton.className = `${options.classPrefix}${CLASSES.PREVIEW_MODAL_CLOSE} `
1544
- closeButton.innerHTML = SVG.CLOSE;
1545
- closeButton.onclick = () => overlay.remove();
1546
-
1547
- // Create modal body
1548
- const modalBody = document.createElement('div');
1549
- modalBody.className = `${options.classPrefix}${CLASSES.PREVIEW_MODAL_BODY} `
1550
-
1551
- let element;
1552
- if (isVideo) {
1553
- element = document.createElement('video');
1554
- element.src = src;
1555
- element.controls = true;
1556
- element.style.maxWidth = '100%';
1557
- element.style.maxHeight = '100%';
1558
- element.setAttribute('controlsList', 'nodownload');
1559
- element.setAttribute('disablePictureInPicture', true);
1560
- } else {
1561
- element = document.createElement('img');
1562
- element.src = src;
1563
- element.alt = 'Preview Image';
1564
- }
1565
-
1566
- modalBody.appendChild(element);
1567
- modalContent.append(closeButton, modalBody);
1568
- overlay.appendChild(modalContent);
1569
-
1570
- // Append to body
1571
- document.body.appendChild(overlay);
1572
-
1573
- overlay.addEventListener('click', (event) => {
1574
- if (event.target !== element) {
1575
- overlay.remove();
1576
- }
1577
- });
1578
- // Show modal
1579
- setTimeout(() => {
1580
- overlay.classList.add('active');
1581
- }, 10);
1582
- }
1583
-
1584
- const updateImageResizerPosition = (e, core) => {
1585
- const imageClientRect = core.elements.resizerImage.getBoundingClientRect();
1586
- core.elements.resizer.style.width = imageClientRect.width + 'px';
1587
- core.elements.resizer.style.height = imageClientRect.height + 'px';
1588
- core.elements.resizer.style.top = imageClientRect.top + 'px';
1589
- core.elements.resizer.style.left = imageClientRect.left + 'px';
1590
- }
1591
-
1592
- const destroyImageResizer = (options, core) => {
1593
- if (!options.disabled) changeAllToolbarState(core, 'enabled')
1594
- if (core.elements.resizerPlugin) core.elements.toolbarContainer.removeChild(core.elements.resizerPlugin)
1595
- if (core.elements.resizer) core.elements.iframeWindow.body.removeChild(core.elements.resizer)
1596
- core.elements.iframeWindow.removeEventListener('scroll', core.resizerHandler, false)
1597
- core.elements.iframeWindow.removeEventListener('scroll', core.deleteImageHandler, false)
1598
- core.elements.iframeWindow.removeEventListener('keydown', core.deleteImageHandler, false)
1599
- delete core.elements.resizer
1600
- delete core.resizerHandler
1601
- delete core.deleteImageHandler
1602
- delete core.elements.resizerImage
1603
- delete core.elements.resizerPlugin
1604
- }
1605
-
1606
- const getTableGrid = (table) => {
1607
- const grid = [];
1608
- const rows = table.rows;
1609
- for (let r = 0; r < rows.length; r++) {
1610
- if (!grid[r]) grid[r] = [];
1611
- const row = rows[r];
1612
- let colIndex = 0;
1613
- for (let c = 0; c < row.cells.length; c++) {
1614
- const cell = row.cells[c];
1615
- // Skip occupied cells
1616
- while (grid[r][colIndex]) colIndex++;
1617
-
1618
- const rowspan = cell.rowSpan || 1;
1619
- const colspan = cell.colSpan || 1;
1620
-
1621
- for (let rs = 0; rs < rowspan; rs++) {
1622
- for (let cs = 0; cs < colspan; cs++) {
1623
- if (!grid[r + rs]) grid[r + rs] = [];
1624
- grid[r + rs][colIndex + cs] = cell;
1625
- }
1626
- }
1627
- colIndex += colspan;
1628
- }
1629
- }
1630
- return grid;
1631
- }
1632
-
1633
- const getCellPosition = (grid, cell) => {
1634
- for (let r = 0; r < grid.length; r++) {
1635
- for (let c = 0; c < grid[r].length; c++) {
1636
- if (grid[r][c] === cell) return { r, c };
1637
- }
1638
- }
1639
- return null;
1640
- }
1641
-
1642
- const initTableEditPlugin = (td, options, core) => {
1643
- destroyTableEditPlugin(options, core)
1644
- const table = td.closest('table');
1645
- if (!table) return
1646
-
1647
- const tableClientRect = table.getBoundingClientRect();
1648
- let firstX, firstY;
1649
-
1650
- const handleColumnResizeMouseMove = (e) => {
1651
- const cell = e.target.closest('th, td');
1652
- if (!cell) return;
1653
-
1654
- const rect = cell.getBoundingClientRect();
1655
- const isNearRightEdge =
1656
- rect.right - e.clientX <= RESIZE_MARGIN;
1657
-
1658
- table.style.cursor = isNearRightEdge ? 'col-resize' : 'default';
1659
- };
1660
-
1661
- const handleColumnResizeMouseDown = (e) => {
1662
- const isTouch = e.type === 'touchstart';
1663
- const clientX = isTouch ? e.touches[0].clientX : e.clientX;
1664
-
1665
- if (!isTouch && table.style.cursor !== 'col-resize') return;
1666
- const target = e.target.closest('td, th');
1667
- if (!target) return;
1668
-
1669
- if (isTouch) {
1670
- const rect = target.getBoundingClientRect();
1671
- const isNearRightEdge = rect.right - clientX <= RESIZE_MARGIN;
1672
- if (!isNearRightEdge) return;
1673
- }
1674
-
1675
- e.preventDefault();
1676
- e.stopPropagation();
1677
-
1678
- const startX = clientX;
1679
-
1680
- const firstRowCells = Array.from(table.rows[0].cells);
1681
- firstRowCells.forEach(cell => {
1682
- cell.style.width = cell.getBoundingClientRect().width + 'px';
1683
- });
1684
-
1685
- const targetRect = target.getBoundingClientRect();
1686
- const resizeTargetIndex = firstRowCells.findIndex(cell => Math.abs(cell.getBoundingClientRect().right - targetRect.right) < 5);
1687
-
1688
- if (resizeTargetIndex === -1) return;
1689
-
1690
- const resizeTarget = firstRowCells[resizeTargetIndex];
1691
- const nextTarget = firstRowCells[resizeTargetIndex + 1];
1692
-
1693
- const startWidth = resizeTarget.getBoundingClientRect().width;
1694
- const startNextWidth = nextTarget ? nextTarget.getBoundingClientRect().width : 0;
1695
- const startTableWidth = table.getBoundingClientRect().width;
1696
-
1697
- const handleMouseMove = (event) => {
1698
- const currentX = event.type === 'touchmove' ? event.touches[0].clientX : event.clientX;
1699
- const diff = currentX - startX;
1700
-
1701
- if (nextTarget) {
1702
- const minWidth = 30;
1703
- const constrainedDiff = Math.min(Math.max(diff, minWidth - startWidth), startNextWidth - minWidth);
1704
- resizeTarget.style.width = (startWidth + constrainedDiff) + 'px';
1705
- nextTarget.style.width = (startNextWidth - constrainedDiff) + 'px';
1706
- } else {
1707
- const newWidth = Math.max(startWidth + diff, 30);
1708
- const effectiveDiff = newWidth - startWidth;
1709
- resizeTarget.style.width = newWidth + 'px';
1710
- table.style.width = (startTableWidth + effectiveDiff) + 'px';
1711
- }
1712
- core.elements.iframeWindow.body.style.cursor = 'col-resize';
1713
- };
1714
-
1715
- const handleMouseUp = () => {
1716
- core.elements.iframeWindow.body.style.cursor = '';
1717
- core.elements.iframeWindow.removeEventListener('mousemove', handleMouseMove);
1718
- core.elements.iframeWindow.removeEventListener('mouseup', handleMouseUp);
1719
- core.elements.iframeWindow.removeEventListener('touchmove', handleMouseMove);
1720
- core.elements.iframeWindow.removeEventListener('touchend', handleMouseUp);
1721
- };
1722
-
1723
- core.elements.iframeWindow.addEventListener('mousemove', handleMouseMove);
1724
- core.elements.iframeWindow.addEventListener('mouseup', handleMouseUp);
1725
- core.elements.iframeWindow.addEventListener('touchmove', handleMouseMove);
1726
- core.elements.iframeWindow.addEventListener('touchend', handleMouseUp);
1727
- };
1728
-
1729
- table.addEventListener('mousemove', handleColumnResizeMouseMove);
1730
- table.addEventListener('mousedown', handleColumnResizeMouseDown);
1731
- table.addEventListener('touchstart', handleColumnResizeMouseDown);
1732
-
1733
- core.tableColumnResizeHandlers = {
1734
- mousemove: handleColumnResizeMouseMove,
1735
- mousedown: handleColumnResizeMouseDown,
1736
- touchstart: handleColumnResizeMouseDown
1737
- };
1738
-
1739
- const handleMouseDown = (e) => {
1740
- firstX = e.clientX || e.touches[0].clientX;
1741
- firstY = e.clientY || e.touches[0].clientY;
1742
-
1743
- core.elements.iframeWindow.addEventListener('mousemove', handleMouseMove)
1744
- core.elements.iframeWindow.addEventListener('mouseup', handleMouseUp)
1745
- core.elements.iframeWindow.addEventListener('touchmove', handleMouseMove)
1746
- core.elements.iframeWindow.addEventListener('touchend', handleMouseUp)
1747
- }
1748
-
1749
- const handleMouseMove = (e) => {
1750
- // Calculate the new dimensions
1751
- const distanceX = (e.clientX || e.touches[0].clientX) - firstX
1752
- const distanceY = (e.clientY || e.touches[0].clientY) - firstY
1753
- const width = tableClientRect.width + distanceX;
1754
- const height = tableClientRect.height + distanceY;
1755
-
1756
- table.style.width = width + 'px';
1757
- table.style.height = height + 'px';
1758
- }
1759
-
1760
- const handleMouseUp = (e) => {
1761
- core.elements.iframeWindow.removeEventListener('mousemove', handleMouseMove, false)
1762
- core.elements.iframeWindow.removeEventListener('mouseup', handleMouseUp, false)
1763
- core.elements.iframeWindow.removeEventListener('touchmove', handleMouseMove, false)
1764
- core.elements.iframeWindow.removeEventListener('touchend', handleMouseUp, false)
1765
- }
1766
-
1767
- const updateTable = (event, value) => {
1768
- const rowIndex = td.parentElement.rowIndex;
1769
- const cellIndex = td.cellIndex;
1770
- const colCount = table.rows[0].cells.length; // Number of columns in the table
1771
-
1772
- const addCellWithBorder = (row, index) => {
1773
- const newCell = row.insertCell(index);
1774
- newCell.innerHTML = '\u00A0'; // Add content (empty for now)
1775
- newCell.style.border = '1px solid black'; // Set the border
1776
- }
1777
-
1778
- // Helper function to toggle border between 'none' and '1px solid black'
1779
- const toggleBorder = (cell, side) => {
1780
- if (cell) {
1781
- const currentBorder = cell.style[side];
1782
- cell.style[side] = ['none', 'medium', 'medium none', '0px none rgb(0, 0, 0)'].includes(currentBorder) ? '1px solid black' : 'none';
1783
- }
1784
- };
1785
-
1786
- // Helper function to check if the table has no rows and remove the table if needed
1787
- const checkAndRemoveTable = () => {
1788
- if (table.rows.length === 0) {
1789
- table.remove();
1790
- }
1791
- };
1792
-
1793
- switch (event) {
1794
- case 'insert-row-above': {
1795
- const newRow = table.insertRow(rowIndex); // Insert row above
1796
- for (let i = 0; i < colCount; i++) {
1797
- addCellWithBorder(newRow, i); // Add proper <td> cells with border
1798
- }
1799
- break;
1800
- }
1801
- case 'insert-row-below': {
1802
- const newRow = table.insertRow(rowIndex + 1); // Insert row below
1803
- for (let i = 0; i < colCount; i++) {
1804
- addCellWithBorder(newRow, i); // Add proper <td> cells with border
1805
- }
1806
- break;
1807
- }
1808
- case 'delete-row': {
1809
- table.deleteRow(rowIndex); // Delete the row
1810
- destroyTableEditPlugin(options, core)
1811
- checkAndRemoveTable()
1812
- break;
1813
- }
1814
- case 'merge-cell-right': {
1815
- const grid = getTableGrid(table);
1816
- const pos = getCellPosition(grid, td);
1817
- if (!pos) break;
1818
- const { r, c } = pos;
1819
- const targetCell = grid[r][c + (td.colSpan || 1)];
1820
-
1821
- if (targetCell && targetCell !== td &&
1822
- (targetCell.rowSpan || 1) === (td.rowSpan || 1) &&
1823
- getCellPosition(grid, targetCell).r === r
1824
- ) {
1825
- if (targetCell.innerHTML.trim() !== '') {
1826
- td.innerHTML += '<br/>' + targetCell.innerHTML;
1827
- }
1828
- td.colSpan = (td.colSpan || 1) + (targetCell.colSpan || 1);
1829
- targetCell.remove();
1830
- }
1831
- break;
1832
- }
1833
- case 'merge-cell-left': {
1834
- const grid = getTableGrid(table);
1835
- const pos = getCellPosition(grid, td);
1836
- if (!pos) break;
1837
- const { r, c } = pos;
1838
- if (c === 0) break;
1839
- const targetCell = grid[r][c - 1];
1840
-
1841
- if (targetCell && targetCell !== td &&
1842
- (targetCell.rowSpan || 1) === (td.rowSpan || 1) &&
1843
- getCellPosition(grid, targetCell).r === r
1844
- ) {
1845
- if (targetCell.innerHTML.trim() !== '') {
1846
- td.innerHTML = targetCell.innerHTML + '<br/>' + td.innerHTML;
1847
- }
1848
- td.colSpan = (td.colSpan || 1) + (targetCell.colSpan || 1);
1849
- targetCell.parentNode.insertBefore(td, targetCell);
1850
- targetCell.remove();
1851
- }
1852
- break;
1853
- }
1854
- case 'merge-cell-down': {
1855
- const grid = getTableGrid(table);
1856
- const pos = getCellPosition(grid, td);
1857
- if (!pos) break;
1858
- const { r, c } = pos;
1859
- const targetRowIdx = r + (td.rowSpan || 1);
1860
- if (targetRowIdx >= grid.length) break;
1861
- const targetCell = grid[targetRowIdx][c];
1862
-
1863
- if (targetCell && targetCell !== td &&
1864
- (targetCell.colSpan || 1) === (td.colSpan || 1) &&
1865
- getCellPosition(grid, targetCell).c === c
1866
- ) {
1867
- if (targetCell.innerHTML.trim() !== '') {
1868
- td.innerHTML += '<br/>' + targetCell.innerHTML;
1869
- }
1870
- td.rowSpan = (td.rowSpan || 1) + (targetCell.rowSpan || 1);
1871
- targetCell.remove();
1872
- }
1873
- break;
1874
- }
1875
- case 'merge-cell-up': {
1876
- const grid = getTableGrid(table);
1877
- const pos = getCellPosition(grid, td);
1878
- if (!pos) break;
1879
- const { r, c } = pos;
1880
- if (r === 0) break;
1881
- const targetCell = grid[r - 1][c];
1882
-
1883
- if (targetCell && targetCell !== td &&
1884
- (targetCell.colSpan || 1) === (td.colSpan || 1) &&
1885
- getCellPosition(grid, targetCell).c === c
1886
- ) {
1887
- if (targetCell.innerHTML.trim() !== '') {
1888
- td.innerHTML = targetCell.innerHTML + '<br/>' + td.innerHTML;
1889
- }
1890
- td.rowSpan = (td.rowSpan || 1) + (targetCell.rowSpan || 1);
1891
- targetCell.parentNode.insertBefore(td, targetCell);
1892
- targetCell.remove();
1893
- }
1894
- break;
1895
- }
1896
- case 'insert-column-left': {
1897
- for (const element of table.rows) {
1898
- const row = element;
1899
- addCellWithBorder(row, cellIndex); // Add a new <td> with border to the left
1900
- }
1901
- break;
1902
- }
1903
- case 'insert-column-right': {
1904
- for (const element of table.rows) {
1905
- const row = element;
1906
- addCellWithBorder(row, cellIndex + 1); // Add a new <td> with border to the right
1907
- }
1908
- break;
1909
- }
1910
- case 'delete-column': {
1911
- for (const element of table.rows) {
1912
- element.deleteCell(cellIndex); // Delete the cell at the current index
1913
- }
1914
- checkAndRemoveTable()
1915
- destroyTableEditPlugin(options, core)
1916
- break;
1917
- }
1918
- case 'split-cell-vertical': {
1919
- const colspan = td.colSpan || 1;
1920
- if (colspan > 1) {
1921
- td.colSpan = colspan - 1;
1922
- const newCell = td.parentNode.insertCell(td.cellIndex + 1);
1923
- newCell.innerHTML = '\u00A0';
1924
- newCell.style.border = td.style.border;
1925
- newCell.rowSpan = td.rowSpan;
1926
- }
1927
- break;
1928
- }
1929
- case 'split-cell-horizontal': {
1930
- const rowspan = td.rowSpan || 1;
1931
- if (rowspan > 1) {
1932
- td.rowSpan = rowspan - 1;
1933
- const grid = getTableGrid(table);
1934
- const pos = getCellPosition(grid, td);
1935
- const { r, c } = pos;
1936
- const nextRowIdx = r + rowspan - 1;
1937
- const nextRow = table.rows[nextRowIdx];
1938
- let insertIndex = 0;
1939
- for (let i = 0; i < nextRow.cells.length; i++) {
1940
- const cell = nextRow.cells[i];
1941
- const cellPos = getCellPosition(grid, cell);
1942
- if (cellPos.c < c) insertIndex++;
1943
- else break;
1944
- }
1945
-
1946
- const newCell = nextRow.insertCell(insertIndex);
1947
- newCell.innerHTML = '\u00A0';
1948
- newCell.style.border = td.style.border;
1949
- newCell.colSpan = td.colSpan;
1950
- }
1951
- break;
1952
- }
1953
- case 'align-left': {
1954
- table.style.marginLeft = '0';
1955
- table.style.marginRight = 'auto';
1956
- break;
1957
- }
1958
- case 'align-center': {
1959
- table.style.marginLeft = 'auto';
1960
- table.style.marginRight = 'auto';
1961
- break;
1962
- }
1963
- case 'align-right': {
1964
- table.style.marginLeft = 'auto';
1965
- table.style.marginRight = '0';
1966
- break;
1967
- }
1968
- case 'cell-border-top': {
1969
- toggleBorder(td, 'borderTop');
1970
- if (rowIndex > 0) {
1971
- const aboveCell = table.rows[rowIndex - 1].cells[cellIndex];
1972
- toggleBorder(aboveCell, 'borderBottom');
1973
- }
1974
- break;
1975
- }
1976
- case 'cell-border-right': {
1977
- toggleBorder(td, 'borderRight');
1978
- if (cellIndex < table.rows[rowIndex].cells.length - 1) {
1979
- const rightCell = table.rows[rowIndex].cells[cellIndex + 1];
1980
- toggleBorder(rightCell, 'borderLeft');
1981
- }
1982
- break;
1983
- }
1984
- case 'cell-border-bottom': {
1985
- toggleBorder(td, 'borderBottom');
1986
- if (rowIndex < table.rows.length - 1) {
1987
- const belowCell = table.rows[rowIndex + 1].cells[cellIndex];
1988
- toggleBorder(belowCell, 'borderTop');
1989
- }
1990
- break;
1991
- }
1992
- case 'cell-border-left': {
1993
- toggleBorder(td, 'borderLeft');
1994
- if (cellIndex > 0) {
1995
- const leftCell = table.rows[rowIndex].cells[cellIndex - 1];
1996
- toggleBorder(leftCell, 'borderRight');
1997
- }
1998
- break;
1999
- }
2000
- case 'cell-border-full': {
2001
- td.style.border = '1px solid black';
2002
- // Handle top neighbor
2003
- if (rowIndex > 0) {
2004
- if (table.rows[rowIndex - 1].cells[cellIndex])
2005
- table.rows[rowIndex - 1].cells[cellIndex].style.borderBottom = '1px solid black';
2006
- }
2007
-
2008
- // Handle bottom neighbor
2009
- if (rowIndex < table.rows.length - 1) {
2010
- if (table.rows[rowIndex + 1].cells[cellIndex])
2011
- table.rows[rowIndex + 1].cells[cellIndex].style.borderTop = '1px solid black';
2012
- }
2013
-
2014
- // Handle left neighbor
2015
- if (cellIndex > 0) {
2016
- if (table.rows[rowIndex].cells[cellIndex - 1])
2017
- table.rows[rowIndex].cells[cellIndex - 1].style.borderRight = '1px solid black';
2018
- }
2019
-
2020
- // Handle right neighbor
2021
- if (cellIndex < table.rows[rowIndex].cells.length - 1) {
2022
- if (table.rows[rowIndex].cells[cellIndex + 1])
2023
- table.rows[rowIndex].cells[cellIndex + 1].style.borderLeft = '1px solid black';
2024
- }
2025
- break;
2026
- }
2027
- case 'cell-border-none': {
2028
- // Remove all borders from the current cell
2029
- td.style.border = 'none';
2030
-
2031
- // Handle top neighbor
2032
- if (rowIndex > 0) {
2033
- if (table.rows[rowIndex - 1].cells[cellIndex])
2034
- table.rows[rowIndex - 1].cells[cellIndex].style.borderBottom = 'none';
2035
- }
2036
-
2037
- // Handle bottom neighbor
2038
- if (rowIndex < table.rows.length - 1) {
2039
- if (table.rows[rowIndex + 1].cells[cellIndex])
2040
- table.rows[rowIndex + 1].cells[cellIndex].style.borderTop = 'none';
2041
- }
2042
-
2043
- // Handle left neighbor
2044
- if (cellIndex > 0) {
2045
- if (table.rows[rowIndex].cells[cellIndex - 1])
2046
- table.rows[rowIndex].cells[cellIndex - 1].style.borderRight = 'none';
2047
- }
2048
-
2049
- // Handle right neighbor
2050
- if (cellIndex < table.rows[rowIndex].cells.length - 1) {
2051
- if (table.rows[rowIndex].cells[cellIndex + 1])
2052
- table.rows[rowIndex].cells[cellIndex + 1].style.borderLeft = 'none';
2053
- }
2054
- break;
2055
- }
2056
- case 'fixed-column-width': {
2057
- if (table.style.tableLayout === 'fixed') {
2058
- table.style.tableLayout = 'auto';
2059
- } else {
2060
- table.style.tableLayout = 'fixed';
2061
- table.style.wordBreak = 'break-all'
2062
- }
2063
- break;
2064
- }
2065
- case 'border-color': {
2066
- td.style.borderColor = value;
2067
-
2068
- // Handle top neighbor
2069
- if (rowIndex > 0) {
2070
- if (table.rows[rowIndex - 1].cells[cellIndex])
2071
- table.rows[rowIndex - 1].cells[cellIndex].style.borderBottomColor = value;
2072
- }
2073
-
2074
- // Handle bottom neighbor
2075
- if (rowIndex < table.rows.length - 1) {
2076
- if (table.rows[rowIndex + 1].cells[cellIndex])
2077
- table.rows[rowIndex + 1].cells[cellIndex].style.borderTopColor = value;
2078
- }
2079
-
2080
- // Handle left neighbor
2081
- if (cellIndex > 0) {
2082
- if (table.rows[rowIndex].cells[cellIndex - 1])
2083
- table.rows[rowIndex].cells[cellIndex - 1].style.borderRightColor = value;
2084
- }
2085
-
2086
- // Handle right neighbor
2087
- if (cellIndex < table.rows[rowIndex].cells.length - 1) {
2088
- if (table.rows[rowIndex].cells[cellIndex + 1])
2089
- table.rows[rowIndex].cells[cellIndex + 1].style.borderLeftColor = value;
2090
- }
2091
- break;
2092
- }
2093
- case 'background-color': {
2094
- td.style.backgroundColor = value;
2095
- break;
2096
- }
2097
- case 'row-background-color': {
2098
- const row = td.parentElement;
2099
- for (let i = 0; i < row.cells.length; i++) {
2100
- row.cells[i].style.backgroundColor = value;
2101
- }
2102
- break;
2103
- }
2104
- case 'column-background-color': {
2105
- for (let r = 0; r < table.rows.length; r++) {
2106
- const cell = table.rows[r].cells[cellIndex];
2107
- if (cell) {
2108
- cell.style.backgroundColor = value;
2109
- }
2110
- }
2111
- break;
2112
- }
2113
- case 'cell-width': {
2114
- for (let i = 0; i < table.rows.length; i++) {
2115
- const cell = table.rows[i].cells[cellIndex];
2116
- if (cell) cell.style.width = value;
2117
- }
2118
- break;
2119
- }
2120
- case 'cell-height': {
2121
- td.parentElement.style.height = value;
2122
- break;
2123
- }
2124
- case 'cell-padding': {
2125
- td.style.padding = value;
2126
- break;
2127
- }
2128
-
2129
- }
2130
- }
2131
-
2132
- makeEditToolbar(options, core, { type: 'td', td, updateTable })
2133
-
2134
-
2135
- const resizer = document.createElement('div');
2136
- resizer.className = `table-resizer`
2137
- resizer.style.width = tableClientRect.width + 'px';
2138
- resizer.style.height = tableClientRect.height + 'px';
2139
- resizer.style.top = tableClientRect.top + 'px';
2140
- resizer.style.left = tableClientRect.left + 'px';
2141
-
2142
- const resizerButton = document.createElement('button');
2143
- resizerButton.className = `table-resizer-btn`
2144
- resizerButton.style.top = tableClientRect.bottom - 6 + 'px';
2145
- resizerButton.style.left = tableClientRect.right - 6 + 'px';
2146
- resizerButton.addEventListener('mousedown', handleMouseDown)
2147
- resizerButton.addEventListener('touchstart', handleMouseDown)
2148
-
2149
- resizer.appendChild(resizerButton)
2150
-
2151
- core.elements.table = table
2152
- core.elements.tableResizer = resizer
2153
- core.elements.tableResizerBtn = resizerButton
2154
- core.resizerHandler = (e) => updateTableResizerPosition(options, core);
2155
- core.elements.iframeWindow.addEventListener('scroll', core.resizerHandler)
2156
- core.elements.iframeWindow.body.appendChild(resizer)
2157
- }
2158
-
2159
- const updateTableResizerPosition = (options, core) => {
2160
- if (core.elements.tableResizerBtn) {
2161
- if (!core.elements.editor.contains(core.elements.table)) return destroyTableEditPlugin(options, core)
2162
- const tableClientRect = core.elements.table.getBoundingClientRect();
2163
- core.elements.tableResizer.style.width = tableClientRect.width + 'px';
2164
- core.elements.tableResizer.style.height = tableClientRect.height + 'px';
2165
- core.elements.tableResizer.style.top = tableClientRect.top + 'px';
2166
- core.elements.tableResizer.style.left = tableClientRect.left + 'px';
2167
- core.elements.tableResizerBtn.style.top = tableClientRect.bottom - 6 + 'px';
2168
- core.elements.tableResizerBtn.style.left = tableClientRect.right - 6 + 'px';
2169
- }
2170
- }
2171
-
2172
- const destroyTableEditPlugin = (options, core) => {
2173
- if (core.elements.table && core.tableColumnResizeHandlers) {
2174
- core.elements.table.removeEventListener('mousemove', core.tableColumnResizeHandlers.mousemove);
2175
- core.elements.table.removeEventListener('mousedown', core.tableColumnResizeHandlers.mousedown);
2176
- core.elements.table.removeEventListener('touchstart', core.tableColumnResizeHandlers.touchstart);
2177
- core.elements.table.style.cursor = '';
2178
- delete core.tableColumnResizeHandlers;
2179
- }
2180
- if (core.elements.resizerPlugin) core.elements.toolbarContainer.removeChild(core.elements.resizerPlugin)
2181
- if (core.elements.tableResizer) core.elements.iframeWindow.body.removeChild(core.elements.tableResizer)
2182
- core.elements.iframeWindow.removeEventListener('scroll', core.resizerHandler, false)
2183
- delete core.elements.resizerPlugin
2184
- delete core.resizerHandler
2185
- delete core.elements.table
2186
- delete core.elements.tableResizer
2187
- delete core.elements.tableResizerBtn
2188
- }
2189
-
2190
- const changeToolbarStatByPlugin = (plugin, action, value) => {
2191
- switch (action) {
2192
- case 'disabled':
2193
- if (plugin.getAttribute('data-type') === 'button') {
2194
- plugin.disabled = true
2195
- plugin.classList.add('disabled')
2196
- } else if (plugin.getAttribute('data-type') === 'dropdown' || plugin.getAttribute('data-type') === 'select') {
2197
- plugin.childNodes[0].disabled = true
2198
- plugin.childNodes[0].classList.add('disabled')
2199
- } else if (plugin.getAttribute('data-type') === 'color') {
2200
- plugin.childNodes[0].disabled = true
2201
- plugin.classList.add('disabled')
2202
- } else if (plugin.getAttribute('data-type') === 'button-select') {
2203
- plugin.querySelectorAll('button').forEach(btn => {
2204
- btn.disabled = true
2205
- btn.classList.add('disabled')
2206
- })
2207
- }
2208
- break;
2209
- case 'enabled':
2210
- if (plugin.getAttribute('data-type') === 'button') {
2211
- plugin.disabled = false
2212
- plugin.classList.remove('disabled')
2213
- } else if (plugin.getAttribute('data-type') === 'dropdown' || plugin.getAttribute('data-type') === 'select') {
2214
- plugin.childNodes[0].disabled = false
2215
- plugin.childNodes[0].classList.remove('disabled')
2216
- } else if (plugin.getAttribute('data-type') === 'color') {
2217
- plugin.childNodes[0].disabled = false
2218
- plugin.classList.remove('disabled')
2219
- } else if (plugin.getAttribute('data-type') === 'button-select') {
2220
- plugin.querySelectorAll('button').forEach(btn => {
2221
- btn.disabled = false
2222
- btn.classList.remove('disabled')
2223
- })
2224
- }
2225
- break;
2226
- case 'active':
2227
- if (plugin.getAttribute('data-type') === 'button') {
2228
- plugin.classList.add('active')
2229
- } else if (plugin.getAttribute('data-type') === 'dropdown' || plugin.getAttribute('data-type') === 'select') {
2230
- plugin.childNodes[0].classList.add('active')
2231
- } else if (plugin.getAttribute('data-type') === 'color') {
2232
- plugin.classList.add('active')
2233
- } else if (plugin.getAttribute('data-type') === 'button-select') {
2234
- plugin.querySelector('button[data-type="button"]')?.classList.add('active')
2235
- }
2236
- break;
2237
- case 'inactive':
2238
- if (plugin.getAttribute('data-type') === 'button') {
2239
- plugin.classList.remove('active')
2240
- } else if (plugin.getAttribute('data-type') === 'dropdown' || plugin.getAttribute('data-type') === 'select') {
2241
- plugin.childNodes[0].classList.remove('active')
2242
- } else if (plugin.getAttribute('data-type') === 'color') {
2243
- plugin.classList.remove('active')
2244
- } else if (plugin.getAttribute('data-type') === 'button-select') {
2245
- plugin.querySelector('button[data-type="button"]')?.classList.remove('active')
2246
- }
2247
- break;
2248
- case 'font':
2249
- plugin.childNodes[0].childNodes[0].childNodes[0].innerHTML = FONTS[value] ? FONTS[value] : value ? value : 'Font'
2250
- break;
2251
- case 'font-size':
2252
- plugin.childNodes[0].childNodes[0].childNodes[0].innerHTML = FONT_SIZES[value] ? FONT_SIZES[value] : value ? value : 'Font-Size'
2253
- break;
2254
- case 'format-block':
2255
- plugin.childNodes[0].childNodes[0].childNodes[0].innerHTML = FORMATS[value] ?? 'Format'
2256
- break;
2257
- case 'font_color':
2258
- plugin.childNodes[0].value = value || '#000000'
2259
- break;
2260
- case 'highlight_color':
2261
- plugin.childNodes[0].value = value || '#ffffff'
2262
- break;
2263
- case 'icon':
2264
- plugin.innerHTML = value
2265
- }
2266
- }
2267
-
2268
- const changeAllToolbarState = (core, action, preventPlugins = []) => {
2269
- Object.keys(core.elements.toolbar).forEach(pluginName => {
2270
- core.elements.toolbar[pluginName].forEach(plugin => {
2271
- if (preventPlugins.includes(pluginName)) return
2272
- changeToolbarStatByPlugin(plugin, action)
2273
- })
2274
- })
2275
- }
2276
-
2277
- const changeToolbarStateByName = (core, action, pluginNames = []) => {
2278
- Object.keys(core.elements.toolbar).forEach(pluginName => {
2279
- if (!pluginNames.includes(pluginName)) return
2280
- core.elements.toolbar[pluginName].forEach(plugin => {
2281
- changeToolbarStatByPlugin(plugin, action)
2282
- })
2283
- })
2284
- }
2285
-
2286
- const changeToolbarValueByName = (core, pluginName, value) => {
2287
- core.elements.toolbar[pluginName]?.forEach(plugin => {
2288
- changeToolbarStatByPlugin(plugin, pluginName, value)
2289
- })
2290
- }
2291
-
2292
- const changeToolbarHtmlByName = (core, pluginName, value) => {
2293
- core.elements.toolbar[pluginName]?.forEach(plugin => {
2294
- changeToolbarStatByPlugin(plugin, 'icon', value)
2295
- })
2296
- }
2297
-
2298
- const rgbToHex = (rgb) => {
2299
- const rgbArray = rgb.match(/\d+/g); // Extracts the numeric RGB values
2300
- return `#${rgbArray.map(x => {
2301
- const hex = parseInt(x).toString(16);
2302
- return hex.length === 1 ? '0' + hex : hex; // Pads with 0 if needed
2303
- }).join('')
2304
- } `.slice(0, 7);
2305
- };
2306
-
2307
- const extractSocialMediaId = (url) => {
2308
- for (const platform in SOCIAL_MEDIA_PATTERNS) {
2309
- const match = url.match(SOCIAL_MEDIA_PATTERNS[platform]);
2310
- if (match) {
2311
-
2312
- return { platform, id: match[1] };
2313
- }
2314
- }
2315
-
2316
- return { platform: null, id: null };
2317
- };
2318
-
2319
- const constructEmbedUrl = (platform, id) => {
2320
- if (!SOCIAL_MEDIA_BASEURLS[platform] || !id) return null;
2321
- const parentDomain = window.location.hostname;
2322
- switch (platform) {
2323
- case "INSTAGRAM":
2324
- case "THREADS":
2325
- return `${SOCIAL_MEDIA_BASEURLS[platform]}${id}/embed`;
2326
- case "REDDIT":
2327
- return `${SOCIAL_MEDIA_BASEURLS[platform]}${id}/?ref_source=embed&ref=share`;
2328
- case "TWITCH":
2329
- case "TWITCH_CHANNEL":
2330
- return `${SOCIAL_MEDIA_BASEURLS[platform]}${id}&parent=${parentDomain}&autoplay=false&time=0h0m0s`
2331
- default:
2332
- return `${SOCIAL_MEDIA_BASEURLS[platform]}${id}`;
2333
- }
2334
- };
2335
-
2336
- const showAnchorPopover = (element, x, y, options, core) => {
2337
- let href = element.getAttribute("href");
2338
-
2339
- // Create popover container
2340
- let popover = document.createElement("div");
2341
- popover.id = "anchor-popover";
2342
- popover.style.position = "absolute";
2343
- popover.style.top = "0";
2344
- popover.style.left = "0";
2345
- popover.style.padding = "8px";
2346
- popover.style.background = "white";
2347
- popover.style.border = "1px solid #ccc";
2348
- popover.style.borderRadius = "6px";
2349
- popover.style.boxShadow = "0px 4px 8px rgba(0,0,0,0.2)";
2350
- popover.style.zIndex = "1000";
2351
- popover.style.display = "flex";
2352
- popover.style.alignItems = "center";
2353
- popover.style.gap = "5px";
2354
- popover.style.whiteSpace = "nowrap";
2355
- popover.style.maxWidth = "300px";
2356
- popover.style.visibility = "hidden";
2357
-
2358
- // Create clickable link
2359
- let linkDiv = document.createElement("div");
2360
- linkDiv.textContent = href;
2361
- linkDiv.style.textDecoration = "none";
2362
- linkDiv.style.color = "#007bff";
2363
- linkDiv.style.padding = "5px";
2364
- linkDiv.style.overflow = "hidden";
2365
- linkDiv.style.textOverflow = "ellipsis";
2366
- linkDiv.style.webkitBoxOrient = "vertical";
2367
- linkDiv.style.webkitLineClamp = "1";
2368
- linkDiv.style.maxWidth = "250px";
2369
- linkDiv.style.wordBreak = "break-word";
2370
- linkDiv.style.cursor = "pointer";
2371
- linkDiv.onclick = () => core.onLinkClick(href);
2372
-
2373
- // Create Edit button
2374
- let editButton = document.createElement("span");
2375
- editButton.innerHTML = SVG.EDIT_LINK;
2376
- editButton.style.padding = "5px";
2377
- editButton.title = "Edit link";
2378
- editButton.onclick = (event) => editAnchorTag(event, core, options);
2379
-
2380
- // Create Copy button
2381
- let copyButton = document.createElement("span");
2382
- copyButton.innerHTML = SVG.COPY_LINK;
2383
- copyButton.style.padding = "5px";
2384
- copyButton.title = "Copy link";
2385
- copyButton.onclick = (event) => navigator.clipboard.writeText(href);
2386
-
2387
- // Create Delete button
2388
- let deleteButton = document.createElement("span");
2389
- deleteButton.innerHTML = SVG.DELETE_LINK;
2390
- deleteButton.style.padding = "5px";
2391
- deleteButton.title = "Remove link";
2392
- deleteButton.onclick = (event) => removeAnchorTag(event, element, core);
2393
-
2394
- // Append elements to popover
2395
- popover.append(linkDiv, copyButton, editButton, deleteButton);
2396
-
2397
- let parent = core.elements.iframeWindow.body;
2398
- parent.appendChild(popover);
2399
-
2400
- requestAnimationFrame(() => {
2401
- let popoverRect = popover.getBoundingClientRect();
2402
- let parentRect = parent.getBoundingClientRect();
2403
-
2404
- let scrollX = parent.scrollLeft;
2405
- let scrollY = parent.scrollTop;
2406
-
2407
- let finalX = x + scrollX + 5;
2408
- let finalY = y + scrollY + 5;
2409
-
2410
- let spaceRight = parentRect.right - x;
2411
- let spaceLeft = x - parentRect.left;
2412
- let spaceBelow = parentRect.bottom - y;
2413
- let spaceAbove = y - parentRect.top;
2414
-
2415
- if (spaceBelow < popoverRect.height && spaceAbove >= popoverRect.height) {
2416
- finalY = y + scrollY - popoverRect.height - 5;
2417
- } else if (spaceBelow < popoverRect.height) {
2418
- finalY = Math.max(0, y + scrollY - popoverRect.height);
2419
- }
2420
-
2421
- if (spaceRight < popoverRect.width && spaceLeft >= popoverRect.width) {
2422
- finalX = x + scrollX - popoverRect.width - 5;
2423
- } else if (spaceRight < popoverRect.width) {
2424
- finalX = Math.max(0, x + scrollX - popoverRect.width);
2425
- }
2426
-
2427
- // Boundary safety
2428
- finalX = Math.max(5, Math.min(finalX, parent.scrollWidth - popoverRect.width - 5));
2429
- finalY = Math.max(5, Math.min(finalY, parent.scrollHeight - popoverRect.height - 5));
2430
-
2431
- popover.style.left = `${finalX}px`;
2432
- popover.style.top = `${finalY}px`;
2433
- popover.style.visibility = "visible";
2434
- });
2435
-
2436
- core.elements.anchorPopover = popover;
2437
-
2438
- const closePopover = (event) => {
2439
- if (!popover.contains(event.target) || event.key === "Escape") {
2440
- destroyAnchorPopover(core);
2441
- document.removeEventListener("click", closePopover);
2442
- document.removeEventListener("keydown", closePopover);
2443
- document.removeEventListener("scroll", closePopover, true);
2444
- }
2445
- };
2446
-
2447
- document.addEventListener("click", closePopover);
2448
- document.addEventListener("keydown", closePopover);
2449
- document.addEventListener("scroll", closePopover, true);
2450
- };
2451
-
2452
-
2453
- const editAnchorTag = (event, core, options) => {
2454
- event.stopPropagation();
2455
-
2456
- if (!core.elements.selectedElement || core.elements.selectedElement.tagName !== "A") {
2457
- console.warn("No anchor tag selected for editing.");
2458
- return;
2459
- }
2460
-
2461
- const anchorElement = core.elements.selectedElement;
2462
- let anchorLink = anchorElement.getAttribute("href") || "";
2463
- let anchorText = anchorElement.innerText || "";
2464
-
2465
- const body = document.createElement("div");
2466
-
2467
- const linkLabel = document.createElement("label");
2468
- linkLabel.innerText = "Link";
2469
- linkLabel.style.display = "block";
2470
- linkLabel.style.paddingLeft = "4px";
2471
-
2472
- // Link Input
2473
- const inputLink = document.createElement("input");
2474
- inputLink.value = anchorLink;
2475
- inputLink.type = "text";
2476
- inputLink.placeholder = "Insert link, mail, phone no";
2477
-
2478
- const textLabel = document.createElement("label");
2479
- textLabel.innerText = "Link Text";
2480
- textLabel.style.display = "block";
2481
- textLabel.style.marginTop = "8px";
2482
- textLabel.style.paddingLeft = "4px";
2483
-
2484
- // Text Input
2485
- const inputText = document.createElement("input");
2486
- inputText.value = anchorText;
2487
- inputText.type = "text";
2488
- inputText.placeholder = "Link text";
2489
-
2490
- const span = document.createElement("span");
2491
- span.className = "warning";
2492
-
2493
- body.append(linkLabel, inputLink, span, textLabel, inputText);
2494
-
2495
- // Footer (Buttons)
2496
- const footer = document.createElement("div");
2497
- const button = document.createElement("button");
2498
- button.type = "button";
2499
- button.className = "submit";
2500
- button.innerText = "Update";
2501
-
2502
- footer.append(button);
2503
-
2504
- // Open Modal
2505
- const modal = openModal(
2506
- {
2507
- title: "Edit Link",
2508
- bodyNode: body,
2509
- footerNode: footer,
2510
- },
2511
- core,
2512
- options
2513
- );
2514
-
2515
- inputLink.focus();
2516
-
2517
- // Handle Enter Key Submission
2518
- const onInputKeydown = (event) => {
2519
- if (event.key === "Enter") {
2520
- event.preventDefault();
2521
- button.click();
2522
- }
2523
- };
2524
-
2525
- inputLink.addEventListener("keydown", onInputKeydown);
2526
- inputText.addEventListener("keydown", onInputKeydown);
2527
-
2528
- // Button Click Logic
2529
- button.onclick = () => {
2530
- const newLink = inputLink.value.trim();
2531
- const newText = inputText.value.trim();
2532
-
2533
- if (REGEX.URL.test(newLink)) {
2534
- anchorElement.setAttribute("href", newLink);
2535
- anchorElement.innerText = newText || newLink;
2536
- modal.close();
2537
- } else {
2538
- span.innerText = "Invalid URL";
2539
- }
2540
- };
2541
- destroyAnchorPopover(core);
2542
- core.updateCaretPosition();
2543
- };
2544
-
2545
- const removeAnchorTag = (event, element, core) => {
2546
- event.stopPropagation();
2547
- const anchorContent = core.elements.selectedElement.innerHTML;
2548
- core.elements.selectedElement.outerHTML = anchorContent;
2549
-
2550
- destroyAnchorPopover(core);
2551
- };
2552
-
2553
- const destroyAnchorPopover = (core) => {
2554
- if (core.elements.anchorPopover) {
2555
- core.elements.anchorPopover.style.display = "none"
2556
- delete core.elements.anchorPopover;
2557
- }
2558
-
2559
- };
2560
-
2561
- function getCharIndex(root, node, offset) {
2562
- const range = document.createRange();
2563
- range.selectNodeContents(root);
2564
- range.setEnd(node, offset);
2565
- return range.toString().length;
2566
- }
2567
-
2568
- const getNodeAndOffsetFromIndex = (root, index) => {
2569
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
2570
- let currentNode, currentIndex = 0;
2571
-
2572
- while ((currentNode = walker.nextNode())) {
2573
- const len = currentNode.textContent.length;
2574
- if (currentIndex + len >= index) {
2575
- return { node: currentNode, offset: index - currentIndex };
2576
- }
2577
- currentIndex += len;
2578
- }
2579
-
2580
- return null;
2581
- }
2582
-
2583
- const getTightClientRects = (root, range) => {
2584
- const rects = [];
2585
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
2586
-
2587
- let node;
2588
- while ((node = walker.nextNode())) {
2589
- // For each text node, check if it overlaps our range
2590
- const nodeRange = document.createRange();
2591
- nodeRange.selectNodeContents(node);
2592
-
2593
- // If nodeRange and range do not intersect, skip
2594
- if (
2595
- range.compareBoundaryPoints(Range.END_TO_START, nodeRange) >= 0 ||
2596
- range.compareBoundaryPoints(Range.START_TO_END, nodeRange) <= 0
2597
- ) {
2598
- continue;
2599
- }
2600
-
2601
- // Compute the overlap offsets:
2602
- const startOffset = node === range.startContainer
2603
- ? range.startOffset
2604
- : 0;
2605
- const endOffset = node === range.endContainer
2606
- ? range.endOffset
2607
- : node.textContent.length;
2608
-
2609
- // Create a sub-range just for that text node’s overlap
2610
- const subRange = document.createRange();
2611
- subRange.setStart(node, startOffset);
2612
- subRange.setEnd(node, endOffset);
2613
-
2614
- // Gather the tight bounding rect(s) for that fragment
2615
- Array.from(subRange.getClientRects()).forEach(r => rects.push(r));
2616
-
2617
- subRange.detach();
2618
- }
2619
-
2620
- return rects;
2621
- }
2622
-
2623
- /**
2624
- * Draws highlight overlays behind exactly the text selected in `range`.
2625
- * - `editor` is the contenteditable container (positioned ancestor).
2626
- * - `color` is a CSS color (e.g. "#ff0000")—we’ll add “33” alpha for a translucent fill.
2627
- */
2628
- const highlightTextRange = (editor, range, color, overlayContainer) => {
2629
- const editorBox = editor.getBoundingClientRect();
2630
-
2631
- // 1. Get all tight rectangles for only the text portions
2632
- const tightRects = getTightClientRects(editor, range);
2633
-
2634
- // 2. Draw a div behind each rect
2635
- tightRects.forEach(rect => {
2636
- const highlight = document.createElement('div');
2637
- highlight.style.position = 'absolute';
2638
- highlight.style.left = (rect.left + editor.scrollLeft - editorBox.left) + 'px';
2639
- highlight.style.top = (rect.top + editor.scrollTop - editorBox.top) + 'px';
2640
- highlight.style.width = rect.width + 'px';
2641
- highlight.style.height = rect.height + 'px';
2642
- highlight.style.background = color + '33'; // add alpha for translucency
2643
- overlayContainer.appendChild(highlight);
2644
- });
2645
- }
2646
-
2647
- const showRemoteCursor = (core, user, data) => {
2648
- const { id, color, name } = user
2649
- const { startIndex, endIndex, cursorIndex, isCollapsed } = data;
2650
-
2651
- const editor = core.elements.editor;
2652
- const overlayContainer = core.elements.editorCursor; // Where overlays go
2653
-
2654
- if (!editor || !overlayContainer) return;
2655
-
2656
- // Remove old overlay (scoped to the correct container)
2657
- const oldOverlay = overlayContainer.querySelector(`#remote-overlay-${id}`);
2658
- if (oldOverlay) oldOverlay.remove();
2659
-
2660
- const overlay = document.createElement('div');
2661
- overlay.id = `remote-overlay-${id}`;
2662
- overlay.style.position = 'absolute';
2663
- overlay.style.pointerEvents = 'none';
2664
-
2665
- if (!isCollapsed) {
2666
- const range = document.createRange();
2667
-
2668
- const startNodeInfo = getNodeAndOffsetFromIndex(editor, startIndex);
2669
- const endNodeInfo = getNodeAndOffsetFromIndex(editor, endIndex);
2670
-
2671
- range.setStart(startNodeInfo.node, startNodeInfo.offset);
2672
- range.setEnd(endNodeInfo.node, endNodeInfo.offset);
2673
-
2674
- highlightTextRange(editor, range, color, overlay);
2675
-
2676
- }
2677
-
2678
- // Create a caret div to indicate cursor position
2679
- if (typeof cursorIndex === 'number') {
2680
- if (editor.innerText.trim() === '') {
2681
- editor.innerHTML = '<div>\u200B</div>';
2682
- }
2683
- const cursorNodeInfo = getNodeAndOffsetFromIndex(editor, cursorIndex);
2684
- if (!cursorNodeInfo) return;
2685
-
2686
- const cursorRange = document.createRange();
2687
- cursorRange.setStart(cursorNodeInfo.node, cursorNodeInfo.offset);
2688
- cursorRange.collapse(true);
2689
-
2690
- const cursorRect = cursorRange.getBoundingClientRect();
2691
- const cursor = document.createElement('div');
2692
- cursor.style.position = 'absolute';
2693
- cursor.style.left = cursorRect.left + editor.scrollLeft - editor.getBoundingClientRect().left + 'px';
2694
- cursor.style.top = cursorRect.top + editor.scrollTop - editor.getBoundingClientRect().top + 'px';
2695
- cursor.style.width = '2px';
2696
- cursor.style.height = cursorRect.height + 'px';
2697
- cursor.style.backgroundColor = color;
2698
- cursor.style.borderRadius = '1px';
2699
-
2700
- const label = document.createElement('div')
2701
- label.style.fontSize = '12px'
2702
- label.style.color = 'white';
2703
- label.style.backgroundColor = color;
2704
- label.style.borderRadius = '4px 4px 0 0'
2705
- label.style.padding = '0 4px'
2706
- label.style.position = 'absolute';
2707
- label.style.whiteSpace = 'nowrap';
2708
- label.style.left = cursorRect.left + editor.scrollLeft - editor.getBoundingClientRect().left + 'px';
2709
- label.style.top = cursorRect.top + editor.scrollTop - editor.getBoundingClientRect().top - 6 + 'px';
2710
- label.innerText = name
2711
-
2712
- overlay.appendChild(label);
2713
- overlay.appendChild(cursor);
2714
- }
2715
- overlayContainer.appendChild(overlay);
2716
- }
2717
-
2718
- const updateCursorPosition = (core, editorRef) => {
2719
- const editor = core.elements.editor;
2720
- const selection = core.elements.iframeWindow.getSelection(); // Use iframe's window
2721
- if (!selection || !editor || selection.rangeCount === 0) return;
2722
-
2723
- const range = selection.getRangeAt(0);
2724
- if (!editor.contains(range.startContainer) || !editor.contains(range.endContainer)) return;
2725
-
2726
- // Helper to get character index from start of editor to a container+offset
2727
- const getCharIndex = (container, offset) => {
2728
- const tempRange = core.elements.iframeWindow.createRange();
2729
- tempRange.selectNodeContents(editor);
2730
- tempRange.setEnd(container, offset);
2731
- return tempRange.toString().length;
2732
- }
2733
-
2734
- const start = getCharIndex(range.startContainer, range.startOffset);
2735
- const end = getCharIndex(range.endContainer, range.endOffset);
2736
-
2737
- const isBackward = (() => {
2738
- const position = selection.anchorNode.compareDocumentPosition(selection.focusNode);
2739
- if (position === 0) {
2740
- return selection.anchorOffset > selection.focusOffset;
2741
- }
2742
- return position === Node.DOCUMENT_POSITION_PRECEDING;
2743
- })();
2744
-
2745
- const cursorIndex = isBackward ? start : end;
2746
-
2747
- core.cursor = {
2748
- startIndex: start,
2749
- endIndex: end,
2750
- cursorIndex,
2751
- isCollapsed: range.collapsed,
2752
- };
2753
- if (editorRef.onLocalCursorChange instanceof Function) editorRef.onLocalCursorChange(core.cursor)
2754
- }
2755
-
2756
- const updateCursorPositionDebounce = asyncDebounce(updateCursorPosition, 200)
2757
-
2758
- const syncRemoteChanges = (core) => {
2759
- if (!core.state.previousDOM) core.state.previousDOM = core.elements.editor.cloneNode(true);
2760
-
2761
- const currentDOM = core.elements.editor.cloneNode(true);
2762
- const diffs = dd.diff(core.state.previousDOM, currentDOM);
2763
-
2764
- if (diffs.length > 0) {
2765
- core.onLocalSyncChanges({ type: 'html-patch', diffs });
2766
- core.state.previousDOM = currentDOM;
2767
- }
2768
- }
2769
-
2770
- const syncRemoteChangesDebounce = debounce(syncRemoteChanges, 50)
2771
-
2772
- const applyRemoteChanges = (core, diffs) => {
2773
- const editor = core.elements.editor;
2774
- const iframeWindow = core.elements.iframeWindow;
2775
- if (!editor || !diffs?.diffs) return;
2776
-
2777
- const selection = iframeWindow.getSelection();
2778
- let saved = null;
2779
-
2780
- if (selection && selection.rangeCount > 0) {
2781
- const range = selection.getRangeAt(0);
2782
- if (editor.contains(range.startContainer) && editor.contains(range.endContainer)) {
2783
- // Save selection as indexes (pre-change)
2784
- saved = {
2785
- start: getCharIndex(editor, range.startContainer, range.startOffset),
2786
- end: getCharIndex(editor, range.endContainer, range.endOffset),
2787
- };
2788
- }
2789
- }
2790
-
2791
- requestAnimationFrame(() => {
2792
- dd.apply(editor, diffs.diffs); // Apply patch
2793
- core.state.previousDOM = editor.cloneNode(true); // Update snapshot
2794
-
2795
- if (saved) {
2796
- const newStart = getNodeAndOffsetFromIndex(editor, saved.start);
2797
- const newEnd = getNodeAndOffsetFromIndex(editor, saved.end);
2798
- if (newStart && newEnd) {
2799
- const newRange = iframeWindow.createRange();
2800
- newRange.setStart(newStart.node, newStart.offset);
2801
- newRange.setEnd(newEnd.node, newEnd.offset);
2802
- const sel = iframeWindow.getSelection();
2803
- if (sel) {
2804
- sel.removeAllRanges();
2805
- sel.addRange(newRange);
2806
- }
2807
- }
2808
- }
2809
- });
2810
- }
2811
-
2812
- const isLinkValid = (link) => {
2813
- const trimmed = link.trim();
2814
-
2815
- const isURL = REGEX.URL.test(trimmed);
2816
- const isMailto = REGEX.MAILTO.test(trimmed);
2817
- const isTel = REGEX.TEL.test(trimmed);
2818
-
2819
- return isURL || isMailto || isTel;
2820
- };
2821
-
2822
- const formatLink = (value) => {
2823
- const trimmed = value.trim();
2824
- if (REGEX.EMAIL.test(trimmed)) {
2825
- return 'mailto:' + trimmed;
2826
- }
2827
- if (REGEX.MOBILE.test(trimmed)) {
2828
- return 'tel:' + trimmed;
2829
- }
2830
- return trimmed;
2831
- };
2832
-
2833
- const buildTributeValues = (labels = []) => {
2834
- return labels.flatMap(category => ([
2835
- { key: category.name, isCategory: true },
2836
- ...category.fields.map(field => ({
2837
- key: field.name,
2838
- value: field.value,
2839
- }))
2840
- ]));
2841
- };
2842
-
2843
- function toPx(value, el = document.body) {
2844
- if (!value) return null;
2845
- if (typeof value === 'number') return value;
2846
-
2847
- const tmp = document.createElement('div');
2848
- tmp.style.width = value;
2849
- tmp.style.position = 'absolute';
2850
- tmp.style.visibility = 'hidden';
2851
- el.appendChild(tmp);
2852
- const px = tmp.getBoundingClientRect().width;
2853
- tmp.remove();
2854
- return px;
2855
- }
2856
-
2857
- const updateHeight = (core, options) => {
2858
- if (options.autoHeight) {
2859
- let h = core.elements.editor.scrollHeight;
2860
- const minH = toPx(options.minHeight, core.elements.iframeContainer.parentElement);
2861
- const maxH = toPx(options.maxHeight, core.elements.iframeContainer.parentElement);
2862
- if (minH != null) h = Math.max(h, minH);
2863
- if (maxH != null) h = Math.min(h, maxH);
2864
- core.elements.iframeContainer.style.height = h + 'px';
2865
- }
2866
- }
2867
-
2868
- const isInsideTableCell = (node) => {
2869
- const element =
2870
- node?.nodeType === Node.ELEMENT_NODE
2871
- ? node
2872
- : node?.parentElement;
2873
-
2874
- return element?.closest('td, th');
2875
- };
2876
-
2877
- const formatListItemContent = (li, tag) => {
2878
- const formattedBlock = document.createElement(tag);
2879
- const childNodes = Array.from(li.childNodes);
2880
-
2881
- childNodes.forEach(child => {
2882
- if (child.nodeType === Node.ELEMENT_NODE && ['UL', 'OL'].includes(child.tagName)) return;
2883
- formattedBlock.appendChild(child);
2884
- });
2885
-
2886
- if (!formattedBlock.childNodes.length) formattedBlock.innerHTML = '<br>';
2887
-
2888
- const firstNestedList = Array.from(li.children).find(child => ['UL', 'OL'].includes(child.tagName));
2889
- li.insertBefore(formattedBlock, firstNestedList || null);
2890
-
2891
- return formattedBlock;
2892
- }
2893
-
2894
- const applyFormatBlockInListItem = (core, tag) => {
2895
- const selection = core.elements.iframeWindow.getSelection();
2896
- const node = selection?.rangeCount ? selection.getRangeAt(0).startContainer : null;
2897
- const element = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
2898
- const li = element?.closest('li');
2899
-
2900
- if (!li || !core.elements.editor.contains(li)) return false;
2901
-
2902
- const currentBlock = element.closest(BLOCK_FORMAT_TAGS.join(','));
2903
- const isCurrentBlockInLi = currentBlock && currentBlock.closest('li') === li;
2904
- let formattedBlock;
2905
-
2906
- const placeCaretAtEnd = (core, element) => {
2907
- const range = document.createRange();
2908
- const selection = core.elements.iframeWindow.getSelection();
2909
-
2910
- range.selectNodeContents(element);
2911
- range.collapse(false);
2912
- selection.removeAllRanges();
2913
- selection.addRange(range);
2914
- }
2915
-
2916
- if (tag === 'p') {
2917
- if (isCurrentBlockInLi) {
2918
- const fragment = document.createDocumentFragment();
2919
-
2920
- while (currentBlock.firstChild) fragment.appendChild(currentBlock.firstChild);
2921
-
2922
- li.insertBefore(fragment, currentBlock);
2923
- currentBlock.remove();
2924
- }
2925
- placeCaretAtEnd(core, li);
2926
- return true;
2927
- }
2928
-
2929
- if (isCurrentBlockInLi) {
2930
- formattedBlock = document.createElement(tag);
2931
- formattedBlock.innerHTML = currentBlock.innerHTML || '<br>';
2932
- currentBlock.replaceWith(formattedBlock);
2933
- } else {
2934
- formattedBlock = formatListItemContent(li, tag);
2935
- }
2936
-
2937
- placeCaretAtEnd(core, formattedBlock);
2938
- return true;
2939
- }
2940
-
2941
- const normalizeHeadingWrappedLists = (core) => {
2942
- core.elements.editor.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
2943
- const lists = Array.from(heading.children).filter(child => ['UL', 'OL'].includes(child.tagName));
2944
- if (!lists.length) return;
2945
-
2946
- const headingTag = heading.tagName.toLowerCase();
2947
- const fragment = document.createDocumentFragment();
2948
-
2949
- lists.forEach(list => {
2950
- list.querySelectorAll('li').forEach(li => {
2951
- const hasBlock = Array.from(li.children).some(child => BLOCK_FORMAT_TAGS.includes(child.tagName));
2952
- if (!hasBlock) formatListItemContent(li, headingTag);
2953
- });
2954
- fragment.appendChild(list);
2955
- });
2956
-
2957
- heading.parentNode.insertBefore(fragment, heading);
2958
-
2959
- if (heading.textContent.replace(/\u200B/g, '').trim() || heading.querySelector('img, br')) {
2960
- const paragraph = document.createElement('p');
2961
- paragraph.innerHTML = heading.innerHTML;
2962
- heading.replaceWith(paragraph);
2963
- } else {
2964
- heading.remove();
2965
- }
2966
- });
2967
- }
2968
-
2969
- const makeUnorderedList = (event, core) => {
2970
- if (event.key !== ' ') return;
2971
-
2972
- core.updateCaretPosition();
2973
- const range = core.state.range;
2974
- if (!range) return;
2975
-
2976
- const textNode = range.startContainer;
2977
- if (textNode.nodeType !== Node.TEXT_NODE) return;
2978
-
2979
- let text = textNode.nodeValue.replace(/\u00A0/g, ' ');
2980
- if (textNode.parentNode.closest('li')) return;
2981
-
2982
- const cell = isInsideTableCell(textNode);
2983
-
2984
- if ((text + event.key) === '* ' || (text + event.key) === '- ') {
2985
- event.preventDefault();
2986
-
2987
- textNode.nodeValue = text.slice(0, -1);
2988
-
2989
- if (cell) {
2990
- const ul = document.createElement('ul');
2991
- const li = document.createElement('li');
2992
- const text = document.createTextNode('\u00A0');
2993
-
2994
- li.appendChild(text);
2995
- ul.appendChild(li);
2996
-
2997
- cell.innerHTML = '';
2998
- cell.appendChild(ul);
2999
-
3000
- const newRange = document.createRange();
3001
- newRange.setStart(text, 1);
3002
- newRange.collapse(true);
3003
-
3004
- core.state.selection.removeAllRanges();
3005
- core.state.selection.addRange(newRange);
3006
-
3007
- } else {
3008
- const newRange = document.createRange();
3009
- newRange.setStart(textNode, textNode.nodeValue.length);
3010
- newRange.setEnd(textNode, textNode.nodeValue.length);
3011
- core.state.selection.removeAllRanges();
3012
- core.state.selection.addRange(newRange);
3013
- applyUnorderedList(core)
3014
- }
3015
- }
3016
- }
3017
-
3018
-
3019
- const makeOrderedList = (event, core) => {
3020
- const textNode = core.state.range.startContainer;
3021
- if (textNode.nodeType === Node.TEXT_NODE) {
3022
- // Get the text typed so far
3023
- let text = textNode.nodeValue.replace(/\u00A0/g, ' ');
3024
- const haveLiNode = textNode.parentNode.closest('li')
3025
-
3026
- // Detect "1. " pattern
3027
- if (!haveLiNode && text === "1. ") {
3028
-
3029
- const cell = isInsideTableCell(textNode);
3030
- text = text.slice(0, -3);
3031
- textNode.nodeValue = text;
3032
- if (cell) {
3033
- const ol = document.createElement('ol');
3034
- const li = document.createElement('li');
3035
- const text = document.createTextNode('\u00A0');
3036
-
3037
- li.appendChild(text);
3038
- ol.appendChild(li);
3039
-
3040
- cell.innerHTML = '';
3041
- cell.appendChild(ol);
3042
-
3043
- const newRange = document.createRange();
3044
- newRange.setStart(text, 1);
3045
- newRange.collapse(true);
3046
-
3047
- core.state.selection.removeAllRanges();
3048
- core.state.selection.addRange(newRange);
3049
-
3050
- } else {
3051
- // Update the caret position
3052
- const newRange = document.createRange();
3053
- newRange.setStart(textNode, text.length);
3054
- newRange.setEnd(textNode, text.length);
3055
- core.state.selection.removeAllRanges();
3056
- core.state.selection.addRange(newRange);
3057
- applyOrderList(core)
3058
- }
3059
- }
3060
- }
3061
-
3062
- }
3063
-
3064
- const makeHeading = (event, core) => {
3065
- if (event.key !== ' ') return;
3066
- core.updateCaretPosition();
3067
- const range = core.state.range;
3068
- const textNode = range.startContainer;
3069
- if (textNode.nodeType !== Node.TEXT_NODE) return;
3070
-
3071
- let text = textNode.nodeValue.replace(/\u00A0/g, ' ');
3072
- const fullText = text + event.key;
3073
-
3074
- const matches = ['# ', '## ', '### ', '#### ', '##### ', '###### '];
3075
- if (!matches.includes(fullText)) return;
3076
-
3077
- event.preventDefault();
3078
-
3079
- const level = fullText.trim().length;
3080
- const cell = isInsideTableCell(textNode);
3081
-
3082
- textNode.nodeValue = text.slice(0, -level);
3083
-
3084
- if (cell) {
3085
-
3086
- const h = document.createElement(`h${level}`);
3087
- h.innerHTML = '&nbsp;';
3088
-
3089
- cell.innerHTML = '';
3090
- cell.appendChild(h);
3091
-
3092
- const newRange = document.createRange();
3093
- newRange.setStart(h, 0);
3094
- newRange.collapse(true);
3095
- core.state.selection.removeAllRanges();
3096
- core.state.selection.addRange(newRange);
3097
-
3098
- } else {
3099
- core.elements.iframeWindow.execCommand(
3100
- 'formatBlock',
3101
- false,
3102
- `h${level}`
3103
- );
3104
- }
3105
-
3106
- core.elements.editor.focus();
3107
- core.updateCaretPosition();
3108
- changeToolbarValueByName(core, 'format-block', `h${level}`);
3109
- };
3110
-
3111
-
3112
- const makeBlockQuote = (event, core) => {
3113
- if (event.key !== ' ') return;
3114
-
3115
- core.updateCaretPosition();
3116
- const range = core.state.range;
3117
- const textNode = range.startContainer;
3118
- if (textNode.nodeType !== Node.TEXT_NODE) return;
3119
-
3120
- let text = textNode.nodeValue.replace(/\u00A0/g, ' ');
3121
- if ((text + event.key) !== '> ') return;
3122
-
3123
- event.preventDefault();
3124
-
3125
- textNode.nodeValue = text.slice(0, -1);
3126
-
3127
- const cell = isInsideTableCell(textNode);
3128
-
3129
- if (cell) {
3130
-
3131
- const blockquote = document.createElement('blockquote');
3132
- blockquote.innerHTML = '&nbsp;';
3133
-
3134
- cell.innerHTML = '';
3135
- cell.appendChild(blockquote);
3136
- const newRange = document.createRange();
3137
- newRange.setStart(blockquote, 0);
3138
- newRange.collapse(true);
3139
- core.state.selection.removeAllRanges();
3140
- core.state.selection.addRange(newRange);
3141
- } else {
3142
- core.elements.iframeWindow.execCommand(
3143
- 'formatBlock',
3144
- false,
3145
- 'blockquote'
3146
- );
3147
- }
3148
-
3149
- core.elements.editor.focus();
3150
- core.updateCaretPosition();
3151
- };
3152
-
3153
-
3154
- const makeStrikethrough = (event, core) => {
3155
- if (event.key !== '~') return;
3156
- core.updateCaretPosition();
3157
- if (!core.state.range) return;
3158
-
3159
- const range = core.state.range;
3160
- const textNode = range.startContainer;
3161
- if (textNode.nodeType !== Node.TEXT_NODE) return;
3162
-
3163
- const text = textNode.nodeValue.replace(/\u00A0/g, ' ');
3164
- const offset = range.startOffset;
3165
- const textBeforeCaret = text.slice(0, offset) + event.key;
3166
- const match = textBeforeCaret.match(/~~([^~]+)~~$/);
3167
- if (!match) return;
3168
-
3169
- event.preventDefault();
3170
-
3171
- const content = match[1];
3172
- const startIndex = match.index;
3173
- const endIndex = startIndex + match[0].length - 1;
3174
-
3175
- // Select ~~text~~
3176
- const replaceRange = document.createRange();
3177
- replaceRange.setStart(textNode, startIndex);
3178
- replaceRange.setEnd(textNode, endIndex);
3179
-
3180
- replaceRange.deleteContents();
3181
-
3182
- const strike = document.createElement('strike');
3183
- strike.textContent = content;
3184
- replaceRange.insertNode(strike);
3185
-
3186
- const zeroWidthSpace = document.createTextNode('\u200B');
3187
- if (strike.nextSibling) {
3188
- strike.parentNode.insertBefore(zeroWidthSpace, strike.nextSibling);
3189
- } else {
3190
- strike.parentNode.appendChild(zeroWidthSpace);
3191
- }
3192
-
3193
- const newRange = document.createRange();
3194
- newRange.setStart(zeroWidthSpace, 1);
3195
- newRange.setEnd(zeroWidthSpace, 1);
3196
-
3197
- const selection = core.state.selection;
3198
- selection.removeAllRanges();
3199
- selection.addRange(newRange);
3200
-
3201
- core.updateCaretPosition();
3202
-
3203
- changeToolbarStateByName(core, 'inactive', ['strike']);
3204
- }
3205
-
3206
- const makeCodeBlock = (event, core) => {
3207
- if (event.key !== '`') return;
3208
- core.updateCaretPosition();
3209
- if (!core.state.range) return;
3210
-
3211
- const range = core.state.range;
3212
- const textNode = range.startContainer;
3213
- if (textNode.nodeType !== Node.TEXT_NODE) return;
3214
-
3215
- const text = textNode.nodeValue.replace(/\u00A0/g, ' ');
3216
- const offset = range.startOffset;
3217
- const textBeforeCaret = text.slice(0, offset) + event.key;
3218
-
3219
- const match = textBeforeCaret.match(/`([^`]+)`$/);
3220
- if (!match) return;
3221
-
3222
- event.preventDefault();
3223
-
3224
- const content = match[1];
3225
- const startIndex = match.index;
3226
- const endIndex = startIndex + match[0].length - 1;
3227
-
3228
- // Select ~~text~~
3229
- const replaceRange = document.createRange();
3230
- replaceRange.setStart(textNode, startIndex);
3231
- replaceRange.setEnd(textNode, endIndex);
3232
-
3233
- replaceRange.deleteContents();
3234
-
3235
- const code = document.createElement('code');
3236
- code.textContent = content;
3237
- replaceRange.insertNode(code);
3238
-
3239
- const zeroWidthSpace = document.createTextNode('\u200B');
3240
- if (code.nextSibling) {
3241
- code.parentNode.insertBefore(zeroWidthSpace, code.nextSibling);
3242
- } else {
3243
- code.parentNode.appendChild(zeroWidthSpace);
3244
- }
3245
-
3246
- const newRange = document.createRange();
3247
- newRange.setStart(zeroWidthSpace, 1);
3248
- newRange.setEnd(zeroWidthSpace, 1);
3249
-
3250
- const selection = core.state.selection;
3251
- selection.removeAllRanges();
3252
- selection.addRange(newRange);
3253
-
3254
- core.updateCaretPosition();
3255
- }
3256
-
3257
- const getListLevel = (li) => {
3258
- let level = 0;
3259
- let parent = li.parentElement;
3260
- const tagName = parent.tagName;
3261
-
3262
- while (parent && parent.tagName === tagName) {
3263
- level++;
3264
- parent = parent.parentElement?.closest(tagName);
3265
- }
3266
-
3267
- return level;
3268
- };
3269
-
3270
- const applyListStyle = (list, level) => {
3271
- let type;
3272
- if (list.tagName === 'OL') {
3273
- type = LIST_STYLES_BY_LEVEL[(level - 1) % LIST_STYLES_BY_LEVEL.length];
3274
- } else if (list.tagName === 'UL') {
3275
- type = UNORDERED_LIST_STYLES_BY_LEVEL[(level - 1) % UNORDERED_LIST_STYLES_BY_LEVEL.length];
3276
- }
3277
- if (type) list.setAttribute('type', type);
3278
- };
3279
-
3280
- const indentListItem = (li, core) => {
3281
- const prevLi = li.previousElementSibling;
3282
- if (!prevLi) return;
3283
-
3284
- const parentList = li.parentElement;
3285
- const parentTag = parentList.tagName; // UL or OL
3286
-
3287
- let subList = prevLi.querySelector(`:scope > ${parentTag}`);
3288
-
3289
- // create sublist if it doesn't exist
3290
- if (!subList) {
3291
- subList = document.createElement(parentTag);
3292
- prevLi.appendChild(subList);
3293
- }
3294
-
3295
- // move the li into the sublist
3296
- subList.appendChild(li);
3297
- const selection = core.elements.iframeWindow.getSelection();
3298
- const range = document.createRange();
3299
- range.selectNodeContents(li);
3300
- range.collapse(true);
3301
- selection.removeAllRanges();
3302
- selection.addRange(range);
3303
- core.elements.editor.focus();
3304
- core.updateCaretPosition();
3305
- if (parentTag === 'OL' || parentTag === 'UL') {
3306
- const level = getListLevel(li);
3307
- applyListStyle(subList, level);
3308
- }
3309
- };
3310
-
3311
- const outdentListItem = (li, core) => {
3312
- const parentOl = li.parentElement;
3313
- const parentLi = parentOl.closest('li');
3314
- if (!parentLi) return;
3315
- parentLi.after(li);
3316
-
3317
- const selection = core.elements.iframeWindow.getSelection();
3318
- const range = document.createRange();
3319
- range.selectNodeContents(li);
3320
- range.collapse(true);
3321
- selection.removeAllRanges();
3322
- selection.addRange(range);
3323
- core.elements.editor.focus();
3324
- core.updateCaretPosition();
3325
-
3326
-
3327
- if (parentOl.children.length === 0) {
3328
- parentOl.remove();
3329
- }
3330
-
3331
- if (li.parentElement.tagName === 'OL' || li.parentElement.tagName === 'UL') {
3332
- const level = getListLevel(li);
3333
- applyListStyle(li.parentElement, level);
3334
- }
3335
- };
3336
-
3337
- const isEmptyLi = (li) => {
3338
- const text = li.textContent.replace(/\u200B/g, '').trim();
3339
- const hasOnlySublist = li.children.length === 1 && li.querySelector('ul, ol');
3340
- const hasMeaningfulElement = li.querySelector('img, iframe, audio, video, table, hr, figure');
3341
- return text === '' && !hasOnlySublist && !hasMeaningfulElement;
3342
- }
3343
-
3344
- const trackListSelectionDeletion = (event, core) => {
3345
- if (event.defaultPrevented) {
3346
- delete core.state.pendingListSelectionDeletion;
3347
- return;
3348
- }
3349
-
3350
- if (!['Backspace', 'Delete'].includes(event.key)) {
3351
- delete core.state.pendingListSelectionDeletion;
3352
- return;
3353
- }
3354
-
3355
- const selection = core.elements.iframeWindow.getSelection();
3356
- if (!selection?.rangeCount) return;
3357
-
3358
- const range = selection.getRangeAt(0);
3359
- if (range.collapsed) {
3360
- delete core.state.pendingListSelectionDeletion;
3361
- return;
3362
- }
3363
-
3364
- const getElementFromNode = (node) => {
3365
- if (!node) return null;
3366
- return node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
3367
- }
3368
-
3369
- const startElement = getElementFromNode(range.startContainer);
3370
- const endElement = getElementFromNode(range.endContainer);
3371
- const startLi = startElement?.closest('li');
3372
- const endLi = endElement?.closest('li');
3373
-
3374
- if (!startLi || !endLi || startLi === endLi) {
3375
- delete core.state.pendingListSelectionDeletion;
3376
- return;
3377
- }
3378
-
3379
- const listItems = [];
3380
-
3381
- core.elements.editor.querySelectorAll('li').forEach(li => {
3382
- if (range.intersectsNode(li)) listItems.push(li);
3383
- });
3384
-
3385
- if (listItems.length < 2) {
3386
- delete core.state.pendingListSelectionDeletion;
3387
- return;
3388
- }
3389
-
3390
- core.state.pendingListSelectionDeletion = {
3391
- listItems,
3392
- lists: Array.from(new Set(listItems.map(li => li.parentElement).filter(Boolean))),
3393
- };
3394
- }
3395
-
3396
- const cleanupListSelectionDeletion = (core) => {
3397
- const pendingDeletion = core.state.pendingListSelectionDeletion;
3398
- if (!pendingDeletion) return false;
3399
-
3400
- delete core.state.pendingListSelectionDeletion;
3401
-
3402
- let didChange = false;
3403
- const lists = new Set(pendingDeletion.lists);
3404
-
3405
- pendingDeletion.listItems.forEach(li => {
3406
- if (!li.isConnected || !core.elements.editor.contains(li)) return;
3407
- if (!isEmptyLi(li)) return;
3408
-
3409
- const parentList = li.parentElement;
3410
- if (parentList) lists.add(parentList);
3411
- li.remove();
3412
- didChange = true;
3413
- });
3414
-
3415
- Array.from(lists).reverse().forEach(list => {
3416
- if (!list?.isConnected || !core.elements.editor.contains(list)) return;
3417
- if (list.querySelector('li')) return;
3418
-
3419
- list.remove();
3420
- didChange = true;
3421
- });
3422
-
3423
- if (didChange) {
3424
- if (!core.elements.editor.textContent.replace(/\u200B/g, '').trim() && !core.elements.editor.querySelector('img, iframe, audio, video, table, hr, figure')) {
3425
- core.elements.editor.innerHTML = '<div><br></div>';
3426
- }
3427
-
3428
- const selection = core.elements.iframeWindow.getSelection();
3429
- if (!selection?.rangeCount || !core.elements.editor.contains(selection.anchorNode)) {
3430
- const range = document.createRange();
3431
- const selection = core.elements.iframeWindow.getSelection();
3432
-
3433
- range.selectNodeContents(core.elements.editor);
3434
- range.collapse(false);
3435
- selection.removeAllRanges();
3436
- selection.addRange(range);
3437
- }
3438
-
3439
- core.updateCaretPosition();
3440
- }
3441
-
3442
- return didChange;
3443
- }
3444
-
3445
- const makeSublist = (event, core) => {
3446
- if (event.key !== 'Tab') return;
3447
-
3448
- const selection = core.elements.iframeWindow.getSelection();
3449
- if (!selection.rangeCount) return;
3450
-
3451
- const range = selection.getRangeAt(0);
3452
- const isAtStart = range.startOffset === 0;
3453
-
3454
- let container = range.commonAncestorContainer;
3455
-
3456
- if (container.nodeType === Node.TEXT_NODE) {
3457
- container = container.parentElement;
3458
- }
3459
- // find the nearest list container
3460
- const list = container.closest('ul, ol');
3461
- if (!list) return;
3462
- const allLis = list.querySelectorAll('li');
3463
-
3464
- const selectedLis = [];
3465
-
3466
- allLis.forEach(li => {
3467
- if (range.intersectsNode(li)) {
3468
- selectedLis.push(li);
3469
- }
3470
- });
3471
-
3472
- if (!selectedLis.length) return;
3473
-
3474
- if (!event.shiftKey) {
3475
- const hasContent = selectedLis.some(li => !isEmptyLi(li));
3476
- if (hasContent && !isAtStart) return;
3477
- }
3478
-
3479
- event.preventDefault();
3480
-
3481
- selectedLis.forEach(li => {
3482
- if (event.shiftKey) {
3483
- outdentListItem(li, core);
3484
- } else {
3485
- indentListItem(li, core);
3486
- }
3487
- });
3488
- };
3489
-
3490
- const transformCase = (text, type) => {
3491
- if (type === 'upper_case') return text.toUpperCase();
3492
- if (type === 'lower_case') return text.toLowerCase();
3493
- if (type === 'title_case') return text.toLowerCase().replace(/\b\w/g, s => s.toUpperCase());
3494
- return text;
3495
- };
3496
-
3497
- const transformTextStyle = (core, type) => {
3498
- const range = core.state.range;
3499
- if (!range) return;
3500
- const startContainer = range.startContainer;
3501
- const startOffset = range.startOffset;
3502
- const endContainer = range.endContainer;
3503
- let endOffset = range.endOffset;
3504
-
3505
- const processTextNode = (node, start, end) => {
3506
- const text = node.nodeValue;
3507
- const transformedText = transformCase(text.slice(start, end), type);
3508
- node.nodeValue = text.slice(0, start) + transformedText + text.slice(end);
3509
- if (node === endContainer) endOffset = start + transformedText.length;
3510
- };
3511
-
3512
- if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE) {
3513
- processTextNode(range.commonAncestorContainer, range.startOffset, range.endOffset);
3514
- } else {
3515
- const iterator = core.elements.iframeWindow.createNodeIterator(
3516
- range.commonAncestorContainer,
3517
- NodeFilter.SHOW_TEXT,
3518
- {
3519
- acceptNode: (node) => {
3520
- return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
3521
- }
3522
- }
3523
- );
3524
-
3525
- const nodes = [];
3526
- let node;
3527
- while ((node = iterator.nextNode())) {
3528
- nodes.push(node);
3529
- }
3530
-
3531
- nodes.forEach(node => {
3532
- let start = 0;
3533
- let end = node.nodeValue.length;
3534
-
3535
- if (node === range.startContainer) start = range.startOffset;
3536
- if (node === range.endContainer) end = range.endOffset;
3537
-
3538
- processTextNode(node, start, end);
3539
- });
3540
- }
3541
-
3542
- const newRange = core.elements.iframeWindow.createRange();
3543
- newRange.setStart(startContainer, startOffset);
3544
- newRange.setEnd(endContainer, endOffset);
3545
- const selection = core.elements.iframeWindow.getSelection();
3546
- selection.removeAllRanges();
3547
- selection.addRange(newRange);
3548
- }
3549
-
3550
- const assignIdToHeading = (heading) => {
3551
- if (!heading.dataset.leksyHeadingId) heading.dataset.leksyHeadingId = uuidv4()
3552
- return heading.dataset.leksyHeadingId
3553
- };
3554
-
3555
- const navigateToHeading = (core, headingId) => {
3556
- if (!headingId) return;
3557
- const heading = core.elements.iframeWindow.querySelector(`[data-leksy-heading-id="${headingId}"]`);
3558
- if (!heading) return;
3559
- heading.scrollIntoView({
3560
- behavior: 'smooth',
3561
- block: 'center'
3562
- });
3563
- const range = document.createRange();
3564
- range.selectNodeContents(heading);
3565
- range.collapse(true);
3566
-
3567
- const sel = core.elements.iframeWindow.getSelection();
3568
- sel.removeAllRanges();
3569
- sel.addRange(range);
3570
-
3571
- core.elements.editor.focus();
3572
- core.updateCaretPosition();
3573
- }
3574
-
3575
- const insertTOC = (core) => {
3576
- const editor = core.elements.editor;
3577
- const headings = editor.querySelectorAll('h1, h2, h3, h4, h5, h6');
3578
-
3579
- if (!headings.length) return;
3580
-
3581
- const tocContainer = document.createElement('div');
3582
- tocContainer.contentEditable = 'false';
3583
- tocContainer.dataset.leksyElement = 'toc'
3584
-
3585
- tocContainer.style.border = '1px solid gray';
3586
- tocContainer.style.display = 'inline-block';
3587
- tocContainer.style.width = '100%';
3588
- tocContainer.style.boxSizing = 'border-box';
3589
- tocContainer.style.borderRadius = '4px';
3590
-
3591
- const title = document.createElement('div');
3592
- title.innerText = 'Table of Contents';
3593
- title.style.fontWeight = 'bold';
3594
- title.style.marginBottom = '8px';
3595
- title.style.borderBottom = '1px solid gray';
3596
- title.style.padding = '8px 12px';
3597
-
3598
- tocContainer.appendChild(title);
3599
-
3600
- const ul = document.createElement('ul');
3601
- ul.style.listStyle = 'none';
3602
- ul.style.padding = '8px 12px';
3603
- ul.style.margin = '0';
3604
-
3605
- headings.forEach((heading) => {
3606
- const headingId = assignIdToHeading(heading)
3607
- const level = Number(heading.tagName[1]);
3608
-
3609
- const li = document.createElement('li');
3610
- li.style.marginBottom = '4px';
3611
- li.style.marginLeft = `${(level - 1) * 15}px`;
3612
- li.style.color = 'var(--link-color)';
3613
- li.style.cursor = 'pointer';
3614
- li.textContent = heading.textContent;
3615
- li.dataset.leksyTocHeadingId = headingId
3616
-
3617
- ul.appendChild(li);
3618
- });
3619
-
3620
- tocContainer.appendChild(ul);
3621
-
3622
- const selection = core.elements.iframeWindow.getSelection();
3623
-
3624
- if (!selection.rangeCount) return;
3625
-
3626
- const range = selection.getRangeAt(0);
3627
-
3628
- if (!range.collapsed) {
3629
- // Replace selected content with TOC
3630
- range.deleteContents();
3631
- range.insertNode(tocContainer);
3632
-
3633
- // Remove empty list items
3634
- editor.querySelectorAll('li').forEach(li => {
3635
- const text = li.textContent.replace(/\u00A0/g, '').trim();
3636
-
3637
- if (
3638
- !text &&
3639
- !li.querySelector(
3640
- 'img, video, table, ul, ol, iframe, blockquote'
3641
- )
3642
- ) {
3643
- li.remove();
3644
- }
3645
- });
3646
-
3647
- // Place cursor after TOC
3648
- const p = document.createElement('p');
3649
- p.innerHTML = '<br>';
3650
- tocContainer.after(p);
3651
-
3652
- const newRange = document.createRange();
3653
- newRange.setStart(p, 0);
3654
- newRange.collapse(true);
3655
-
3656
- selection.removeAllRanges();
3657
- selection.addRange(newRange);
3658
- } else {
3659
- // No selection -> insert after current block
3660
- let node = range.startContainer;
3661
-
3662
- if (node.nodeType === Node.TEXT_NODE) {
3663
- node = node.parentElement;
3664
- }
3665
-
3666
- const block = node.closest(
3667
- 'p, div, h1, h2, h3, h4, h5, h6, blockquote, pre, li'
3668
- );
3669
-
3670
- if (block) {
3671
- block.insertAdjacentElement('afterend', tocContainer);
3672
- } else {
3673
- editor.appendChild(tocContainer);
3674
- }
3675
-
3676
- // Place cursor after TOC
3677
- const p = document.createElement('p');
3678
- p.innerHTML = '<br>';
3679
- tocContainer.after(p);
3680
-
3681
- const newRange = document.createRange();
3682
- newRange.setStart(p, 0);
3683
- newRange.collapse(true);
3684
-
3685
- selection.removeAllRanges();
3686
- selection.addRange(newRange);
3687
- }
3688
-
3689
- core.updateCaretPosition();
3690
- };
3691
-
3692
- function insertOutlinesItems(core, options, tab, outlineContainer) {
3693
- if (!options.showTabs) return
3694
- if (!tab.showOutlines) return
3695
- if (tab.tabId !== core.state.currentTabId) return
3696
-
3697
- outlineContainer.innerHTML = '';
3698
- const editor = core.elements.editor;
3699
- const headings = editor.querySelectorAll('h1, h2, h3, h4, h5, h6');
3700
-
3701
- headings.forEach((heading) => {
3702
- if (!heading.textContent?.trim()) return
3703
- const level = Number(heading.tagName[1]);
3704
-
3705
- const item = document.createElement('div');
3706
- item.className = 'outline-item';
3707
- item.textContent = heading.textContent;
3708
- item.style.paddingLeft = `${(level - 1) * 12 + 8}px`;
3709
-
3710
- item.onclick = () => {
3711
- outlineContainer.querySelectorAll('.outline-item').forEach(el => {
3712
- el.classList.remove('active');
3713
- });
3714
- item.classList.add('active');
3715
-
3716
- heading.scrollIntoView({ behavior: 'smooth', block: 'center' });
3717
-
3718
- const range = document.createRange();
3719
- range.selectNodeContents(heading);
3720
- range.collapse(true);
3721
-
3722
- const sel = core.elements.iframeWindow.getSelection();
3723
- sel.removeAllRanges();
3724
- sel.addRange(range);
3725
-
3726
- core.elements.editor.focus();
3727
- core.updateCaretPosition();
3728
- };
3729
-
3730
- outlineContainer.appendChild(item);
3731
- });
3732
- }
3733
-
3734
- function highlightActiveOutline(core, options, outlineContainer) {
3735
- if (!options.showTabs) return
3736
- if (!outlineContainer) {
3737
- outlineContainer = core.elements.tabs[core.state.currentTabId].querySelector('.outline-items')
3738
- if (!outlineContainer) return;
3739
- }
3740
- const range = core.state.range;
3741
- const node = range?.startContainer;
3742
- if (!node) return;
3743
-
3744
- const items = outlineContainer.querySelectorAll('.outline-item');
3745
- const editor = core.elements.editor;
3746
- const headings = editor.querySelectorAll('h1, h2, h3, h4, h5, h6');
3747
-
3748
- items.forEach(el => el.classList.remove('active'));
3749
-
3750
- headings.forEach((heading, index) => {
3751
- if (heading.contains(node)) {
3752
- items[index]?.classList.add('active');
3753
- }
3754
- });
3755
- }
3756
-
3757
- const updateOutlineItems = debounce((core, options) => {
3758
- if (!options.showTabs) return
3759
- const tab = findTab(core.state.tabs, core.state.currentTabId)
3760
- if (!tab) return;
3761
- const outlineContainer = core.elements.tabs[tab.tabId].querySelector('.outline-items')
3762
- if (!outlineContainer) return;
3763
- insertOutlinesItems(core, options, tab, outlineContainer)
3764
- highlightActiveOutline(core, options, outlineContainer);
3765
- }, 300)
3766
-
3767
- const findAndReplace = (core, options, event) => {
3768
- if (!options.enableFindAndReplace) return;
3769
- if (core.state.findAndReplaceModalOpen) return;
3770
-
3771
- core.state.findAndReplaceModalOpen = true
3772
- const editor = core.elements.editor;
3773
- const iframeWindow = core.elements.iframeWindow;
3774
-
3775
- const headerContainer = document.createElement('div');
3776
- headerContainer.style.display = 'flex';
3777
- headerContainer.style.gap = '15px';
3778
- headerContainer.style.alignItems = 'center';
3779
-
3780
- const findTab = document.createElement('button');
3781
- findTab.type = 'button';
3782
- findTab.innerText = 'Find';
3783
- findTab.className = 'leksy-editor-find-and-replace-modal-tab-btn';
3784
-
3785
- const replaceTab = document.createElement('button');
3786
- replaceTab.type = 'button';
3787
- replaceTab.innerText = 'Replace';
3788
- replaceTab.className = 'leksy-editor-find-and-replace-modal-tab-btn';
3789
-
3790
- headerContainer.append(findTab, replaceTab);
3791
-
3792
- const body = document.createElement('div');
3793
-
3794
- const findLabel = document.createElement("label");
3795
- findLabel.innerText = "Find";
3796
- findLabel.style.paddingLeft = "4px";
3797
-
3798
- const findInputWrapper = document.createElement('div');
3799
- findInputWrapper.style.position = 'relative';
3800
- findInputWrapper.style.marginBottom = '10px';
3801
-
3802
- const findInput = document.createElement('input');
3803
- findInput.type = 'text';
3804
- findInput.placeholder = 'Search text';
3805
- findInput.style.width = '100%';
3806
- findInput.style.boxSizing = 'border-box';
3807
- findInput.style.paddingRight = '60px';
3808
-
3809
- const countSpan = document.createElement('span');
3810
- countSpan.className = 'leksy-editor-find-and-replace-modal-count';
3811
-
3812
- findInputWrapper.append(findInput, countSpan);
3813
-
3814
- const replaceLabel = document.createElement("label");
3815
- replaceLabel.innerText = "Replace with";
3816
- replaceLabel.style.display = "block";
3817
- replaceLabel.style.paddingLeft = "4px";
3818
- const replaceInput = document.createElement('input');
3819
- replaceInput.type = 'text';
3820
- replaceInput.placeholder = 'Replace text';
3821
- replaceInput.style.marginBottom = '10px';
3822
-
3823
- const optionsContainer = document.createElement('div');
3824
- optionsContainer.style.marginBottom = '10px';
3825
- optionsContainer.style.display = 'flex';
3826
- optionsContainer.style.flexDirection = 'column'
3827
-
3828
- const matchCaseCheckboxContainer = document.createElement('div');
3829
- const matchCaseCheckbox = document.createElement('input');
3830
- matchCaseCheckbox.type = 'checkbox';
3831
- matchCaseCheckbox.id = 'match-case-opt';
3832
- matchCaseCheckbox.style.marginRight = '5px';
3833
- matchCaseCheckbox.style.width = '24px';
3834
-
3835
- const matchCaseLabel = document.createElement('label');
3836
- matchCaseLabel.innerText = "Match case";
3837
- matchCaseLabel.htmlFor = 'match-case-opt';
3838
- matchCaseLabel.style.userSelect = 'none';
3839
- matchCaseCheckboxContainer.append(matchCaseCheckbox, matchCaseLabel)
3840
-
3841
- const wholeWordCheckboxContainer = document.createElement('div');
3842
- const wholeWordCheckbox = document.createElement('input');
3843
- wholeWordCheckbox.type = 'checkbox';
3844
- wholeWordCheckbox.id = 'whole-word-opt';
3845
- wholeWordCheckbox.style.marginRight = '5px';
3846
- wholeWordCheckbox.style.width = '24px';
3847
-
3848
- const wholeWordLabel = document.createElement('label');
3849
- wholeWordLabel.innerText = "Whole word";
3850
- wholeWordLabel.htmlFor = 'whole-word-opt';
3851
- wholeWordLabel.style.userSelect = 'none';
3852
- wholeWordCheckboxContainer.append(wholeWordCheckbox, wholeWordLabel);
3853
-
3854
- const regexCheckboxContainer = document.createElement('div');
3855
- const useRegexCheckbox = document.createElement('input');
3856
- useRegexCheckbox.type = 'checkbox';
3857
- useRegexCheckbox.id = 'use-regex-opt';
3858
- useRegexCheckbox.style.marginRight = '5px';
3859
- useRegexCheckbox.style.width = '24px';
3860
-
3861
- const useRegexLabel = document.createElement('label');
3862
- useRegexLabel.innerText = "Use regular expressions";
3863
- useRegexLabel.htmlFor = 'use-regex-opt';
3864
- useRegexLabel.style.userSelect = 'none';
3865
- regexCheckboxContainer.append(useRegexCheckbox, useRegexLabel)
3866
-
3867
- optionsContainer.append(matchCaseCheckboxContainer, regexCheckboxContainer, wholeWordCheckboxContainer);
3868
-
3869
- const span = document.createElement('span');
3870
- span.className = 'warning';
3871
-
3872
- body.append(findLabel, findInputWrapper, replaceLabel, replaceInput, optionsContainer, span);
3873
-
3874
- const footer = document.createElement('div');
3875
- footer.style.display = 'flex';
3876
- footer.style.gap = '8px';
3877
- footer.style.justifyContent = 'flex-end';
3878
- footer.style.width = '100%';
3879
-
3880
- const prevBtn = document.createElement('button');
3881
- prevBtn.type = 'button';
3882
- prevBtn.className = 'submit';
3883
- prevBtn.innerText = 'Previous';
3884
-
3885
- const nextBtn = document.createElement('button');
3886
- nextBtn.type = 'button';
3887
- nextBtn.className = 'submit';
3888
- nextBtn.innerText = 'Next';
3889
-
3890
- const replaceBtn = document.createElement('button');
3891
- replaceBtn.type = 'button';
3892
- replaceBtn.className = 'submit';
3893
- replaceBtn.innerText = 'Replace';
3894
-
3895
- const replaceAllBtn = document.createElement('button');
3896
- replaceAllBtn.type = 'button';
3897
- replaceAllBtn.className = 'submit';
3898
- replaceAllBtn.innerText = 'Replace All';
3899
-
3900
- footer.append(prevBtn, nextBtn, replaceBtn, replaceAllBtn);
3901
-
3902
- const toggleTab = (mode) => {
3903
- if (mode === 'find') {
3904
- replaceLabel.style.display = 'none';
3905
- replaceInput.style.display = 'none';
3906
- replaceBtn.style.display = 'none';
3907
- replaceAllBtn.style.display = 'none';
3908
-
3909
- findTab.classList.add('active');
3910
- replaceTab.classList.remove('active');
3911
- } else {
3912
- replaceLabel.style.display = 'block';
3913
- replaceInput.style.display = 'block';
3914
- replaceBtn.style.display = 'flex';
3915
- replaceAllBtn.style.display = 'flex';
3916
-
3917
- replaceTab.classList.add('active');
3918
- findTab.classList.remove('active');
3919
- }
3920
- }
3921
-
3922
- findTab.onclick = () => toggleTab('find');
3923
- replaceTab.onclick = () => toggleTab('replace');
3924
-
3925
- openDraggableModal({
3926
- title: headerContainer,
3927
- bodyNode: body,
3928
- footerNode: footer,
3929
- onClose: () => {
3930
- core.state.findAndReplaceModalOpen = false
3931
- }
3932
- }, core, options);
3933
-
3934
- if (event.key === 'f')
3935
- toggleTab('find');
3936
- else
3937
- toggleTab('replace');
3938
-
3939
-
3940
- findInput.focus();
3941
-
3942
- const find = (direction) => {
3943
- const searchText = findInput.value;
3944
- const matchCase = matchCaseCheckbox.checked;
3945
- const useRegex = useRegexCheckbox.checked;
3946
- const wholeWord = wholeWordCheckbox.checked;
3947
-
3948
- span.innerText = "";
3949
- countSpan.innerText = "";
3950
-
3951
- if (!searchText) {
3952
- span.innerText = "Please enter text to find";
3953
- return;
3954
- }
3955
-
3956
- let searchPattern = searchText;
3957
-
3958
- if (!useRegex) {
3959
- const cleaned = wholeWord ? searchText.trim() : searchText;
3960
- const escaped = cleaned.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3961
-
3962
- searchPattern = wholeWord
3963
- ? `(?<!\\w)${escaped}(?!\\w)`
3964
- : escaped;
3965
- }
3966
-
3967
- const selection = iframeWindow.getSelection();
3968
-
3969
- let regex;
3970
- try {
3971
- const flags = matchCase ? 'g' : 'gi';
3972
- regex = new RegExp(searchPattern, flags);
3973
- } catch (error) {
3974
- span.innerText = "Invalid regular expression";
3975
- return;
3976
- }
3977
-
3978
- // ✅ Collect matches (node-based + global index)
3979
- const walker = core.elements.iframeWindow.createTreeWalker(
3980
- editor,
3981
- NodeFilter.SHOW_TEXT,
3982
- null
3983
- );
3984
-
3985
- const matches = [];
3986
- let node;
3987
- let globalIndex = 0;
3988
-
3989
- while ((node = walker.nextNode())) {
3990
- const text = node.nodeValue;
3991
- regex.lastIndex = 0;
3992
-
3993
- let match;
3994
- while ((match = regex.exec(text)) !== null) {
3995
- matches.push({
3996
- node,
3997
- start: match.index,
3998
- end: match.index + match[0].length,
3999
- globalStart: globalIndex + match.index
4000
- });
4001
- }
4002
-
4003
- globalIndex += text.length;
4004
- }
4005
-
4006
- if (!matches.length) {
4007
- countSpan.innerText = "0 of 0";
4008
- return;
4009
- }
4010
-
4011
- // ✅ Get current cursor global position
4012
- let currentGlobal = 0;
4013
-
4014
- if (selection.rangeCount > 0) {
4015
- const range = selection.getRangeAt(0);
4016
-
4017
- const walker2 = core.elements.iframeWindow.createTreeWalker(
4018
- editor,
4019
- NodeFilter.SHOW_TEXT,
4020
- null
4021
- );
4022
-
4023
- let n, count = 0;
4024
-
4025
- while ((n = walker2.nextNode())) {
4026
- if (n === range.startContainer) {
4027
- currentGlobal = count + range.startOffset;
4028
- break;
4029
- }
4030
- count += n.nodeValue.length;
4031
- }
4032
- }
4033
-
4034
- // ✅ Find next / prev
4035
- let matchIndex = -1;
4036
-
4037
- if (direction === 'next') {
4038
- matchIndex = matches.findIndex(m => m.globalStart > currentGlobal);
4039
- if (matchIndex === -1) matchIndex = 0;
4040
- } else {
4041
- for (let i = matches.length - 1; i >= 0; i--) {
4042
- if (matches[i].globalStart < currentGlobal) {
4043
- matchIndex = i;
4044
- break;
4045
- }
4046
- }
4047
- if (matchIndex === -1) matchIndex = matches.length - 1;
4048
- }
4049
-
4050
- const match = matches[matchIndex];
4051
-
4052
- // ✅ Select
4053
- const newRange = core.elements.iframeWindow.createRange();
4054
- newRange.setStart(match.node, match.start);
4055
- newRange.setEnd(match.node, match.end);
4056
-
4057
- selection.removeAllRanges();
4058
- selection.addRange(newRange);
4059
-
4060
- match.node.parentElement?.scrollIntoView({
4061
- block: "center",
4062
- behavior: "smooth"
4063
- });
4064
-
4065
- countSpan.innerText = `${matchIndex + 1} of ${matches.length}`;
4066
- };
4067
-
4068
- const replace = () => {
4069
- const searchText = findInput.value;
4070
- const replaceText = replaceInput.value;
4071
- const matchCase = matchCaseCheckbox.checked;
4072
- const useRegex = useRegexCheckbox.checked;
4073
- const wholeWord = wholeWordCheckbox.checked;
4074
-
4075
- if (!searchText) return;
4076
-
4077
- // 🔹 Build regex (same as find)
4078
- let pattern = searchText;
4079
-
4080
- if (!useRegex) {
4081
- const cleaned = wholeWord ? searchText.trim() : searchText;
4082
- const escaped = cleaned.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4083
-
4084
- pattern = wholeWord
4085
- ? `(?<!\\w)${escaped}(?!\\w)`
4086
- : escaped;
4087
- }
4088
-
4089
- let regex;
4090
- try {
4091
- regex = new RegExp(pattern, matchCase ? '' : 'i');
4092
- } catch {
4093
- return;
4094
- }
4095
-
4096
- const selection = iframeWindow.getSelection();
4097
-
4098
- if (!selection.rangeCount) {
4099
- find('next');
4100
- return;
4101
- }
4102
-
4103
- const range = selection.getRangeAt(0);
4104
- const selectedText = range.toString();
4105
-
4106
- const match = selectedText.match(regex);
4107
-
4108
- if (match && match[0] === selectedText) {
4109
- const newText = selectedText.replace(regex, replaceText);
4110
-
4111
- // ✅ safe replace
4112
- range.deleteContents();
4113
- range.insertNode(core.elements.iframeWindow.createTextNode(newText));
4114
-
4115
- // move cursor after replaced text
4116
- range.setStart(range.endContainer, range.endOffset);
4117
- selection.removeAllRanges();
4118
- selection.addRange(range);
4119
- }
4120
-
4121
- find('next');
4122
- };
4123
-
4124
- const replaceAll = () => {
4125
- const searchText = findInput.value;
4126
- const replaceText = replaceInput.value;
4127
- const matchCase = matchCaseCheckbox.checked;
4128
- const useRegex = useRegexCheckbox.checked;
4129
- const wholeWord = wholeWordCheckbox.checked;
4130
-
4131
- if (!searchText) return;
4132
-
4133
- let pattern = searchText;
4134
-
4135
- if (!useRegex) {
4136
- const cleaned = wholeWord ? searchText.trim() : searchText;
4137
- const escaped = cleaned.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4138
-
4139
- pattern = wholeWord
4140
- ? `(?<!\\w)${escaped}(?!\\w)`
4141
- : escaped;
4142
- }
4143
-
4144
- let regex;
4145
- try {
4146
- regex = new RegExp(pattern, matchCase ? 'g' : 'gi');
4147
- } catch {
4148
- return;
4149
- }
4150
-
4151
- // 🔹 Collect matches (same as find)
4152
- const walker = core.elements.iframeWindow.createTreeWalker(
4153
- editor,
4154
- NodeFilter.SHOW_TEXT,
4155
- null
4156
- );
4157
-
4158
- const matches = [];
4159
- let node;
4160
-
4161
- while ((node = walker.nextNode())) {
4162
- const text = node.nodeValue;
4163
- regex.lastIndex = 0;
4164
-
4165
- let match;
4166
- while ((match = regex.exec(text)) !== null) {
4167
- matches.push({
4168
- node,
4169
- start: match.index,
4170
- end: match.index + match[0].length,
4171
- original: match[0]
4172
- });
4173
- }
4174
- }
4175
-
4176
- // 🔹 Replace in reverse (important)
4177
- let count = 0;
4178
-
4179
- for (let i = matches.length - 1; i >= 0; i--) {
4180
- const m = matches[i];
4181
- const text = m.node.nodeValue;
4182
-
4183
- const newText = m.original.replace(
4184
- new RegExp(pattern, matchCase ? '' : 'i'),
4185
- replaceText
4186
- );
4187
-
4188
- m.node.nodeValue =
4189
- text.slice(0, m.start) +
4190
- newText +
4191
- text.slice(m.end);
4192
-
4193
- count++;
4194
- }
4195
-
4196
- span.innerText = `Replaced ${count} occurrences.`;
4197
-
4198
- find('next');
4199
- };
4200
-
4201
- prevBtn.onclick = () => find('prev');
4202
- nextBtn.onclick = () => find('next');
4203
- replaceBtn.onclick = replace;
4204
- replaceAllBtn.onclick = replaceAll;
4205
-
4206
- const onInputKeydown = (event) => {
4207
- if (event.key === 'Enter') {
4208
- event.preventDefault();
4209
- find('next');
4210
- }
4211
- };
4212
- findInput.addEventListener('keydown', onInputKeydown);
4213
- }
4214
-
4215
- const isCaretAtStart = (range, li) => {
4216
- const preRange = range.cloneRange();
4217
- preRange.selectNodeContents(li);
4218
- preRange.setEnd(range.startContainer, range.startOffset);
4219
-
4220
- return preRange.toString().length === 0;
4221
- };
4222
- const handleBackspaceInList = (event, core) => {
4223
- if (event.key !== 'Backspace') return;
4224
-
4225
- const selection = core.elements.iframeWindow.getSelection();
4226
- if (!selection.rangeCount) return;
4227
- const range = selection.getRangeAt(0);
4228
-
4229
- if (!range.collapsed) return;
4230
-
4231
- let element = range.startContainer;
4232
- if (element.nodeType === Node.TEXT_NODE) {
4233
- element = element.parentElement;
4234
- }
4235
-
4236
- const li = element.closest('li');
4237
- if (!li || !core.elements.editor.contains(li)) return;
4238
-
4239
- const nestedList = li.querySelector('ul, ol');
4240
- if (!nestedList) return;
4241
-
4242
-
4243
- if (!isCaretAtStart(range, li)) return;
4244
-
4245
- event.preventDefault();
4246
-
4247
- const parentList = li.parentElement;
4248
- const prevLi = li.previousElementSibling;
4249
-
4250
- const clone = li.cloneNode(true);
4251
- const nestedInClone = clone.querySelector('ul, ol');
4252
- if (nestedInClone) nestedInClone.remove();
4253
-
4254
- const parentText = clone.textContent.replace(/\u200B/g, '').trim();
4255
-
4256
- const fragment = document.createDocumentFragment();
4257
- const children = Array.from(nestedList.children);
4258
-
4259
- children.forEach(child => fragment.appendChild(child));
4260
-
4261
- // Case 1: previous list item exists
4262
- if (prevLi) {
4263
-
4264
- if (parentText.length > 0) {
4265
- prevLi.appendChild(document.createTextNode(parentText));
4266
- }
4267
-
4268
- if (children.length > 0) {
4269
- parentList.insertBefore(fragment, li);
4270
- }
4271
-
4272
- li.remove();
4273
-
4274
- const newRange = document.createRange();
4275
- newRange.selectNodeContents(prevLi);
4276
- newRange.collapse(false);
4277
-
4278
- selection.removeAllRanges();
4279
- selection.addRange(newRange);
4280
- }
4281
- // Case 2: first list item
4282
- else {
4283
- const span = document.createElement('span');
4284
- span.textContent = parentText || '';
4285
-
4286
- parentList.parentElement.insertBefore(span, parentList);
4287
-
4288
- if (children.length > 0) {
4289
- parentList.insertBefore(fragment, li);
4290
- }
4291
-
4292
- li.remove();
4293
-
4294
- const newRange = document.createRange();
4295
- newRange.selectNodeContents(span);
4296
- newRange.collapse(true);
4297
-
4298
- selection.removeAllRanges();
4299
- selection.addRange(newRange);
4300
- }
4301
-
4302
- core.updateCaretPosition();
4303
- };
4304
-
4305
- export {
4306
- cleanHTML,
4307
- debounce,
4308
- makeToolbarButton,
4309
- makeToolbarDropdown,
4310
- openModal,
4311
- initImageResizer,
4312
- destroyImageResizer,
4313
- changeAllToolbarState,
4314
- changeToolbarValueByName,
4315
- makeToolbarSelect,
4316
- changeToolbarStateByName,
4317
- changeToolbarHtmlByName,
4318
- makeToolbarColor,
4319
- initTableEditPlugin,
4320
- destroyTableEditPlugin,
4321
- updateTableResizerPosition,
4322
- rgbToHex,
4323
- extractSocialMediaId,
4324
- constructEmbedUrl,
4325
- showAnchorPopover,
4326
- destroyAnchorPopover,
4327
- showRemoteCursor,
4328
- syncRemoteChangesDebounce,
4329
- applyRemoteChanges,
4330
- updateCursorPositionDebounce,
4331
- isLinkValid,
4332
- formatLink,
4333
- buildTributeValues,
4334
- toPx,
4335
- updateHeight,
4336
- getTableGrid,
4337
- getCellPosition,
4338
- applyFormatBlockInListItem,
4339
- normalizeHeadingWrappedLists,
4340
- makeUnorderedList,
4341
- makeOrderedList,
4342
- showTooltip,
4343
- makeHeading,
4344
- makeBlockQuote,
4345
- makeStrikethrough,
4346
- makeCodeBlock,
4347
- makeSublist,
4348
- transformTextStyle,
4349
- makeToolbarButtonSelect,
4350
- insertTOC,
4351
- insertOutlinesItems,
4352
- navigateToHeading,
4353
- updateOutlineItems,
4354
- highlightActiveOutline,
4355
- findAndReplace,
4356
- handleBackspaceInList,
4357
- trackListSelectionDeletion,
4358
- cleanupListSelectionDeletion,
4359
- }