leksy-editor 1.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 ADDED
@@ -0,0 +1,1798 @@
1
+ import { CLASSES, FONT_SIZES, FONTS, FORMATS, GIPHY_POWERED_IMAGE, REGEX, RESIZER_PLUGINS, SOCIAL_MEDIA_BASEURLS, SOCIAL_MEDIA_PATTERNS, SVG, TABLE_PLUGINS } from "./constant";
2
+
3
+ const traverseAndClean = (element) => {
4
+ if (!element?.childNodes) return;
5
+
6
+ // Iterate through child nodes
7
+ const childNodes = element.childNodes;
8
+ for (let i = childNodes.length - 1; i >= 0; i--) {
9
+ const node = childNodes[i];
10
+
11
+ // Recursively process child elements
12
+ if (node.nodeType === Node.ELEMENT_NODE) {
13
+ // Check if element tag is allowed
14
+ if (!REGEX.WHILE_LISTED_TAG.test(node.tagName)) {
15
+ // Remove the element if not allowed
16
+ node.parentNode.removeChild(node);
17
+ } else {
18
+ // Remove attributes not in the whitelist
19
+ const attributesToRemove = [];
20
+ for (const attr of node.attributes) {
21
+ if (!REGEX.WHILE_LISTED_ATTRIBUTES.test(attr.name) || attr.value.includes('var(')) {
22
+ attributesToRemove.push(attr.name);
23
+ }
24
+ }
25
+ attributesToRemove.forEach(attr => node.removeAttribute(attr));
26
+
27
+ // Process child nodes
28
+ traverseAndClean(node);
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ const cleanHTML = (html) => {
35
+ // Create a temporary div to manipulate the HTML
36
+ const tempDiv = document.createElement('div');
37
+ tempDiv.innerHTML = html;
38
+
39
+ // Traverse through all elements and clean attributes
40
+ traverseAndClean(tempDiv);
41
+
42
+ // Return manipulated HTML
43
+ return tempDiv.innerHTML;
44
+ }
45
+
46
+ const debounce = (fn, delay) => {
47
+ let timeout = null;
48
+
49
+ return (...args) => {
50
+ if (timeout) {
51
+ clearTimeout(timeout);
52
+ }
53
+
54
+ timeout = setTimeout(async () => {
55
+ fn(...args);
56
+ timeout = null; // Clear the timeout reference after execution
57
+ }, delay);
58
+ }
59
+ }
60
+
61
+ function asyncDebounce(fn, delay) {
62
+ let timeout = null;
63
+ let isRunning = false;
64
+ let callNextFn = false;
65
+ let newArgs = [];
66
+
67
+ function call(...args) {
68
+ if (isRunning) {
69
+ newArgs = args;
70
+ callNextFn = true;
71
+ return;
72
+ } else {
73
+ clearTimeout(timeout);
74
+ newArgs = [];
75
+ callNextFn = false;
76
+ }
77
+
78
+ return new Promise((resolve) => {
79
+ timeout = setTimeout(async () => {
80
+ isRunning = true;
81
+ await fn(...args);
82
+ resolve();
83
+ isRunning = false;
84
+ if (callNextFn) call(...newArgs);
85
+ }, delay);
86
+ });
87
+ }
88
+
89
+ return call;
90
+ }
91
+
92
+ function getDeviceName() {
93
+ const userAgent = navigator.userAgent;
94
+
95
+ if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
96
+ return "IOS";
97
+ } else if (/Android/.test(userAgent)) {
98
+ return "ANDROID";
99
+ } else if (/Macintosh/.test(userAgent)) {
100
+ return "MAC";
101
+ } else if (/Windows/.test(userAgent)) {
102
+ return "WINDOWS";
103
+ } else if (/Linux/.test(userAgent)) {
104
+ return "LINUX";
105
+ } else {
106
+ return "Unknown";
107
+ }
108
+ }
109
+
110
+ const makeToolbarButton = (_plugin, options, core) => {
111
+ const pluginButton = document.createElement('button');
112
+ pluginButton.type = "button"
113
+ pluginButton.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEM}`
114
+
115
+ pluginButton.setAttribute('data-title', _plugin.title)
116
+ pluginButton.setAttribute('data-type', 'button')
117
+ pluginButton.onclick = (e) => {
118
+ e.preventDefault();
119
+ _plugin.click(e, core, options);
120
+ };
121
+
122
+ const pluginIcon = document.createElement('div');
123
+ pluginIcon.innerHTML = _plugin.icon;
124
+ pluginButton.appendChild(pluginIcon)
125
+ return pluginButton
126
+ }
127
+
128
+ const makeToolbarColor = (_plugin, options, core) => {
129
+ const pluginContainer = document.createElement('div');
130
+ pluginContainer.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEM} ${CLASSES.TOOLBAR_ITEM_COLOR}`
131
+ pluginContainer.setAttribute('data-title', _plugin.title)
132
+ pluginContainer.setAttribute('data-type', 'color')
133
+
134
+ const pluginButton = document.createElement('input');
135
+ pluginButton.type = "color"
136
+ pluginButton.onclick = () => { if (getDeviceName() !== 'IOS') core.elements.editor.focus() }
137
+
138
+ pluginButton.onchange = (e) => {
139
+ e.preventDefault();
140
+ _plugin.change(e, core, options);
141
+ };
142
+
143
+ const pluginIcon = document.createElement('div');
144
+ pluginIcon.innerHTML = _plugin.icon;
145
+ pluginContainer.append(pluginButton, pluginIcon)
146
+ return pluginContainer
147
+ }
148
+
149
+ const makeToolbarDropdown = (_plugin, options, core) => {
150
+ const handleOutsideClick = (e, forceFully) => {
151
+ if (!forceFully && (e.target.getAttribute('data-toolbar-option') || dropdownPlugin.contains(e.target))) return
152
+ dropdownContent.style.display = 'none'
153
+ document.removeEventListener('mousedown', handleOutsideClick, false);
154
+ core.elements.editor.removeEventListener('click', handleOutsideClick, false);
155
+ }
156
+ const renderGalary = (images) => {
157
+ if ((core.state.page == 1 && !core.state.next) || (!core.state.page && !core.state.next)) container.innerHTML = ''
158
+ images.forEach(image => {
159
+ const imageElement = document.createElement('img');
160
+ imageElement.src = image.src;
161
+ imageElement.style.width = '120px';
162
+ imageElement.style.height = '120px';
163
+ imageElement.style.cursor = 'pointer';
164
+ imageElement.style.margin = '2px';
165
+ imageElement.onclick = (e) => {
166
+ const img = document.createElement('img');
167
+ img.src = image.src
168
+ core.insertNode(img)
169
+ core.updateCaretPosition()
170
+ options.closePluginOnClick && handleOutsideClick(e, true)
171
+ };
172
+ container.appendChild(imageElement);
173
+ })
174
+ }
175
+ const renderMentions = (value) => {
176
+ container.innerHTML = ''
177
+ const labels = JSON.parse(JSON.stringify(options.labels))
178
+ .map(label => {
179
+ label.fields = label.fields.filter(field => field.name.toLowerCase().includes(value.toLowerCase()))
180
+ return label
181
+ })
182
+ .filter(label => label.fields.length)
183
+ labels.forEach(category => {
184
+ const categoryElement = document.createElement('div')
185
+ categoryElement.textContent = category.name
186
+ container.append(categoryElement)
187
+ category.fields.forEach(field => {
188
+ const labelElement = document.createElement('button')
189
+ labelElement.type = "button"
190
+ labelElement.textContent = field.name
191
+ labelElement.onclick = (e) => {
192
+ core.elements.editor.focus();
193
+ const span = document.createElement('span');
194
+ span.spellcheck = false
195
+ span.contentEditable = false
196
+ span.innerText = field.name
197
+ core.insertNode(span)
198
+ core.updateCaretPosition()
199
+ options.closePluginOnClick && handleOutsideClick(e, true)
200
+ }
201
+ container.append(labelElement)
202
+ })
203
+ })
204
+ }
205
+
206
+ const dropdownPlugin = document.createElement('div');
207
+ dropdownPlugin.setAttribute('data-type', 'dropdown')
208
+
209
+ const dropdownButton = document.createElement('button');
210
+ dropdownButton.type = "button"
211
+ dropdownButton.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEM}`
212
+ dropdownButton.setAttribute('data-title', _plugin.title)
213
+
214
+ const pluginIcon = document.createElement('div');
215
+ pluginIcon.innerHTML = _plugin.icon;
216
+ dropdownButton.appendChild(pluginIcon)
217
+
218
+ const dropdownContent = document.createElement('div');
219
+ dropdownContent.className = `${options.classPrefix}${CLASSES.DROPDOWN_CONTENT} ${_plugin.type}`
220
+
221
+ const container = document.createElement('div');
222
+
223
+ if (_plugin.type === 'dropdown') {
224
+ _plugin.options.forEach(option => {
225
+ const optionDiv = document.createElement('button');
226
+ optionDiv.type = "button"
227
+ optionDiv.textContent = option.label;
228
+ optionDiv.setAttribute('data-value', option.value); // Set custom attribute for value
229
+ optionDiv.setAttribute('data-toolbar-option', true); // Set custom attribute for value
230
+
231
+ // Event listener to handle selection
232
+ optionDiv.addEventListener('click', function (e) {
233
+ dropdownContent.style.display = 'none';
234
+ _plugin.click(e, core, options)
235
+ });
236
+
237
+ // Append option to the dropdown content div
238
+ dropdownContent.appendChild(optionDiv);
239
+ });
240
+ } else if (_plugin.type === 'gallery') {
241
+ const search = document.createElement('input');
242
+ search.placeholder = 'Search...'
243
+ const fetchAndRenderImages = async (searchQuery = '', page) => {
244
+ let res;
245
+ if (searchQuery) {
246
+ res = await _plugin.search(options, searchQuery, page);
247
+ } else {
248
+ res = await _plugin.suggestions(options, page);
249
+ }
250
+ if (res.totalPages) core.state.totalPages = res.totalPages; // Update total pages if available
251
+ if (res.next) core.state.next = res.next; // Update next page if available
252
+ renderGalary(res.images); // Pass search query and append flag
253
+ };
254
+
255
+ const debounce = asyncDebounce(async (value) => {
256
+ core.state.page = 1; // Reset page on new search
257
+ core.state.next = "";
258
+ core.state.currentSearch = value; // Store the search term
259
+ if (_plugin.title == "Tenor") {
260
+ await fetchAndRenderImages(value, "");
261
+ } else {
262
+ await fetchAndRenderImages(value, 1);
263
+ }
264
+ }, 300);
265
+
266
+ search.oninput = (e) => debounce(e.target.value)
267
+
268
+ dropdownContent.addEventListener("scroll", async () => {
269
+ if (core.state.isFetching) return; // Prevent multiple calls while fetching
270
+
271
+ if (dropdownContent.scrollTop + dropdownContent.clientHeight >= dropdownContent.scrollHeight - 10) {
272
+ if (core.state.next) {
273
+ core.state.isFetching = true; // Set fetching state
274
+ await fetchAndRenderImages(core.state.currentSearch, core.state.next); // Fetch new images
275
+ core.state.isFetching = false; // Reset fetching state
276
+ }
277
+ if (core.state.totalPages && core.state.page < core.state.totalPages) {
278
+ core.state.isFetching = true; // Set fetching state
279
+ core.state.page += 1; // Increment page
280
+ await fetchAndRenderImages(core.state.currentSearch, core.state.page); // Fetch new images
281
+ core.state.isFetching = false; // Reset fetching state
282
+ }
283
+ }
284
+ });
285
+ const footer = document.createElement('div');
286
+ footer.className = 'footer';
287
+ if (_plugin.title === "GIPHY") {
288
+ const img = document.createElement('img');
289
+
290
+ img.src = `data:image/png;base64,${GIPHY_POWERED_IMAGE}`;
291
+ img.alt = "Powered By GIPHY";
292
+ footer.append(img)
293
+ } else {
294
+ footer.innerHTML = `<span>Powered By ${_plugin.title}</span>`;
295
+ }
296
+ footer.style.position = 'sticky';
297
+ footer.style.bottom = '0';
298
+ footer.style.width = '100%';
299
+ footer.style.background = '#f1f1f1';
300
+ footer.style.textAlign = 'center';
301
+ footer.style.height = '44px';
302
+ footer.style.display = 'flex';
303
+ footer.style.alignItems = 'center';
304
+ footer.style.justifyContent = 'center';
305
+ footer.style.overflow = 'hidden';
306
+
307
+ dropdownContent.append(search, container, footer)
308
+ } else if (_plugin.type === 'category') {
309
+ _plugin.categories.forEach(category => {
310
+ const categoryElement = document.createElement('div')
311
+ categoryElement.textContent = category.label
312
+ dropdownContent.append(categoryElement)
313
+ category.icons.forEach(emoji => {
314
+ const emojiElement = document.createElement('span')
315
+ emojiElement.textContent = emoji
316
+ emojiElement.onclick = (e) => {
317
+ core.elements.editor.focus();
318
+ core.elements.iframeWindow.execCommand('insertText', false, emoji)
319
+ options.closePluginOnClick && handleOutsideClick(e, true)
320
+ }
321
+ dropdownContent.append(emojiElement)
322
+ })
323
+ })
324
+ } else if (_plugin.type === 'mention') {
325
+ const search = document.createElement('input');
326
+ search.placeholder = 'Search...'
327
+ search.oninput = (e) => renderMentions(e.target.value)
328
+ dropdownContent.append(search, container)
329
+ renderMentions('')
330
+ } else if (_plugin.type === 'table') {
331
+ const totalRows = 10;
332
+ const totalCols = 10;
333
+ const table = document.createElement('div');
334
+ table.className = `${options.classPrefix}${CLASSES.TOOLBAR_TABLE}`
335
+
336
+ // Create table selector grid
337
+ for (let i = 0; i < totalRows; i++) {
338
+ for (let j = 0; j < totalCols; j++) {
339
+
340
+ const cell = document.createElement('div');
341
+ cell.className = 'cell'
342
+
343
+ cell.setAttribute('data-row', i);
344
+ cell.setAttribute('data-col', j);
345
+ if (j === 0) {
346
+ cell.setAttribute('tabIndex', '0');
347
+ }
348
+
349
+ cell.addEventListener('mouseover', () => {
350
+ const cells = table.querySelectorAll('.cell');
351
+ cells.forEach((cell) => {
352
+ const cellRow = parseInt(cell.getAttribute('data-row'));
353
+ const cellCol = parseInt(cell.getAttribute('data-col'));
354
+
355
+ if (cellRow <= i && cellCol <= j) {
356
+ cell.classList.add('selected');
357
+ } else {
358
+ cell.classList.remove('selected');
359
+ }
360
+ });
361
+ });
362
+ cell.addEventListener('click', () => {
363
+ _plugin.create(core, options, { rows: i, cols: j })
364
+ dropdownContent.style.display = 'none'
365
+ });
366
+
367
+ table.appendChild(cell);
368
+ }
369
+ }
370
+ dropdownContent.appendChild(table)
371
+ }
372
+
373
+ let isSuggestionLoaded = false;
374
+ dropdownButton.onclick = () => {
375
+ if (dropdownContent.style.display === 'block') {
376
+ dropdownContent.style.display = 'none'
377
+ document.removeEventListener('mousedown', handleOutsideClick, false);
378
+ core.elements.editor.removeEventListener('click', handleOutsideClick, false);
379
+ return
380
+ }
381
+ dropdownContent.style.display = 'block'
382
+ if (dropdownContent.getBoundingClientRect().right > window.innerWidth) dropdownContent.style.right = '10px'
383
+
384
+ if (!isSuggestionLoaded && _plugin.type === 'gallery') {
385
+ _plugin.suggestions(options)
386
+ .then(res => {
387
+ isSuggestionLoaded = true
388
+ if (res.totalPages) {
389
+ core.state.totalPages = res.totalPages;
390
+ core.state.next = ""
391
+ } // Update total pages if available
392
+ if (res.next) {
393
+ core.state.next = res.next;
394
+ core.state.totalPages = null
395
+ } // Update next page if available
396
+ renderGalary(res.images)
397
+ })
398
+ }
399
+
400
+ document.addEventListener('mousedown', handleOutsideClick);
401
+ core.elements.editor.addEventListener('click', handleOutsideClick);
402
+ }
403
+
404
+ dropdownPlugin.appendChild(dropdownButton)
405
+ dropdownPlugin.appendChild(dropdownContent)
406
+ return dropdownPlugin
407
+ }
408
+
409
+ const makeToolbarSelect = (_plugin, options, core) => {
410
+ const handleOutsideClick = (e) => {
411
+ if (e.target.getAttribute('data-toolbar-option') || selectPlugin.contains(e.target)) return
412
+ dropdownContent.style.display = 'none'
413
+ document.removeEventListener('mousedown', handleOutsideClick, false);
414
+ core.elements.editor.removeEventListener('click', handleOutsideClick, false);
415
+ }
416
+
417
+ const selectPlugin = document.createElement('div');
418
+ selectPlugin.setAttribute('data-type', 'select')
419
+
420
+ const dropdownButton = document.createElement('button');
421
+ dropdownButton.type = "button"
422
+ dropdownButton.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEM} ${CLASSES.TOOLBAR_ITEM_SELECT}`
423
+ dropdownButton.setAttribute('data-title', _plugin.title)
424
+
425
+ const pluginIcon = document.createElement('div');
426
+ pluginIcon.style.width = _plugin.width
427
+ pluginIcon.style.textAlign = 'left'
428
+ pluginIcon.style.display = 'flex'
429
+ pluginIcon.style.justifyContent = 'space-between'
430
+ const iconName = document.createElement('span');
431
+ iconName.innerHTML = _plugin.icon;
432
+ const arrow = document.createElement('span');
433
+ arrow.innerHTML = SVG.ARROW_DOWN;
434
+ arrow.style.paddingLeft = '8px';
435
+ pluginIcon.append(iconName, arrow)
436
+
437
+ dropdownButton.append(pluginIcon)
438
+
439
+ const dropdownContent = document.createElement('div');
440
+ dropdownContent.className = `${options.classPrefix}${CLASSES.DROPDOWN_CONTENT}`
441
+ _plugin.options.forEach(option => {
442
+ const optionDiv = document.createElement('button');
443
+ optionDiv.type = "button"
444
+ optionDiv.innerHTML = option.label;
445
+ optionDiv.setAttribute('data-value', option.value); // Set custom attribute for value
446
+ optionDiv.setAttribute('data-toolbar-option', true); // Set custom attribute for value
447
+
448
+ // Event listener to handle selection
449
+ optionDiv.addEventListener('click', function (e) {
450
+ dropdownContent.style.display = 'none';
451
+ _plugin.click(option.value, core, options)
452
+ });
453
+
454
+ // Append option to the dropdown content div
455
+ dropdownContent.appendChild(optionDiv);
456
+ });
457
+
458
+ dropdownButton.onclick = () => {
459
+ if (dropdownContent.style.display === 'block') {
460
+ dropdownContent.style.display = 'none'
461
+ document.removeEventListener('mousedown', handleOutsideClick, false);
462
+ core.elements.editor.removeEventListener('click', handleOutsideClick, false);
463
+ return
464
+ }
465
+ dropdownContent.style.display = 'block'
466
+ if (dropdownContent.getBoundingClientRect().right > window.innerWidth) dropdownContent.style.right = '10px'
467
+ document.addEventListener('mousedown', handleOutsideClick);
468
+ core.elements.editor.addEventListener('click', handleOutsideClick);
469
+ }
470
+
471
+ selectPlugin.appendChild(dropdownButton)
472
+ selectPlugin.appendChild(dropdownContent)
473
+ return selectPlugin
474
+ }
475
+
476
+ const makeEditToolbar = (options, core, { type, td, image, updateImage, updateTable }) => {
477
+ const handleOutsideClick = (e) => {
478
+ if (popoverPlugin.contains(e.target)) return
479
+ popoverContent.style.display = 'none'
480
+ document.removeEventListener('mousedown', handleOutsideClick, false);
481
+ core.elements.editor.removeEventListener('click', handleOutsideClick, false);
482
+ }
483
+
484
+ const popoverPlugin = document.createElement('div');
485
+ popoverPlugin.setAttribute('data-type', 'popover')
486
+
487
+ const popoverButton = document.createElement('button');
488
+ popoverButton.type = "button"
489
+ popoverButton.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEM}`
490
+ popoverButton.setAttribute('data-title', `Edit ${type === 'td' ? 'Cell' : type === 'image' ? 'Image' : "Embed"}`)
491
+
492
+ const pluginIcon = document.createElement('div');
493
+ pluginIcon.innerHTML = SVG.EDIT;
494
+ popoverButton.appendChild(pluginIcon)
495
+
496
+ const popoverContent = document.createElement('div');
497
+ popoverContent.className = `${options.classPrefix}${CLASSES.POPOVER_CONTENT}`
498
+
499
+ if (type === 'image' || type === 'figure') {
500
+ const resizeByIds = {
501
+ byPercentage: Math.random(),
502
+ byPixels: Math.random(),
503
+ percentageWidth: Math.random(),
504
+ percentageHeight: Math.random(),
505
+ pixelsWidth: Math.random(),
506
+ pixelsHeight: Math.random(),
507
+ percentage: Math.random(),
508
+ pixels: Math.random(),
509
+ aspectRatio: Math.random(),
510
+ altText: Math.random(),
511
+ submit: Math.random(),
512
+ }
513
+
514
+ popoverContent.innerHTML = (`
515
+ <div style="border-bottom: 1px solid gray; padding-bottom: 4px;">
516
+ <div>
517
+ By:
518
+ <input type="radio" value="percentage" id="${resizeByIds.byPercentage}" checked/>
519
+ <label for="${resizeByIds.byPercentage}">Percentage</label>
520
+ <input type="radio" value="pixels" id="${resizeByIds.byPixels}" />
521
+ <label for="${resizeByIds.byPixels}">Pixels</label>
522
+ </div>
523
+ <div id="${resizeByIds.percentage}">
524
+ <div style="display: flex; justify-content: space-between; margin-top: 4px">
525
+ <label for="${resizeByIds.percentageWidth}">Width (%)</label>
526
+ <input style="width: 100px" type="number" name='width' placeholder="Width" id="${resizeByIds.percentageWidth}" />
527
+ </div>
528
+ <div style="display: flex; justify-content: space-between; margin-top: 4px">
529
+ <label for="${resizeByIds.percentageHeight}">Height (%)</label>
530
+ <input style="width: 100px" type="number" name='height' placeholder="Height" id="${resizeByIds.percentageHeight}" />
531
+ </div>
532
+ </div>
533
+ <div id="${resizeByIds.pixels}" style="display: none">
534
+ <div style="display: flex; justify-content: space-between; margin-top: 4px">
535
+ <label for="${resizeByIds.pixelsWidth}">Width (px)</label>
536
+ <input style="width: 100px" type="number" name='width' placeholder="Width" id="${resizeByIds.pixelsWidth}" />
537
+ </div>
538
+ <div style="display: flex; justify-content: space-between; margin-top: 4px">
539
+ <label for="${resizeByIds.pixelsHeight}">Height (px)</label>
540
+ <input style="width: 100px" type="number" name='height' placeholder="Height" id="${resizeByIds.pixelsHeight}" />
541
+ </div>
542
+ </div>
543
+ <div style="margin-top: 4px;">
544
+ <input type="checkbox" id="${resizeByIds.aspectRatio}" checked/>
545
+ <label for="${resizeByIds.aspectRatio}">Maintain Aspect Ratio</label>
546
+ </div>
547
+ ${type === "image" ? `
548
+ <div style="display: flex; justify-content: space-between; margin-top: 4px">
549
+ <label for="${resizeByIds.altText}">Alt Text</label>
550
+ <input style="width: 100px" type="text" id="${resizeByIds.altText}" placeholder="Enter alt text" style="width: 100%;" />
551
+ </div>` : ""}
552
+ <div style="display: flex; justify-content: end; margin-top: 4px;">
553
+ <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>
554
+ </div>
555
+ </div >
556
+ `)
557
+
558
+ const pluginSet = document.createElement('div')
559
+ pluginSet.className = `${options.classPrefix}${CLASSES.RESIZE_ITEMS} `
560
+ RESIZER_PLUGINS.forEach(plugin => {
561
+ const pluginButton = document.createElement('button');
562
+ pluginButton.type = "button"
563
+ pluginButton.className = `${options.classPrefix}${CLASSES.RESIZE_ITEM} `
564
+
565
+ pluginButton.setAttribute('data-title', plugin.title)
566
+ pluginButton.setAttribute('data-type', 'button')
567
+ pluginButton.onclick = (e) => {
568
+ e.preventDefault();
569
+ updateImage(plugin.event);
570
+ };
571
+
572
+ const pluginIcon = document.createElement('div');
573
+ pluginIcon.innerHTML = plugin.icon;
574
+ pluginButton.appendChild(pluginIcon)
575
+ pluginSet.appendChild(pluginButton)
576
+ })
577
+ popoverContent.appendChild(pluginSet)
578
+ popoverButton.onclick = () => {
579
+ const onInputKeydown = (event) => {
580
+ if (event.key === 'Enter') {
581
+ event.preventDefault()
582
+ resizeBy.submit.click();
583
+ }
584
+ }
585
+
586
+ if (popoverContent.style.display === 'block') {
587
+ popoverContent.style.display = 'none'
588
+ document.removeEventListener('mousedown', handleOutsideClick, false);
589
+ core.elements.editor.removeEventListener('click', handleOutsideClick, false);
590
+ return
591
+ }
592
+ popoverContent.style.display = 'block'
593
+ if (popoverContent.getBoundingClientRect().right > window.innerWidth) popoverContent.style.right = '10px'
594
+ const resizeBy = {
595
+ byPercentage: document.getElementById(resizeByIds.byPercentage),
596
+ byPixels: document.getElementById(resizeByIds.byPixels),
597
+ percentageWidth: document.getElementById(resizeByIds.percentageWidth),
598
+ percentageHeight: document.getElementById(resizeByIds.percentageHeight),
599
+ pixelsWidth: document.getElementById(resizeByIds.pixelsWidth),
600
+ pixelsHeight: document.getElementById(resizeByIds.pixelsHeight),
601
+ percentage: document.getElementById(resizeByIds.percentage),
602
+ pixels: document.getElementById(resizeByIds.pixels),
603
+ aspectRatio: document.getElementById(resizeByIds.aspectRatio),
604
+ submit: document.getElementById(resizeByIds.submit),
605
+ }
606
+ if (type == "image") resizeBy.altText = document.getElementById(resizeByIds.altText)
607
+
608
+ const imageClientRect = image.getBoundingClientRect()
609
+ resizeBy.percentageWidth.value = (imageClientRect.width / image.naturalWidth) * 100
610
+ resizeBy.percentageHeight.value = (imageClientRect.height / image.naturalHeight) * 100
611
+ resizeBy.pixelsWidth.value = imageClientRect.width
612
+ resizeBy.pixelsHeight.value = imageClientRect.height
613
+ if (type == "image") resizeBy.altText.value = image.alt || "";
614
+
615
+ resizeBy.byPercentage.onchange = (e) => {
616
+ if (e.target.checked) {
617
+ resizeBy.pixels.style.display = 'none'
618
+ resizeBy.percentage.style.display = 'block'
619
+ resizeBy.byPixels.checked = false
620
+ }
621
+ }
622
+ resizeBy.byPixels.onchange = (e) => {
623
+ if (e.target.checked) {
624
+ resizeBy.percentage.style.display = 'none'
625
+ resizeBy.pixels.style.display = 'block'
626
+ resizeBy.byPercentage.checked = false
627
+ }
628
+ }
629
+
630
+ const handlePercentage = (e) => {
631
+ let value = parseInt(e.target.value)
632
+ if (value) {
633
+ if (value > 100) value = 100;
634
+ if (resizeBy.aspectRatio.checked) {
635
+ resizeBy.percentageWidth.value = value
636
+ resizeBy.percentageHeight.value = value
637
+ } else {
638
+ e.target.value = value
639
+ }
640
+ }
641
+ }
642
+ resizeBy.percentageWidth.oninput = handlePercentage
643
+ resizeBy.percentageHeight.oninput = handlePercentage
644
+
645
+ const handlePixel = (e) => {
646
+ let value = parseInt(e.target.value)
647
+ if (value) {
648
+ if (resizeBy.aspectRatio.checked) {
649
+ let width, height;
650
+ if (e.target.name === 'width') {
651
+ width = value
652
+ const delta = image.naturalWidth / value
653
+ height = image.naturalHeight / delta
654
+ } else {
655
+ height = value
656
+ const delta = image.naturalHeight / value
657
+ width = image.naturalWidth / delta
658
+ }
659
+
660
+ resizeBy.pixelsWidth.value = width
661
+ resizeBy.pixelsHeight.value = height
662
+ } else {
663
+ e.target.value = value
664
+ }
665
+ }
666
+ }
667
+ resizeBy.pixelsWidth.oninput = handlePixel
668
+ resizeBy.pixelsHeight.oninput = handlePixel
669
+
670
+ resizeBy.percentageWidth.onkeydown = onInputKeydown
671
+ resizeBy.percentageHeight.onkeydown = onInputKeydown
672
+ resizeBy.pixelsWidth.onkeydown = onInputKeydown
673
+ resizeBy.pixelsHeight.onkeydown = onInputKeydown
674
+
675
+ resizeBy.aspectRatio.onchange = (e) => {
676
+ if (e.target.checked) {
677
+ if (resizeBy.byPixels.checked) {
678
+ const delta = image.naturalWidth / parseInt(resizeBy.pixelsWidth.value)
679
+ const height = image.naturalHeight / delta
680
+ resizeBy.pixelsHeight.value = height
681
+ } else {
682
+ resizeBy.percentageHeight.value = resizeBy.percentageWidth.value
683
+ }
684
+ }
685
+ }
686
+
687
+ resizeBy.submit.onclick = (e) => {
688
+ if (resizeBy.byPixels.checked) {
689
+ if (parseInt(resizeBy.pixelsWidth.value) && parseInt(resizeBy.pixelsHeight.value))
690
+ updateImage('update', { width: resizeBy.pixelsWidth.value + 'px', height: resizeBy.pixelsHeight.value + 'px', })
691
+ } else if (resizeBy.byPercentage.checked) {
692
+ if (parseInt(resizeBy.percentageWidth.value) && parseInt(resizeBy.percentageHeight.value))
693
+ updateImage('update', { width: resizeBy.percentageWidth.value + '%', height: resizeBy.percentageHeight.value + '%', })
694
+ }
695
+ if (type == "image") image.alt = resizeBy.altText.value;
696
+ }
697
+
698
+ document.addEventListener('mousedown', handleOutsideClick);
699
+ core.elements.editor.addEventListener('click', handleOutsideClick);
700
+ }
701
+ } else if (type === 'td') {
702
+ const pluginSet = document.createElement('div')
703
+ pluginSet.className = `${options.classPrefix}${CLASSES.RESIZE_ITEMS} `
704
+ TABLE_PLUGINS.forEach(plugin => {
705
+ const pluginButton = plugin.event === 'border-color'
706
+ ? document.createElement('input')
707
+ : document.createElement('button');
708
+ if (plugin.event === 'border-color') {
709
+ pluginButton.type = "color"
710
+ pluginButton.style.width = '30px'
711
+ pluginButton.style.padding = '4px'
712
+ pluginButton.oninput = (e) => {
713
+ updateTable(plugin.event, e.target.value);
714
+ }
715
+ } else
716
+ pluginButton.type = "button"
717
+ pluginButton.className = `${options.classPrefix}${CLASSES.RESIZE_ITEM} `
718
+ pluginButton.setAttribute('data-title', plugin.title)
719
+ pluginButton.setAttribute('data-type', 'button')
720
+
721
+ if (plugin.event !== 'border-color') {
722
+ pluginButton.onclick = (e) => {
723
+ e.preventDefault();
724
+ updateTable(plugin.event);
725
+ };
726
+
727
+ const pluginIcon = document.createElement('div');
728
+ pluginIcon.innerHTML = plugin.icon;
729
+ pluginButton.appendChild(pluginIcon)
730
+ }
731
+ pluginSet.appendChild(pluginButton)
732
+ })
733
+ popoverContent.appendChild(pluginSet)
734
+
735
+ popoverButton.onclick = () => {
736
+ if (popoverContent.style.display === 'block') {
737
+ popoverContent.style.display = 'none'
738
+ document.removeEventListener('mousedown', handleOutsideClick, false);
739
+ core.elements.editor.removeEventListener('click', handleOutsideClick, false);
740
+ return
741
+ }
742
+ popoverContent.style.display = 'block'
743
+ if (popoverContent.getBoundingClientRect().right > window.innerWidth) popoverContent.style.right = '10px'
744
+ document.addEventListener('mousedown', handleOutsideClick);
745
+ core.elements.editor.addEventListener('click', handleOutsideClick);
746
+ }
747
+ }
748
+
749
+ popoverPlugin.appendChild(popoverButton)
750
+ popoverPlugin.appendChild(popoverContent)
751
+ core.elements.toolbarContainer.appendChild(popoverPlugin)
752
+ core.elements.resizerPlugin = popoverPlugin
753
+ }
754
+
755
+ const openModal = ({ title, bodyNode, footerNode }, core, options) => {
756
+ const modalContainer = document.createElement('div');
757
+ modalContainer.className = `${options.classPrefix}${CLASSES.MODAL} `
758
+
759
+ const modalContent = document.createElement('div');
760
+ modalContent.className = `${options.classPrefix}${CLASSES.MODAL_CONTENT} `
761
+
762
+ const modalHeader = document.createElement('div');
763
+ modalHeader.className = `${options.classPrefix}${CLASSES.MODAL_HEADER} `
764
+ const modalTitle = document.createElement('span');
765
+ modalTitle.innerText = title ?? ''
766
+ const modalCloseButton = document.createElement('button');
767
+ modalCloseButton.className = 'modal-close-btn';
768
+ modalCloseButton.innerHTML = SVG.CLOSE;
769
+ modalCloseButton.type = 'button'
770
+ modalCloseButton.onclick = () => {
771
+ core.elements.base.removeChild(modalContainer)
772
+ }
773
+ modalHeader.append(modalTitle, modalCloseButton)
774
+
775
+ const modalBody = document.createElement('div');
776
+ modalBody.className = `${options.classPrefix}${CLASSES.MODAL_BODY} `
777
+ modalBody.appendChild(bodyNode)
778
+
779
+ const modalFooter = document.createElement('div');
780
+ modalFooter.className = `${options.classPrefix}${CLASSES.MODAL_FOOTER} `
781
+ modalFooter.appendChild(footerNode)
782
+
783
+ modalContent.appendChild(modalHeader);
784
+ modalContent.appendChild(modalBody);
785
+ modalContent.appendChild(modalFooter);
786
+
787
+ modalContainer.appendChild(modalContent)
788
+ core.elements.base.appendChild(modalContainer)
789
+
790
+ return {
791
+ close: () => { core.elements.base.removeChild(modalContainer) }
792
+ }
793
+ }
794
+
795
+ const initImageResizer = (type, image, options, core) => {
796
+ destroyImageResizer(options, core)
797
+ changeAllToolbarState(core, 'disabled', ['link'])
798
+ core.elements.editor.blur() // to remove focus from the editor
799
+
800
+ const imageClientRect = image.getBoundingClientRect();
801
+ const resizer = document.createElement('div');
802
+ resizer.className = `resizer`
803
+ resizer.style.width = imageClientRect.width + 'px';
804
+ resizer.style.height = imageClientRect.height + 'px';
805
+ resizer.style.top = imageClientRect.top + 'px';
806
+ resizer.style.left = imageClientRect.left + 'px';
807
+
808
+ // Full-screen button (only for images)
809
+ if (type === 'image') {
810
+ const fullscreenButton = document.createElement('span');
811
+ fullscreenButton.innerHTML = SVG.FULLSCREEN_IMAGE
812
+ fullscreenButton.style.position = 'absolute';
813
+ fullscreenButton.style.top = '5px';
814
+ fullscreenButton.style.right = '5px';
815
+ fullscreenButton.style.background = 'rgba(0, 0, 0, 0.6)';
816
+ fullscreenButton.style.color = 'white';
817
+ fullscreenButton.style.border = 'none';
818
+ fullscreenButton.style.cursor = 'pointer';
819
+ fullscreenButton.style.borderRadius = '4px';
820
+ fullscreenButton.style.fontSize = '14px';
821
+ fullscreenButton.style.transition = '0.3s ease-in-out';
822
+ fullscreenButton.style.width = '28px';
823
+ fullscreenButton.style.height = '28px';
824
+ fullscreenButton.style.display = 'flex';
825
+ fullscreenButton.style.alignItems = 'center';
826
+ fullscreenButton.style.justifyContent = 'center';
827
+
828
+ fullscreenButton.addEventListener('click', () => {
829
+ createPreviewModal(image.src, core, options); // Open custom modal with image preview
830
+ });
831
+
832
+ resizer.appendChild(fullscreenButton);
833
+ }
834
+
835
+ let firstX, firstY, direction, aspectRatio, resizerClientRect;
836
+ const handleMouseDown = (e) => {
837
+ resizerClientRect = resizer.getBoundingClientRect();
838
+ direction = e.target.getAttribute('data-resize')
839
+ firstX = e.clientX || e.touches[0].clientX;
840
+ firstY = e.clientY || e.touches[0].clientY;
841
+ aspectRatio = resizer.offsetWidth / resizer.offsetHeight;
842
+ core.elements.iframeWindow.addEventListener('mousemove', handleMouseMove)
843
+ core.elements.iframeWindow.addEventListener('mouseup', handleMouseUp)
844
+ core.elements.iframeWindow.addEventListener('touchmove', handleMouseMove)
845
+ core.elements.iframeWindow.addEventListener('touchend', handleMouseUp)
846
+ }
847
+
848
+ const handleMouseMove = (e) => {
849
+ const position = {
850
+ clientX: e.clientX || e.touches[0].clientX,
851
+ clientY: e.clientY || e.touches[0].clientY,
852
+ }
853
+ // Calculate the new dimensions
854
+ if (direction === 'up') {
855
+ let top = resizerClientRect.top + (position.clientY - firstY);
856
+ let height = resizerClientRect.height - (position.clientY - firstY);
857
+ if (height < 0) {
858
+ top = resizerClientRect.bottom;
859
+ height = 0
860
+ }
861
+ resizer.style.top = top + 'px';
862
+ resizer.style.height = height + 'px';
863
+ } else if (direction === 'bottom') {
864
+ let height = resizerClientRect.height + (position.clientY - firstY);
865
+ if (height < 0) height = 0
866
+ resizer.style.height = height + 'px';
867
+ } else if (direction === 'left') {
868
+ let left = resizerClientRect.left + (position.clientX - firstX);
869
+ let width = resizerClientRect.width - (position.clientX - firstX);
870
+ if (width < 0) {
871
+ left = resizerClientRect.right;
872
+ width = 0
873
+ }
874
+ resizer.style.left = left + 'px';
875
+ resizer.style.width = width + 'px';
876
+ } else if (direction === 'right') {
877
+ let width = resizerClientRect.width + (position.clientX - firstX);
878
+ if (width < 0) width = 0
879
+ resizer.style.width = width + 'px';
880
+ } else if (direction === 'bottom-right') {
881
+ const distanceX = position.clientX - firstX
882
+ const distanceY = position.clientY - firstY
883
+ const delta = Math.sign(distanceX) === -1 || Math.sign(distanceY) === -1
884
+ ? Math.min(distanceX, distanceY)
885
+ : Math.max(distanceX, distanceY)
886
+ const width = resizerClientRect.width + delta;
887
+ const height = width / aspectRatio;
888
+ resizer.style.width = width + 'px';
889
+ resizer.style.height = height + 'px';
890
+ } else if (direction === 'bottom-left') {
891
+ const distanceX = position.clientX - firstX
892
+ const distanceY = position.clientY - firstY
893
+ const delta = distanceX > 0 ? Math.max(distanceX, distanceY) : distanceX
894
+ let width = resizerClientRect.width - delta;
895
+ if (width < 0) width = 0
896
+ let left = resizerClientRect.left + resizerClientRect.width - width;
897
+ const height = width / aspectRatio;
898
+ resizer.style.width = width + 'px';
899
+ resizer.style.left = left + 'px';
900
+ resizer.style.height = height + 'px';
901
+ } else if (direction === 'up-right') {
902
+ const distanceX = position.clientX - firstX
903
+ const distanceY = position.clientY - firstY
904
+ const delta = distanceX > 0 ? Math.max(distanceX, distanceY) : distanceX
905
+
906
+ let width = resizerClientRect.width + delta;
907
+ if (width < 0) width = 0
908
+ const height = width / aspectRatio;
909
+ const top = resizerClientRect.top + (resizerClientRect.height - height)
910
+ resizer.style.top = top + 'px';
911
+ resizer.style.width = width + 'px';
912
+ resizer.style.height = height + 'px';
913
+ } else if (direction === 'up-left') {
914
+ const distanceX = position.clientX - firstX
915
+ const distanceY = position.clientY - firstY
916
+ const delta = distanceX > 0 ? Math.max(distanceX, distanceY) : distanceX
917
+ let width = resizerClientRect.width - delta;
918
+ if (width < 0) width = 0
919
+ const height = width / aspectRatio;
920
+ const top = resizerClientRect.top + (resizerClientRect.height - height)
921
+ const left = resizerClientRect.left + (resizerClientRect.width - width)
922
+ resizer.style.top = top + 'px';
923
+ resizer.style.width = width + 'px';
924
+ resizer.style.height = height + 'px';
925
+ resizer.style.left = left + 'px';
926
+ }
927
+ position.textContent = `${parseInt(resizer.style.width)}x${parseInt(resizer.style.height)} `;
928
+ }
929
+
930
+ const handleMouseUp = (e) => {
931
+ image.style.width = parseInt(resizer.style.width) + 'px'
932
+ image.style.height = parseInt(resizer.style.height) + 'px'
933
+ const imageClientRect = image.getBoundingClientRect();
934
+ resizer.style.width = imageClientRect.width + 'px';
935
+ resizer.style.height = imageClientRect.height + 'px';
936
+ resizer.style.top = imageClientRect.top + 'px';
937
+ resizer.style.left = imageClientRect.left + 'px';
938
+ core.elements.iframeWindow.removeEventListener('mousemove', handleMouseMove, false)
939
+ core.elements.iframeWindow.removeEventListener('mouseup', handleMouseUp, false)
940
+ core.elements.iframeWindow.removeEventListener('touchmove', handleMouseMove, false)
941
+ core.elements.iframeWindow.removeEventListener('touchend', handleMouseUp, false)
942
+ }
943
+
944
+ const dot1 = document.createElement('button');
945
+ dot1.type = 'button'
946
+ dot1.className = 'dot up-left'
947
+ dot1.setAttribute('data-resize', 'up-left')
948
+ dot1.addEventListener('mousedown', handleMouseDown)
949
+ dot1.addEventListener('touchstart', handleMouseDown)
950
+
951
+ const dot2 = document.createElement('button');
952
+ dot2.type = 'button'
953
+ dot2.className = 'dot up'
954
+ dot2.setAttribute('data-resize', 'up')
955
+ dot2.addEventListener('mousedown', handleMouseDown)
956
+ dot2.addEventListener('touchstart', handleMouseDown)
957
+
958
+ const dot3 = document.createElement('button');
959
+ dot3.type = 'button'
960
+ dot3.className = 'dot up-right'
961
+ dot3.setAttribute('data-resize', 'up-right')
962
+ dot3.addEventListener('mousedown', handleMouseDown)
963
+ dot3.addEventListener('touchstart', handleMouseDown)
964
+
965
+ const dot4 = document.createElement('button');
966
+ dot4.type = 'button'
967
+ dot4.className = 'dot right'
968
+ dot4.setAttribute('data-resize', 'right')
969
+ dot4.addEventListener('mousedown', handleMouseDown)
970
+ dot4.addEventListener('touchstart', handleMouseDown)
971
+
972
+ const dot5 = document.createElement('button');
973
+ dot5.type = 'button'
974
+ dot5.className = 'dot bottom-right'
975
+ dot5.setAttribute('data-resize', 'bottom-right')
976
+ dot5.addEventListener('mousedown', handleMouseDown)
977
+ dot5.addEventListener('touchstart', handleMouseDown)
978
+
979
+ const dot6 = document.createElement('button');
980
+ dot6.type = 'button'
981
+ dot6.className = 'dot bottom'
982
+ dot6.setAttribute('data-resize', 'bottom')
983
+ dot6.addEventListener('mousedown', handleMouseDown)
984
+ dot6.addEventListener('touchstart', handleMouseDown)
985
+
986
+ const dot7 = document.createElement('button');
987
+ dot7.type = 'button'
988
+ dot7.className = 'dot bottom-left'
989
+ dot7.setAttribute('data-resize', 'bottom-left')
990
+ dot7.addEventListener('mousedown', handleMouseDown)
991
+ dot7.addEventListener('touchstart', handleMouseDown)
992
+
993
+ const dot8 = document.createElement('button');
994
+ dot8.type = 'button'
995
+ dot8.className = 'dot left'
996
+ dot8.setAttribute('data-resize', 'left')
997
+ dot8.addEventListener('mousedown', handleMouseDown)
998
+ dot8.addEventListener('touchstart', handleMouseDown)
999
+
1000
+ const position = document.createElement('div')
1001
+ position.className = 'position';
1002
+ position.textContent = `${parseInt(imageClientRect.width)}x${parseInt(imageClientRect.height)} `;
1003
+
1004
+ resizer.append(dot1, dot2, dot3, dot4, dot5, dot6, dot7, dot8, position)
1005
+
1006
+ const updateImage = (event, data) => {
1007
+ switch (event) {
1008
+ case 'update':
1009
+ image.style.width = data.width;
1010
+ image.style.height = data.height;
1011
+ break;
1012
+ case '100%':
1013
+ case '75%':
1014
+ case '50%':
1015
+ case 'auto':
1016
+ image.style.width = event;
1017
+ image.style.height = 'unset';
1018
+ break;
1019
+ case 'revert':
1020
+ image.style.width = 'unset';
1021
+ image.style.height = 'unset';
1022
+ break;
1023
+ case 'delete':
1024
+ image.remove()
1025
+ destroyImageResizer(options, core)
1026
+ break;
1027
+ }
1028
+ const imageClientRect = image.getBoundingClientRect();
1029
+ resizer.style.width = imageClientRect.width + 'px';
1030
+ resizer.style.height = imageClientRect.height + 'px';
1031
+ resizer.style.top = imageClientRect.top + 'px';
1032
+ resizer.style.left = imageClientRect.left + 'px';
1033
+ position.textContent = `${parseInt(resizer.style.width)}x${parseInt(resizer.style.height)} `;
1034
+ }
1035
+
1036
+ makeEditToolbar(options, core, { type, image, updateImage })
1037
+
1038
+ core.elements.resizerImage = image
1039
+ core.elements.resizer = resizer
1040
+ core.elements.iframeWindow.body.appendChild(resizer)
1041
+ core.resizerHandler = (e) => updateImageResizerPosition(e, core);
1042
+ core.deleteImageHandler = (e) => {
1043
+ if (e.key === 'Backspace' || e.key === 'Delete') updateImage('delete')
1044
+ }
1045
+ core.elements.iframeWindow.addEventListener('scroll', core.resizerHandler)
1046
+ core.elements.iframeWindow.addEventListener('keydown', core.deleteImageHandler)
1047
+ }
1048
+
1049
+ const createPreviewModal = (imageSrc, core, options) => {
1050
+ // Create overlay
1051
+ const overlay = document.createElement('div');
1052
+ overlay.className = `${options.classPrefix}${CLASSES.PREVIEW_MODAL_OVERLAY} `
1053
+
1054
+ // Create modal content
1055
+ const modalContent = document.createElement('div');
1056
+ modalContent.className = `${options.classPrefix}${CLASSES.PREVIEW_MODAL_CONTENT} `
1057
+
1058
+ // Close button
1059
+ const closeButton = document.createElement('span');
1060
+ closeButton.className = `${options.classPrefix}${CLASSES.PREVIEW_MODAL_CLOSE} `
1061
+ closeButton.innerHTML = SVG.CLOSE;
1062
+ closeButton.onclick = () => overlay.remove();
1063
+
1064
+ // Create modal body
1065
+ const modalBody = document.createElement('div');
1066
+ modalBody.className = `${options.classPrefix}${CLASSES.PREVIEW_MODAL_BODY} `
1067
+
1068
+ // Image element
1069
+ const image = document.createElement('img');
1070
+ image.src = imageSrc;
1071
+ image.alt = 'Preview Image';
1072
+
1073
+ modalBody.appendChild(image);
1074
+ modalContent.append(closeButton, modalBody);
1075
+ overlay.appendChild(modalContent);
1076
+
1077
+ // Append to body
1078
+ document.body.appendChild(overlay);
1079
+
1080
+ overlay.addEventListener('click', (event) => {
1081
+ if (event.target !== image) {
1082
+ overlay.remove();
1083
+ }
1084
+ });
1085
+ // Show modal
1086
+ setTimeout(() => {
1087
+ overlay.classList.add('active');
1088
+ }, 10);
1089
+ }
1090
+
1091
+ const updateImageResizerPosition = (e, core) => {
1092
+ const imageClientRect = core.elements.resizerImage.getBoundingClientRect();
1093
+ core.elements.resizer.style.width = imageClientRect.width + 'px';
1094
+ core.elements.resizer.style.height = imageClientRect.height + 'px';
1095
+ core.elements.resizer.style.top = imageClientRect.top + 'px';
1096
+ core.elements.resizer.style.left = imageClientRect.left + 'px';
1097
+ }
1098
+
1099
+ const destroyImageResizer = (options, core) => {
1100
+ changeAllToolbarState(core, 'enabled')
1101
+ if (core.elements.resizerPlugin) core.elements.toolbarContainer.removeChild(core.elements.resizerPlugin)
1102
+ if (core.elements.resizer) core.elements.iframeWindow.body.removeChild(core.elements.resizer)
1103
+ core.elements.iframeWindow.removeEventListener('scroll', core.resizerHandler, false)
1104
+ core.elements.iframeWindow.removeEventListener('scroll', core.deleteImageHandler, false)
1105
+ core.elements.iframeWindow.removeEventListener('keydown', core.deleteImageHandler, false)
1106
+ delete core.elements.resizer
1107
+ delete core.resizerHandler
1108
+ delete core.deleteImageHandler
1109
+ delete core.elements.resizerImage
1110
+ delete core.elements.resizerPlugin
1111
+ }
1112
+
1113
+ const initTableEditPlugin = (td, options, core) => {
1114
+ destroyTableEditPlugin(options, core)
1115
+ const table = td.closest('table');
1116
+ if (!table) return
1117
+
1118
+ const tableClientRect = table.getBoundingClientRect();
1119
+ let firstX, firstY;
1120
+
1121
+ const handleMouseDown = (e) => {
1122
+ firstX = e.clientX || e.touches[0].clientX;
1123
+ firstY = e.clientY || e.touches[0].clientY;
1124
+
1125
+ core.elements.iframeWindow.addEventListener('mousemove', handleMouseMove)
1126
+ core.elements.iframeWindow.addEventListener('mouseup', handleMouseUp)
1127
+ core.elements.iframeWindow.addEventListener('touchmove', handleMouseMove)
1128
+ core.elements.iframeWindow.addEventListener('touchend', handleMouseUp)
1129
+ }
1130
+
1131
+ const handleMouseMove = (e) => {
1132
+ // Calculate the new dimensions
1133
+ const distanceX = (e.clientX || e.touches[0].clientX) - firstX
1134
+ const distanceY = (e.clientY || e.touches[0].clientY) - firstY
1135
+ const width = tableClientRect.width + distanceX;
1136
+ const height = tableClientRect.height + distanceY;
1137
+
1138
+ table.style.width = width + 'px';
1139
+ table.style.height = height + 'px';
1140
+ }
1141
+
1142
+ const handleMouseUp = (e) => {
1143
+ core.elements.iframeWindow.removeEventListener('mousemove', handleMouseMove, false)
1144
+ core.elements.iframeWindow.removeEventListener('mouseup', handleMouseUp, false)
1145
+ core.elements.iframeWindow.removeEventListener('touchmove', handleMouseMove, false)
1146
+ core.elements.iframeWindow.removeEventListener('touchend', handleMouseUp, false)
1147
+ }
1148
+
1149
+ const updateTable = (event, value) => {
1150
+ const rowIndex = td.parentElement.rowIndex;
1151
+ const cellIndex = td.cellIndex;
1152
+ const colCount = table.rows[0].cells.length; // Number of columns in the table
1153
+
1154
+ const addCellWithBorder = (row, index) => {
1155
+ const newCell = row.insertCell(index);
1156
+ newCell.innerHTML = '\u00A0'; // Add content (empty for now)
1157
+ newCell.style.border = '1px solid black'; // Set the border
1158
+ }
1159
+
1160
+ // Helper function to toggle border between 'none' and '1px solid black'
1161
+ const toggleBorder = (cell, side) => {
1162
+ if (cell) {
1163
+ const currentBorder = cell.style[side];
1164
+ cell.style[side] = ['none', 'medium', 'medium none', '0px none rgb(0, 0, 0)'].includes(currentBorder) ? '1px solid black' : 'none';
1165
+ }
1166
+ };
1167
+
1168
+ // Helper function to check if the table has no rows and remove the table if needed
1169
+ const checkAndRemoveTable = () => {
1170
+ if (table.rows.length === 0) {
1171
+ table.remove();
1172
+ }
1173
+ };
1174
+
1175
+ switch (event) {
1176
+ case 'insert-row-above': {
1177
+ const newRow = table.insertRow(rowIndex); // Insert row above
1178
+ for (let i = 0; i < colCount; i++) {
1179
+ addCellWithBorder(newRow, i); // Add proper <td> cells with border
1180
+ }
1181
+ break;
1182
+ }
1183
+ case 'insert-row-below': {
1184
+ const newRow = table.insertRow(rowIndex + 1); // Insert row below
1185
+ for (let i = 0; i < colCount; i++) {
1186
+ addCellWithBorder(newRow, i); // Add proper <td> cells with border
1187
+ }
1188
+ break;
1189
+ }
1190
+ case 'delete-row': {
1191
+ table.deleteRow(rowIndex); // Delete the row
1192
+ destroyTableEditPlugin(options, core)
1193
+ checkAndRemoveTable()
1194
+ break;
1195
+ }
1196
+ case 'merge-cells-horizontal': {
1197
+ const nextCell = td.nextElementSibling;
1198
+ if (nextCell) {
1199
+ // Check if next cell has data and merge accordingly
1200
+ if (nextCell.innerHTML.trim() !== '') {
1201
+ td.innerHTML += '<br/>' + nextCell.innerHTML; // Add content of next cell with line break
1202
+ }
1203
+ // Adjust colSpan to include the next cell
1204
+ td.colSpan = (td.colSpan || 1) + (nextCell.colSpan || 1); // Merge colSpan correctly
1205
+ nextCell.remove();
1206
+ }
1207
+ break;
1208
+ }
1209
+ case 'insert-column-left': {
1210
+ for (const element of table.rows) {
1211
+ const row = element;
1212
+ addCellWithBorder(row, cellIndex); // Add a new <td> with border to the left
1213
+ }
1214
+ break;
1215
+ }
1216
+ case 'insert-column-right': {
1217
+ for (const element of table.rows) {
1218
+ const row = element;
1219
+ addCellWithBorder(row, cellIndex + 1); // Add a new <td> with border to the right
1220
+ }
1221
+ break;
1222
+ }
1223
+ case 'delete-column': {
1224
+ for (const element of table.rows) {
1225
+ element.deleteCell(cellIndex); // Delete the cell at the current index
1226
+ }
1227
+ checkAndRemoveTable()
1228
+ destroyTableEditPlugin(options, core)
1229
+ break;
1230
+ }
1231
+ case 'merge-cells-vertical': {
1232
+ const nextRow = table.rows[rowIndex + 1];
1233
+ if (nextRow) {
1234
+ const nextCell = nextRow.cells[cellIndex];
1235
+ if (nextCell) {
1236
+ // Check if next cell has data and merge accordingly
1237
+ if (nextCell.innerHTML.trim() !== '') {
1238
+ td.innerHTML += '<br/>' + nextCell.innerHTML; // Add content of next cell with line break
1239
+ }
1240
+ // Adjust rowSpan to include the next row's cell
1241
+ td.rowSpan = (td.rowSpan || 1) + (nextCell.rowSpan || 1); // Merge rowSpan correctly
1242
+ nextCell.remove();
1243
+ }
1244
+ // Check if the row is now empty and remove it if it is
1245
+ const isRowEmpty = Array.from(nextRow.cells).every(cell => cell.innerHTML.trim() === '');
1246
+ if (isRowEmpty) {
1247
+ nextRow.remove();
1248
+ }
1249
+ }
1250
+ break;
1251
+ }
1252
+ case 'cell-border-top': {
1253
+ toggleBorder(td, 'borderTop');
1254
+ if (rowIndex > 0) {
1255
+ const aboveCell = table.rows[rowIndex - 1].cells[cellIndex];
1256
+ toggleBorder(aboveCell, 'borderBottom');
1257
+ }
1258
+ break;
1259
+ }
1260
+ case 'cell-border-right': {
1261
+ toggleBorder(td, 'borderRight');
1262
+ if (cellIndex < table.rows[rowIndex].cells.length - 1) {
1263
+ const rightCell = table.rows[rowIndex].cells[cellIndex + 1];
1264
+ toggleBorder(rightCell, 'borderLeft');
1265
+ }
1266
+ break;
1267
+ }
1268
+ case 'cell-border-bottom': {
1269
+ toggleBorder(td, 'borderBottom');
1270
+ if (rowIndex < table.rows.length - 1) {
1271
+ const belowCell = table.rows[rowIndex + 1].cells[cellIndex];
1272
+ toggleBorder(belowCell, 'borderTop');
1273
+ }
1274
+ break;
1275
+ }
1276
+ case 'cell-border-left': {
1277
+ toggleBorder(td, 'borderLeft');
1278
+ if (cellIndex > 0) {
1279
+ const leftCell = table.rows[rowIndex].cells[cellIndex - 1];
1280
+ toggleBorder(leftCell, 'borderRight');
1281
+ }
1282
+ break;
1283
+ }
1284
+ case 'cell-border-full': {
1285
+ td.style.border = '1px solid black';
1286
+ // Handle top neighbor
1287
+ if (rowIndex > 0) {
1288
+ if (table.rows[rowIndex - 1].cells[cellIndex])
1289
+ table.rows[rowIndex - 1].cells[cellIndex].style.borderBottom = '1px solid black';
1290
+ }
1291
+
1292
+ // Handle bottom neighbor
1293
+ if (rowIndex < table.rows.length - 1) {
1294
+ if (table.rows[rowIndex + 1].cells[cellIndex])
1295
+ table.rows[rowIndex + 1].cells[cellIndex].style.borderTop = '1px solid black';
1296
+ }
1297
+
1298
+ // Handle left neighbor
1299
+ if (cellIndex > 0) {
1300
+ if (table.rows[rowIndex].cells[cellIndex - 1])
1301
+ table.rows[rowIndex].cells[cellIndex - 1].style.borderRight = '1px solid black';
1302
+ }
1303
+
1304
+ // Handle right neighbor
1305
+ if (cellIndex < table.rows[rowIndex].cells.length - 1) {
1306
+ if (table.rows[rowIndex].cells[cellIndex + 1])
1307
+ table.rows[rowIndex].cells[cellIndex + 1].style.borderLeft = '1px solid black';
1308
+ }
1309
+ break;
1310
+ }
1311
+ case 'cell-border-none': {
1312
+ // Remove all borders from the current cell
1313
+ td.style.border = 'none';
1314
+
1315
+ // Handle top neighbor
1316
+ if (rowIndex > 0) {
1317
+ if (table.rows[rowIndex - 1].cells[cellIndex])
1318
+ table.rows[rowIndex - 1].cells[cellIndex].style.borderBottom = 'none';
1319
+ }
1320
+
1321
+ // Handle bottom neighbor
1322
+ if (rowIndex < table.rows.length - 1) {
1323
+ if (table.rows[rowIndex + 1].cells[cellIndex])
1324
+ table.rows[rowIndex + 1].cells[cellIndex].style.borderTop = 'none';
1325
+ }
1326
+
1327
+ // Handle left neighbor
1328
+ if (cellIndex > 0) {
1329
+ if (table.rows[rowIndex].cells[cellIndex - 1])
1330
+ table.rows[rowIndex].cells[cellIndex - 1].style.borderRight = 'none';
1331
+ }
1332
+
1333
+ // Handle right neighbor
1334
+ if (cellIndex < table.rows[rowIndex].cells.length - 1) {
1335
+ if (table.rows[rowIndex].cells[cellIndex + 1])
1336
+ table.rows[rowIndex].cells[cellIndex + 1].style.borderLeft = 'none';
1337
+ }
1338
+ break;
1339
+ }
1340
+ case 'fixed-column-width': {
1341
+ if (table.style.tableLayout === 'fixed') {
1342
+ table.style.tableLayout = 'auto';
1343
+ } else {
1344
+ table.style.tableLayout = 'fixed';
1345
+ table.style.wordBreak = 'break-all'
1346
+ }
1347
+ break;
1348
+ }
1349
+ case 'border-color': {
1350
+ td.style.borderColor = value;
1351
+
1352
+ // Handle top neighbor
1353
+ if (rowIndex > 0) {
1354
+ if (table.rows[rowIndex - 1].cells[cellIndex])
1355
+ table.rows[rowIndex - 1].cells[cellIndex].style.borderBottomColor = value;
1356
+ }
1357
+
1358
+ // Handle bottom neighbor
1359
+ if (rowIndex < table.rows.length - 1) {
1360
+ if (table.rows[rowIndex + 1].cells[cellIndex])
1361
+ table.rows[rowIndex + 1].cells[cellIndex].style.borderTopColor = value;
1362
+ }
1363
+
1364
+ // Handle left neighbor
1365
+ if (cellIndex > 0) {
1366
+ if (table.rows[rowIndex].cells[cellIndex - 1])
1367
+ table.rows[rowIndex].cells[cellIndex - 1].style.borderRightColor = value;
1368
+ }
1369
+
1370
+ // Handle right neighbor
1371
+ if (cellIndex < table.rows[rowIndex].cells.length - 1) {
1372
+ if (table.rows[rowIndex].cells[cellIndex + 1])
1373
+ table.rows[rowIndex].cells[cellIndex + 1].style.borderLeftColor = value;
1374
+ }
1375
+ break;
1376
+ }
1377
+ }
1378
+ }
1379
+
1380
+ makeEditToolbar(options, core, { type: 'td', td, updateTable })
1381
+
1382
+
1383
+ const resizer = document.createElement('div');
1384
+ resizer.className = `table-resizer`
1385
+ resizer.style.width = tableClientRect.width + 'px';
1386
+ resizer.style.height = tableClientRect.height + 'px';
1387
+ resizer.style.top = tableClientRect.top + 'px';
1388
+ resizer.style.left = tableClientRect.left + 'px';
1389
+
1390
+ const resizerButton = document.createElement('button');
1391
+ resizerButton.className = `table-resizer-btn`
1392
+ resizerButton.style.top = tableClientRect.bottom - 6 + 'px';
1393
+ resizerButton.style.left = tableClientRect.right - 6 + 'px';
1394
+ resizerButton.addEventListener('mousedown', handleMouseDown)
1395
+ resizerButton.addEventListener('touchstart', handleMouseDown)
1396
+
1397
+ resizer.appendChild(resizerButton)
1398
+
1399
+ core.elements.table = table
1400
+ core.elements.tableResizer = resizer
1401
+ core.elements.tableResizerBtn = resizerButton
1402
+ core.resizerHandler = (e) => updateTableResizerPosition(options, core);
1403
+ core.elements.iframeWindow.addEventListener('scroll', core.resizerHandler)
1404
+ core.elements.iframeWindow.body.appendChild(resizer)
1405
+ }
1406
+
1407
+ const updateTableResizerPosition = (options, core) => {
1408
+ if (core.elements.tableResizerBtn) {
1409
+ if (!core.elements.editor.contains(core.elements.table)) return destroyTableEditPlugin(options, core)
1410
+ const tableClientRect = core.elements.table.getBoundingClientRect();
1411
+ core.elements.tableResizer.style.width = tableClientRect.width + 'px';
1412
+ core.elements.tableResizer.style.height = tableClientRect.height + 'px';
1413
+ core.elements.tableResizer.style.top = tableClientRect.top + 'px';
1414
+ core.elements.tableResizer.style.left = tableClientRect.left + 'px';
1415
+ core.elements.tableResizerBtn.style.top = tableClientRect.bottom - 6 + 'px';
1416
+ core.elements.tableResizerBtn.style.left = tableClientRect.right - 6 + 'px';
1417
+ }
1418
+ }
1419
+
1420
+ const destroyTableEditPlugin = (options, core) => {
1421
+ if (core.elements.resizerPlugin) core.elements.toolbarContainer.removeChild(core.elements.resizerPlugin)
1422
+ if (core.elements.tableResizer) core.elements.iframeWindow.body.removeChild(core.elements.tableResizer)
1423
+ core.elements.iframeWindow.removeEventListener('scroll', core.resizerHandler, false)
1424
+ delete core.elements.resizerPlugin
1425
+ delete core.resizerHandler
1426
+ delete core.elements.table
1427
+ delete core.elements.tableResizer
1428
+ delete core.elements.tableResizerBtn
1429
+ }
1430
+
1431
+ const changeToolbarStatByPlugin = (plugin, action, value) => {
1432
+ switch (action) {
1433
+ case 'disabled':
1434
+ if (plugin.getAttribute('data-type') === 'button') {
1435
+ plugin.disabled = true
1436
+ plugin.classList.add('disabled')
1437
+ } else if (plugin.getAttribute('data-type') === 'dropdown' || plugin.getAttribute('data-type') === 'select') {
1438
+ plugin.childNodes[0].disabled = true
1439
+ plugin.childNodes[0].classList.add('disabled')
1440
+ } else if (plugin.getAttribute('data-type') === 'color') {
1441
+ plugin.childNodes[0].disabled = true
1442
+ plugin.classList.add('disabled')
1443
+ }
1444
+ break;
1445
+ case 'enabled':
1446
+ if (plugin.getAttribute('data-type') === 'button') {
1447
+ plugin.disabled = false
1448
+ plugin.classList.remove('disabled')
1449
+ } else if (plugin.getAttribute('data-type') === 'dropdown' || plugin.getAttribute('data-type') === 'select') {
1450
+ plugin.childNodes[0].disabled = false
1451
+ plugin.childNodes[0].classList.remove('disabled')
1452
+ } else if (plugin.getAttribute('data-type') === 'color') {
1453
+ plugin.childNodes[0].disabled = false
1454
+ plugin.classList.remove('disabled')
1455
+ }
1456
+ break;
1457
+ case 'active':
1458
+ if (plugin.getAttribute('data-type') === 'button') {
1459
+ plugin.classList.add('active')
1460
+ } else if (plugin.getAttribute('data-type') === 'dropdown' || plugin.getAttribute('data-type') === 'select') {
1461
+ plugin.childNodes[0].classList.add('active')
1462
+ } else if (plugin.getAttribute('data-type') === 'color') {
1463
+ plugin.classList.add('active')
1464
+ }
1465
+ break;
1466
+ case 'inactive':
1467
+ if (plugin.getAttribute('data-type') === 'button') {
1468
+ plugin.classList.remove('active')
1469
+ } else if (plugin.getAttribute('data-type') === 'dropdown' || plugin.getAttribute('data-type') === 'select') {
1470
+ plugin.childNodes[0].classList.remove('active')
1471
+ } else if (plugin.getAttribute('data-type') === 'color') {
1472
+ plugin.classList.remove('active')
1473
+ }
1474
+ break;
1475
+ case 'font':
1476
+ plugin.childNodes[0].childNodes[0].childNodes[0].innerHTML = FONTS[value] ? FONTS[value] : value ? value : 'Font'
1477
+ break;
1478
+ case 'font-size':
1479
+ plugin.childNodes[0].childNodes[0].childNodes[0].innerHTML = FONT_SIZES[value] ? FONT_SIZES[value] : value ? value : 'Font-Size'
1480
+ break;
1481
+ case 'format-block':
1482
+ plugin.childNodes[0].childNodes[0].childNodes[0].innerHTML = FORMATS[value] ?? 'Format'
1483
+ break;
1484
+ case 'font_color':
1485
+ plugin.childNodes[0].value = value || '#000000'
1486
+ break;
1487
+ case 'highlight_color':
1488
+ plugin.childNodes[0].value = value || '#ffffff'
1489
+ break;
1490
+ case 'icon':
1491
+ plugin.innerHTML = value
1492
+ }
1493
+ }
1494
+
1495
+ const changeAllToolbarState = (core, action, preventPlugins = []) => {
1496
+ Object.keys(core.elements.toolbar).forEach(pluginName => {
1497
+ core.elements.toolbar[pluginName].forEach(plugin => {
1498
+ if (preventPlugins.includes(pluginName)) return
1499
+ changeToolbarStatByPlugin(plugin, action)
1500
+ })
1501
+ })
1502
+ }
1503
+
1504
+ const changeToolbarStateByName = (core, action, pluginNames = []) => {
1505
+ Object.keys(core.elements.toolbar).forEach(pluginName => {
1506
+ if (!pluginNames.includes(pluginName)) return
1507
+ core.elements.toolbar[pluginName].forEach(plugin => {
1508
+ changeToolbarStatByPlugin(plugin, action)
1509
+ })
1510
+ })
1511
+ }
1512
+
1513
+ const changeToolbarValueByName = (core, pluginName, value) => {
1514
+ core.elements.toolbar[pluginName]?.forEach(plugin => {
1515
+ changeToolbarStatByPlugin(plugin, pluginName, value)
1516
+ })
1517
+ }
1518
+
1519
+ const changeToolbarHtmlByName = (core, pluginName, value) => {
1520
+ core.elements.toolbar[pluginName]?.forEach(plugin => {
1521
+ changeToolbarStatByPlugin(plugin, 'icon', value)
1522
+ })
1523
+ }
1524
+
1525
+ const rgbToHex = (rgb) => {
1526
+ const rgbArray = rgb.match(/\d+/g); // Extracts the numeric RGB values
1527
+ return `#${rgbArray.map(x => {
1528
+ const hex = parseInt(x).toString(16);
1529
+ return hex.length === 1 ? '0' + hex : hex; // Pads with 0 if needed
1530
+ }).join('')
1531
+ } `.slice(0, 7);
1532
+ };
1533
+
1534
+ const extractSocialMediaId = (url) => {
1535
+ for (const platform in SOCIAL_MEDIA_PATTERNS) {
1536
+ const match = url.match(SOCIAL_MEDIA_PATTERNS[platform]);
1537
+ if (match) {
1538
+
1539
+ return { platform, id: match[1] };
1540
+ }
1541
+ }
1542
+
1543
+ return { platform: null, id: null };
1544
+ };
1545
+
1546
+ const constructEmbedUrl = (platform, id) => {
1547
+ if (!SOCIAL_MEDIA_BASEURLS[platform] || !id) return null;
1548
+
1549
+ switch (platform) {
1550
+ case "INSTAGRAM":
1551
+ case "THREADS":
1552
+ return `${SOCIAL_MEDIA_BASEURLS[platform]}${id}/embed`;
1553
+ case "REDDIT":
1554
+ return `${SOCIAL_MEDIA_BASEURLS[platform]}${id}/?ref_source=embed&ref=share`;
1555
+ default:
1556
+ return `${SOCIAL_MEDIA_BASEURLS[platform]}${id}`;
1557
+ }
1558
+ };
1559
+
1560
+ const showAnchorPopover = (element, x, y, options, core) => {
1561
+ let href = element.getAttribute("href");
1562
+
1563
+ // Create popover container
1564
+ let popover = document.createElement("div");
1565
+ popover.id = "anchor-popover";
1566
+ popover.style.position = "absolute";
1567
+ popover.style.top = `${y + 5}px`;
1568
+ popover.style.left = `${x + 5}px`;
1569
+ popover.style.padding = "8px";
1570
+ popover.style.background = "white";
1571
+ popover.style.border = "1px solid #ccc";
1572
+ popover.style.borderRadius = "6px";
1573
+ popover.style.boxShadow = "0px 4px 8px rgba(0,0,0,0.2)";
1574
+ popover.style.zIndex = "1000";
1575
+ popover.style.display = "flex";
1576
+ popover.style.alignItems = "center";
1577
+ popover.style.gap = "5px";
1578
+ popover.style.whiteSpace = "nowrap";
1579
+ popover.style.maxWidth = "300px";
1580
+
1581
+ // Create clickable link
1582
+ let link = document.createElement("a");
1583
+ link.href = href;
1584
+ link.target = "_blank";
1585
+ link.textContent = href;
1586
+ link.style.textDecoration = "none";
1587
+ link.style.color = "#007bff";
1588
+ link.style.padding = "5px";
1589
+ link.style.overflow = "hidden";
1590
+ link.style.textOverflow = "ellipsis";
1591
+ link.style.webkitBoxOrient = "vertical";
1592
+ link.style.webkitLineClamp = "1"; // Limit to 2 lines
1593
+ link.style.maxWidth = "250px";
1594
+ link.style.wordBreak = "break-word"; // Ensure long URLs wrap properly
1595
+
1596
+ // Create Edit button
1597
+ let editButton = document.createElement("span");
1598
+ editButton.innerHTML = SVG.EDIT_LINK;
1599
+ editButton.style.padding = "5px";
1600
+ editButton.title = "Edit link";
1601
+ editButton.onclick = (event) => editAnchorTag(event, core, options);
1602
+
1603
+ let copyButton = document.createElement("span");
1604
+ copyButton.innerHTML = SVG.COPY_LINK
1605
+ copyButton.style.padding = "5px";
1606
+ copyButton.title = "Copy link";
1607
+ copyButton.onclick = (event) => navigator.clipboard.writeText(href);
1608
+
1609
+ // Create Delete button
1610
+ let deleteButton = document.createElement("span");
1611
+ deleteButton.innerHTML = SVG.DELETE_LINK
1612
+ deleteButton.style.padding = "5px";
1613
+ deleteButton.title = "Remove link"
1614
+ deleteButton.onclick = (event) => removeAnchorTag(event, element, core);
1615
+
1616
+ // Append elements to popover
1617
+ popover.append(link, copyButton, editButton, deleteButton);
1618
+
1619
+ let parent = core.elements.iframeWindow.body; // Change this if needed
1620
+ parent.appendChild(popover);
1621
+
1622
+ requestAnimationFrame(() => {
1623
+ let popoverRect = popover.getBoundingClientRect();
1624
+ let parentRect = parent.getBoundingClientRect(); // Get parent size
1625
+
1626
+ let finalX = x + 5;
1627
+ let finalY = y + 5;
1628
+
1629
+ let spaceRight = parentRect.right - (x + popoverRect.width);
1630
+ let spaceLeft = x - popoverRect.left - popoverRect.width;
1631
+ let spaceBottom = parentRect.bottom - (y + popoverRect.height);
1632
+ let spaceTop = y - parentRect.top - popoverRect.height;
1633
+
1634
+ // Prefer showing below if there's space
1635
+ if (spaceBottom > 0) {
1636
+ finalY = y + 5;
1637
+ }
1638
+ // Otherwise, show above
1639
+ else if (spaceTop > 0) {
1640
+ finalY = y - popoverRect.height - 5;
1641
+ }
1642
+
1643
+ // Prefer showing on the right if there's space
1644
+ if (spaceRight > 0) {
1645
+ finalX = x + 5;
1646
+ }
1647
+ // Otherwise, show on the left
1648
+ else if (spaceLeft > 0) {
1649
+ finalX = x - popoverRect.width - 5;
1650
+ }
1651
+
1652
+ // Ensure the popover stays within the parent’s boundaries
1653
+ finalX = Math.max(parentRect.left + 10, Math.min(finalX, parentRect.right - popoverRect.width - 10));
1654
+ finalY = Math.max(parentRect.top + 10, Math.min(finalY, parentRect.bottom - popoverRect.height - 10));
1655
+
1656
+ // Apply final position
1657
+ popover.style.left = `${finalX - parentRect.left}px`;
1658
+ popover.style.top = `${finalY - parentRect.top}px`;
1659
+ popover.style.visibility = "visible"; // Now show the popover
1660
+ });
1661
+
1662
+ core.elements.anchorPopover = popover;
1663
+
1664
+ const closePopover = (event) => {
1665
+ if (!popover.contains(event.target) || event.key === "Escape") {
1666
+ destroyAnchorPopover(core);
1667
+ document.removeEventListener("click", closePopover);
1668
+ document.removeEventListener("keydown", closePopover);
1669
+ document.removeEventListener("scroll", closePopover, true);
1670
+ }
1671
+ };
1672
+
1673
+ document.addEventListener("click", closePopover);
1674
+ document.addEventListener("keydown", closePopover);
1675
+ document.addEventListener("scroll", closePopover, true);
1676
+ };
1677
+
1678
+ const editAnchorTag = (event, core, options) => {
1679
+ event.stopPropagation();
1680
+
1681
+ if (!core.elements.selectedElement || core.elements.selectedElement.tagName !== "A") {
1682
+ console.warn("No anchor tag selected for editing.");
1683
+ return;
1684
+ }
1685
+
1686
+ const anchorElement = core.elements.selectedElement;
1687
+ let anchorLink = anchorElement.getAttribute("href") || "";
1688
+ let anchorText = anchorElement.innerText || "";
1689
+
1690
+ const body = document.createElement("div");
1691
+
1692
+ // Link Input
1693
+ const inputLink = document.createElement("input");
1694
+ inputLink.value = anchorLink;
1695
+ inputLink.type = "text";
1696
+ inputLink.placeholder = "Insert the link";
1697
+
1698
+ // Text Input
1699
+ const inputText = document.createElement("input");
1700
+ inputText.value = anchorText;
1701
+ inputText.type = "text";
1702
+ inputText.style.marginTop = "8px";
1703
+ inputText.placeholder = "Link text";
1704
+
1705
+ const span = document.createElement("span");
1706
+ span.className = "warning";
1707
+
1708
+ body.append(inputLink, inputText, span);
1709
+
1710
+ // Footer (Buttons)
1711
+ const footer = document.createElement("div");
1712
+ const button = document.createElement("button");
1713
+ button.type = "button";
1714
+ button.className = "submit";
1715
+ button.innerText = "Update";
1716
+
1717
+ footer.append(button);
1718
+
1719
+ // Open Modal
1720
+ const modal = openModal(
1721
+ {
1722
+ title: "Edit Link",
1723
+ bodyNode: body,
1724
+ footerNode: footer,
1725
+ },
1726
+ core,
1727
+ options
1728
+ );
1729
+
1730
+ inputLink.focus();
1731
+
1732
+ // Handle Enter Key Submission
1733
+ const onInputKeydown = (event) => {
1734
+ if (event.key === "Enter") {
1735
+ event.preventDefault();
1736
+ button.click();
1737
+ }
1738
+ };
1739
+
1740
+ inputLink.addEventListener("keydown", onInputKeydown);
1741
+ inputText.addEventListener("keydown", onInputKeydown);
1742
+
1743
+ // Button Click Logic
1744
+ button.onclick = () => {
1745
+ const newLink = inputLink.value.trim();
1746
+ const newText = inputText.value.trim();
1747
+
1748
+ if (REGEX.URL.test(newLink)) {
1749
+ anchorElement.setAttribute("href", newLink);
1750
+ anchorElement.innerText = newText || newLink;
1751
+ modal.close();
1752
+ } else {
1753
+ span.innerText = "Invalid URL";
1754
+ }
1755
+ };
1756
+ destroyAnchorPopover(core);
1757
+ core.updateCaretPosition();
1758
+ };
1759
+
1760
+ const removeAnchorTag = (event, element, core) => {
1761
+ event.stopPropagation();
1762
+ const anchorContent = core.elements.selectedElement.innerHTML;
1763
+ core.elements.selectedElement.outerHTML = anchorContent;
1764
+
1765
+ destroyAnchorPopover(core);
1766
+ };
1767
+
1768
+ const destroyAnchorPopover = (core) => {
1769
+ if (core.elements.anchorPopover) {
1770
+ core.elements.anchorPopover.style.display = "none"
1771
+ delete core.elements.anchorPopover;
1772
+ }
1773
+
1774
+ };
1775
+
1776
+ export {
1777
+ cleanHTML,
1778
+ debounce,
1779
+ makeToolbarButton,
1780
+ makeToolbarDropdown,
1781
+ openModal,
1782
+ initImageResizer,
1783
+ destroyImageResizer,
1784
+ changeAllToolbarState,
1785
+ changeToolbarValueByName,
1786
+ makeToolbarSelect,
1787
+ changeToolbarStateByName,
1788
+ changeToolbarHtmlByName,
1789
+ makeToolbarColor,
1790
+ initTableEditPlugin,
1791
+ destroyTableEditPlugin,
1792
+ updateTableResizerPosition,
1793
+ rgbToHex,
1794
+ extractSocialMediaId,
1795
+ constructEmbedUrl,
1796
+ showAnchorPopover,
1797
+ destroyAnchorPopover,
1798
+ }