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/index.js ADDED
@@ -0,0 +1,696 @@
1
+ import './style.css'
2
+ import { CLASSES, CSS, CSS_VARIABLES, ERRORS, SVG } from "./constant"
3
+ import PLUGINS, { applyOrderList, applyTextFormat } from './plugin';
4
+ import { showAnchorPopover, changeAllToolbarState, changeToolbarStateByName, changeToolbarValueByName, cleanHTML, debounce, destroyImageResizer, destroyTableEditPlugin, initImageResizer, initTableEditPlugin, makeToolbarButton, makeToolbarColor, makeToolbarDropdown, makeToolbarSelect, rgbToHex, updateTableResizerPosition, destroyAnchorPopover, changeToolbarHtmlByName } from './utilities';
5
+
6
+ class LeksyEditor {
7
+
8
+ /**
9
+ * @typedef {Object} EditorOptions
10
+ * @property {String} classPrefix
11
+ * @property {String} placeholder
12
+ * @property {Array<String>} plugins
13
+ * @property {Array<Object>} labels
14
+ * @property {Array<Object>} customPlugins
15
+ * @property {Boolean} spellcheck
16
+ * @property {Boolean} closePluginOnClick
17
+ * @property {String} value
18
+ * @property {Boolean} hideNavigation
19
+ * @property {String} giphyApiKey
20
+ * @property {String} pexelsApiKey
21
+ * @property {String} tenorApiKey
22
+ * @property {Object} cssVariables
23
+ */
24
+ /**
25
+ *
26
+ * @param {HTMLElement} baseElement
27
+ * @param {EditorOptions} options
28
+ */
29
+ static create(baseElement, options = {}) {
30
+ /******************************* validations ******************************* */
31
+ if (!(baseElement instanceof HTMLElement)) throw Error(ERRORS.INVALID_ELEMENT);
32
+
33
+ /****************************** for development **************************** */
34
+ baseElement.innerHTML = ''
35
+
36
+ /******************************* initializing ****************************** */
37
+ if (!options.classPrefix) options.classPrefix = CLASSES.PREFIX
38
+ if (!options.placeholder) options.placeholder = ''
39
+ if (options.closePluginOnClick !== false) options.closePluginOnClick = true
40
+ /**
41
+ * For Internal Use
42
+ */
43
+ const core = {
44
+ elements: {
45
+ base: baseElement,
46
+ toolbar: {},
47
+ },
48
+ state: { isCodeViewOpen: false, page: 1, totalPages: null, next: null },
49
+ html: options.value || '<div><br></div>',
50
+ history: {
51
+ stack: [],
52
+ currentIndex: -1, // Keeps track of the current position in the history stack
53
+ push: debounce(() => { core.history.saveHistory() }, 300),
54
+ saveHistory: () => {
55
+ // Save the caret position using the selection range
56
+ const selection = core.elements.iframeWindow.getSelection();
57
+ let caretOffset = null;
58
+
59
+ if (selection.rangeCount > 0) {
60
+ const range = selection.getRangeAt(0);
61
+ const preCaretRange = range.cloneRange();
62
+ preCaretRange.selectNodeContents(core.elements.editor);
63
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
64
+ caretOffset = preCaretRange.toString().length; // Save the character offset of the caret
65
+ }
66
+ const html = core.elements.editor.innerHTML;
67
+
68
+ if (core.history.stack[core.history.currentIndex]?.html !== html) {
69
+ // If new state is pushed, remove redo states if present
70
+ core.history.stack = core.history.stack.slice(0, core.history.currentIndex + 1);
71
+ core.history.stack.push({ caretOffset, html });
72
+ core.history.currentIndex++;
73
+ }
74
+ },
75
+ undo: () => {
76
+ if (core.history.currentIndex > 0) {
77
+ core.history.currentIndex--;
78
+
79
+ const { html, caretOffset } = core.history.stack[core.history.currentIndex];
80
+ core.elements.editor.innerHTML = html;
81
+ core.history.restoreCaretPosition(caretOffset);
82
+ }
83
+ },
84
+ redo: () => {
85
+ if (core.history.currentIndex < core.history.stack.length - 1) {
86
+ core.history.currentIndex++;
87
+
88
+ const { html, caretOffset } = core.history.stack[core.history.currentIndex];
89
+ core.elements.editor.innerHTML = html;
90
+ core.history.restoreCaretPosition(caretOffset);
91
+ }
92
+ },
93
+ restoreCaretPosition: (caretOffset) => {
94
+ const selection = core.elements.iframeWindow.getSelection();
95
+ const range = document.createRange();
96
+ let charCount = 0;
97
+
98
+ function setCaret(node) {
99
+ if (node.nodeType === 3) { // Text node
100
+ const length = node.length;
101
+ if (charCount + length >= caretOffset) {
102
+ range.setStart(node, caretOffset - charCount);
103
+ range.setEnd(node, caretOffset - charCount);
104
+ selection.removeAllRanges();
105
+ selection.addRange(range);
106
+ return true;
107
+ } else {
108
+ charCount += length;
109
+ }
110
+ } else if (node.nodeType === 1) { // Element node
111
+ for (const element of node.childNodes) {
112
+ if (setCaret(element)) {
113
+ return true;
114
+ }
115
+ }
116
+ }
117
+ return false;
118
+ }
119
+ setCaret(core.elements.editor);
120
+ }
121
+ },
122
+ onChange: (html = core.element.editor.innerHTML) => {
123
+ updateTableResizerPosition(options, core)
124
+ if (html === '<br>' || html === '<div><br></div>') html = ''
125
+ core.html = html;
126
+ if (editorRef.onChange instanceof Function) editorRef.onChange(html)
127
+ if (html.trim()) core.hidePlaceholder()
128
+ else core.showPlaceholder()
129
+ core.history.push()
130
+ },
131
+ onBlur: (html) => {
132
+ if (editorRef.onBlur instanceof Function) editorRef.onBlur(html)
133
+ },
134
+ onAttachment: (files) => {
135
+ if (editorRef.onAttachment instanceof Function) editorRef.onAttachment(files)
136
+ },
137
+ manuplateImage: async (type, content) => {
138
+ if (editorRef.manuplateImage instanceof Function) {
139
+ core.changeStatus('Loading...')
140
+ const _content = (await editorRef.manuplateImage(type, content)) ?? content
141
+ core.changeStatus('')
142
+ return _content
143
+ }
144
+ return content
145
+ },
146
+ updateBreadcumb: (parentTags) => {
147
+ if (!options.hideNavigation && Array.isArray(parentTags)) {
148
+ core.elements.breadcumb.innerHTML = ''
149
+ parentTags.forEach(tag => {
150
+ const li = document.createElement('li');
151
+ li.innerText = tag;
152
+ core.elements.breadcumb.appendChild(li)
153
+ })
154
+ }
155
+ },
156
+ changeStatus: (text) => {
157
+ if (!options.hideNavigation) {
158
+ core.elements.status.innerText = text
159
+ }
160
+ },
161
+ insertNode: (node) => {
162
+ if (core.state.range) {
163
+ core.state.range.deleteContents();
164
+ core.state.range.insertNode(node);
165
+ //cursor at the last with this
166
+ core.state.range.collapse(false);
167
+ core.state.selection.removeAllRanges();
168
+ core.state.selection.addRange(core.state.range);
169
+ } else {
170
+ core.elements.editor.appendChild(node);
171
+ }
172
+ },
173
+ uploadVideo: async (file) => {
174
+ if (editorRef.uploadVideo instanceof Function) {
175
+ core.changeStatus('Loading...')
176
+ changeToolbarHtmlByName(core, 'video', SVG.LOADER)
177
+ const url = await editorRef.uploadVideo(file)
178
+ core.changeStatus('')
179
+ changeToolbarHtmlByName(core, 'video', SVG.VIDEO)
180
+ return url
181
+ }
182
+ },
183
+ showPlaceholder: () => {
184
+ core.elements.placeholder.style.display = 'block'
185
+ },
186
+ hidePlaceholder: () => {
187
+ core.elements.placeholder.style.display = 'none'
188
+ },
189
+ updateCaretPosition: () => {
190
+ const selection = core.elements.iframeWindow.getSelection();
191
+ let parentTags = [];
192
+ let parentElements = [];
193
+ if (selection?.rangeCount !== 0) {
194
+ const range = selection.getRangeAt(0);
195
+ core.state.selection = selection;
196
+ core.state.range = range;
197
+ core.state.parentNode = range.commonAncestorContainer.parentNode;
198
+
199
+ let element = range.commonAncestorContainer;
200
+ // If the parent is a text node, get the parent element
201
+ if (element.nodeType === Node.TEXT_NODE) {
202
+ element = element.parentNode;
203
+ }
204
+
205
+ while (element && element !== core.elements.editor) {
206
+ parentElements.push(element)
207
+ parentTags.push(element.tagName);
208
+ element = element.parentElement;
209
+ }
210
+ parentTags = parentTags.reverse()
211
+ parentElements = parentElements.reverse()
212
+ core.updateBreadcumb(parentTags)
213
+ }
214
+ return { parentTags, parentElements }
215
+ },
216
+ }
217
+
218
+ /**
219
+ * For External Use
220
+ */
221
+ const editorRef = {
222
+ setContents: (html) => {
223
+ core.html = html
224
+ core.elements.editor.innerHTML = html
225
+ },
226
+ getContents: () => core.html,
227
+ onChange: () => { },
228
+ onBlur: () => { },
229
+ onAttachment: () => { },
230
+ manuplateImage: async () => { },
231
+ uploadVideo: async () => { },
232
+ focus: () => { core.elements.editor.focus() },
233
+ getCore: () => core,
234
+ }
235
+
236
+ core.elements.base.className = `${options.classPrefix}${CLASSES.CONTAINER}`
237
+
238
+ if (Array.isArray(options.plugins) && options.plugins.length) this.#initToolbar(options, core)
239
+
240
+ this.#initPlaceholder(options, core)
241
+ this.#initCodeviewContainer(options, core);
242
+ this.#initEditableContainer(options, core);
243
+ if (!options.hideNavigation) this.#initStepper(options, core);
244
+
245
+ if (options.value) core.hidePlaceholder()
246
+ core.history.push()
247
+
248
+ return editorRef
249
+ }
250
+
251
+ static #initToolbar(options, core) {
252
+ const toolbarContainer = document.createElement('div')
253
+ toolbarContainer.className = `${options.classPrefix}${CLASSES.TOOLBAR}`
254
+
255
+ /**
256
+ * @param {String} plugin
257
+ * @returns {HTMLElement}
258
+ */
259
+ const initToolbarItems = (plugin) => {
260
+ /************************* to creating button sets ************************* */
261
+ if (Array.isArray(plugin)) {
262
+ const buttonSets = document.createElement('div');
263
+ buttonSets.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEMS}`
264
+ plugin.forEach(plugin => {
265
+ const button = initToolbarItems(plugin)
266
+ buttonSets.appendChild(button)
267
+ })
268
+ return buttonSets
269
+ }
270
+
271
+ const _plugin = PLUGINS[plugin] ?? options.customPlugins?.[plugin];
272
+ /******************************* validations ******************************* */
273
+ if (!_plugin) throw Error(ERRORS.INVALID_PLUGIN);
274
+ if (_plugin.title === 'GIPHY' && !options.giphyApiKey) throw Error(ERRORS.GIPHY_KEY_NOT_FOUND);
275
+ if (_plugin.title === 'Pexels' && !options.pexelsApiKey) throw Error(ERRORS.PEXELS_KEY_NOT_FOUND);
276
+ if (_plugin.title === 'Tenor' && !options.tenorApiKey) throw Error(ERRORS.TENOR_KEY_NOT_FOUND);
277
+
278
+ let toolbarPlugin
279
+ if (_plugin.type === 'button') {
280
+ toolbarPlugin = makeToolbarButton(_plugin, options, core)
281
+ } else if (_plugin.type === 'dropdown' || _plugin.type === 'gallery' || _plugin.type === 'category' || _plugin.type === 'mention' || _plugin.type === 'table') {
282
+ toolbarPlugin = makeToolbarDropdown(_plugin, options, core)
283
+ } else if (_plugin.type === 'select') {
284
+ toolbarPlugin = makeToolbarSelect(_plugin, options, core)
285
+ } else if (_plugin.type === 'color') {
286
+ toolbarPlugin = makeToolbarColor(_plugin, options, core)
287
+ }
288
+ if (core.elements.toolbar[plugin])
289
+ core.elements.toolbar[plugin].push(toolbarPlugin)
290
+ else
291
+ core.elements.toolbar[plugin] = [toolbarPlugin]
292
+
293
+ return toolbarPlugin
294
+ }
295
+
296
+ options.plugins.forEach(plugin => {
297
+ const toolbarButtonSets = initToolbarItems(plugin)
298
+ toolbarContainer.appendChild(toolbarButtonSets)
299
+ })
300
+
301
+ core.elements.toolbarContainer = toolbarContainer;
302
+ core.elements.base.appendChild(toolbarContainer);
303
+ }
304
+
305
+ static #initPlaceholder(options, core) {
306
+ const placeholderContainer = document.createElement('div');
307
+ const placeholder = document.createElement('span');
308
+ placeholder.className = `${options.classPrefix}${CLASSES.PLACEHOLDER}`
309
+ placeholder.innerText = options.placeholder ?? ''
310
+ placeholderContainer.appendChild(placeholder)
311
+ placeholder.onclick = () => core.elements.editor.focus()
312
+ core.elements.placeholder = placeholder
313
+ core.elements.base.appendChild(placeholderContainer)
314
+ }
315
+
316
+ static #initCodeviewContainer(options, core) {
317
+ const textarea = document.createElement('textarea');
318
+ textarea.className = `${options.classPrefix}${CLASSES.CODEVIEW}`
319
+ textarea.spellcheck = false
320
+ core.elements.codeview = textarea
321
+ core.elements.base.appendChild(textarea)
322
+ }
323
+
324
+ static #initEditableContainer(options, core) {
325
+ const makeOrderedList = () => {
326
+ const textNode = core.state.range.startContainer;
327
+ if (textNode.nodeType === Node.TEXT_NODE) {
328
+ // Get the text typed so far
329
+ let text = textNode.nodeValue.replace(/\u00A0/g, ' ');
330
+
331
+ // Detect "1. " pattern
332
+ if (text === "1. ") {
333
+ text = text.slice(0, -3);
334
+ textNode.nodeValue = text;
335
+
336
+ // Update the caret position
337
+ const newRange = document.createRange();
338
+ newRange.setStart(textNode, text.length);
339
+ newRange.setEnd(textNode, text.length);
340
+ core.state.selection.removeAllRanges();
341
+ core.state.selection.addRange(newRange);
342
+ applyOrderList(core)
343
+ }
344
+ }
345
+
346
+ }
347
+ const handleCaretPositionAndPlugin = () => {
348
+ const { parentTags, parentElements } = core.updateCaretPosition()
349
+ makeOrderedList()
350
+
351
+ changeAllToolbarState(core, 'inactive', [])
352
+ const activePlugin = []
353
+ if (parentTags.includes('A')) activePlugin.push('link')
354
+ if (core.elements.iframeWindow.queryCommandState('bold')) activePlugin.push('bold')
355
+ if (core.elements.iframeWindow.queryCommandState('underline')) activePlugin.push('underline')
356
+ if (core.elements.iframeWindow.queryCommandState('italic')) activePlugin.push('italic')
357
+ if (core.elements.iframeWindow.queryCommandState('strikeThrough')) activePlugin.push('strike')
358
+ if (core.elements.iframeWindow.queryCommandState('subscript')) activePlugin.push('subscript')
359
+ if (core.elements.iframeWindow.queryCommandState('superscript')) activePlugin.push('superscript')
360
+ if (core.elements.iframeWindow.queryCommandState('insertOrderedList')) activePlugin.push('ordered_list')
361
+ if (core.elements.iframeWindow.queryCommandState('insertUnorderedList')) activePlugin.push('unordered_list')
362
+
363
+ let format = ''
364
+ if (parentTags.includes('H1')) format = 'h1'
365
+ if (parentTags.includes('H2')) format = 'h2'
366
+ if (parentTags.includes('H3')) format = 'h3'
367
+ if (parentTags.includes('H4')) format = 'h4'
368
+ if (parentTags.includes('H5')) format = 'h5'
369
+ if (parentTags.includes('H6')) format = 'h6'
370
+ if (parentTags.includes('P')) format = 'P'
371
+ changeToolbarValueByName(core, 'format-block', format)
372
+
373
+ let selectedNode = core.state.range.commonAncestorContainer;
374
+ if (core.state.range) {
375
+
376
+ // Traverse up to find the element node if selectedNode is a text node
377
+ if (selectedNode.nodeType === Node.TEXT_NODE) {
378
+ selectedNode = selectedNode.parentNode;
379
+ }
380
+
381
+ // Get computed styles for the element where the text is contained
382
+ const computedStyle = window.getComputedStyle(selectedNode);
383
+ const align = computedStyle.textAlign;
384
+ if (align) {
385
+ if (align === 'justify')
386
+ activePlugin.push('align_justify')
387
+ else if (align === 'start' || align === 'left' || align === '-moz-left')
388
+ activePlugin.push('align_left')
389
+ else if (align === 'end' || align === 'right' || align === '-moz-right')
390
+ activePlugin.push('align_right')
391
+ else if (align === 'center' || align === '-moz-center')
392
+ activePlugin.push('align_center')
393
+ }
394
+
395
+ const styles = window.getComputedStyle(selectedNode);
396
+ changeToolbarValueByName(core, 'highlight_color', rgbToHex(styles.backgroundColor))
397
+ }
398
+
399
+ changeToolbarStateByName(core, 'active', activePlugin)
400
+ const styles = window.getComputedStyle(selectedNode);
401
+
402
+ const font = parentElements.find(element => element.tagName === 'FONT')
403
+
404
+ if (font && font.face) {
405
+ changeToolbarValueByName(core, 'font', font.face)
406
+ } else if (styles.fontFamily) {
407
+ const firstFont = styles.fontFamily.split(",")[0].trim().replace(/['"]/g, "");;
408
+ const formattedFont = styles.fontFamily.includes(",") ? `${firstFont}...` : firstFont;
409
+ changeToolbarValueByName(core, 'font', `<p style='user-select: none;'>${formattedFont}</p>`)
410
+ } else {
411
+ changeToolbarValueByName(core, 'font', '')
412
+ }
413
+
414
+ if (font && font.size) {
415
+ changeToolbarValueByName(core, 'font-size', font.size)
416
+ } else if (styles.fontSize) {
417
+ const match = styles.fontSize.match(/^([\d.]+)([a-z%]*)$/);
418
+ const numericValue = parseFloat(match[1]).toFixed(2);
419
+ const unit = match[2];
420
+ const fontSize = numericValue + unit;
421
+ changeToolbarValueByName(core, 'font-size', `<p style='user-select: none;'>${fontSize}</p>`)
422
+ } else {
423
+ changeToolbarValueByName(core, 'font-size', '')
424
+ }
425
+
426
+ if (font && font.color) {
427
+ changeToolbarValueByName(core, 'font_color', font.color)
428
+ } else if (styles.color) {
429
+ changeToolbarValueByName(core, 'font_color', rgbToHex(styles.color))
430
+ } else {
431
+ changeToolbarValueByName(core, 'font_color', '')
432
+ }
433
+
434
+ }
435
+ const keyPressDebounce = debounce(handleCaretPositionAndPlugin, 100)
436
+ const makeObserver = () => {
437
+ const observer = new MutationObserver(() => {
438
+ // Handle mutation and push state to undo stack
439
+ const inputEvent = new Event('custom-input', { bubbles: true });
440
+ contentEditableDiv.dispatchEvent(inputEvent);
441
+ });
442
+
443
+ // Start observing the editor for attribute changes (like resizing)
444
+ observer.observe(contentEditableDiv, {
445
+ attributes: true,
446
+ childList: true,
447
+ subtree: true
448
+ });
449
+ }
450
+
451
+ /***************************** for css theme variables ********************************* */
452
+
453
+ if (options.cssVariables) {
454
+ const cssVariable = options.cssVariables;
455
+ Object.keys(CSS_VARIABLES).forEach((key) => {
456
+ if (cssVariable[key] !== undefined) {
457
+ CSS_VARIABLES[key] = cssVariable[key];
458
+ }
459
+ });
460
+
461
+ CSS.IFRAME_VARIABLES = `
462
+ :root {
463
+ --primary: ${CSS_VARIABLES.primary};
464
+ --white-mid-darker: ${CSS_VARIABLES.midDarker};
465
+ --base-white: ${CSS_VARIABLES.baseWhite};
466
+ --base-white-dark: ${CSS_VARIABLES.whiteDark};
467
+ --shadow: ${CSS_VARIABLES.shadow};
468
+ --resizer: ${CSS_VARIABLES.resizer};
469
+ --resizer-background: ${CSS_VARIABLES.resiserBackground};
470
+ --mention: ${CSS_VARIABLES.mention};
471
+ --mention-highlight: ${CSS_VARIABLES.mentionHighlight};
472
+ --dashed-border: ${CSS_VARIABLES.dashedBorder};
473
+ --table-resizer: ${CSS_VARIABLES.tableResizer};
474
+ --table-resizer-outline: ${CSS_VARIABLES.tableResizerOutline};
475
+ --text-color: ${CSS_VARIABLES.textColor};
476
+ --resizer-position: ${CSS_VARIABLES.resizerPosition};
477
+ --resizer-position-background: ${CSS_VARIABLES.resiserPositionBackground};
478
+ }
479
+ `;
480
+ }
481
+
482
+ const iframe = document.createElement('iframe');
483
+ iframe.style.flex = '1 1 auto';
484
+ iframe.style.height = '250px'
485
+ iframe.style.border = '0';
486
+ core.elements.base.appendChild(iframe)
487
+
488
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
489
+ // Write the basic HTML structure to the iframe's document
490
+ iframeDoc.open();
491
+
492
+ iframeDoc.write(`
493
+ <!DOCTYPE html>
494
+ <html lang='en'>
495
+ <head>
496
+ <style>
497
+ ${CSS.IFRAME_VARIABLES}
498
+ ${CSS.IFRAME_BODY}
499
+ ${CSS.IFRAME_TRIBUTE}
500
+ ${CSS.IFRAME_EDITOR}
501
+ ${CSS.IFRAME_RESIZER}
502
+ </style>
503
+ </head>
504
+ <body></body>
505
+ </html>
506
+ `);
507
+
508
+ iframeDoc.close();
509
+
510
+ iframeDoc.onkeydown = (event) => {
511
+ // Check if Ctrl or Cmd key is pressed
512
+ const isCtrlOrCmd = event.ctrlKey || event.metaKey;
513
+
514
+ // Check for Ctrl + Z (undo) or Ctrl + Y (redo)
515
+ if (isCtrlOrCmd && ['z', 'y', 'b', 'i', 'u'].includes(event.key)) {
516
+ return event.preventDefault(); // Prevent the default undo/redo action
517
+ }
518
+ }
519
+
520
+ const contentEditableDiv = iframeDoc.createElement('div');
521
+ contentEditableDiv.contentEditable = true;
522
+ contentEditableDiv.spellcheck = !!options.spellcheck
523
+ contentEditableDiv.innerHTML = core.html;
524
+ contentEditableDiv.className = 'content-editable';
525
+
526
+ contentEditableDiv.oninput = (e) => {
527
+ core.onChange(e.target.innerHTML)
528
+ }
529
+ contentEditableDiv.onbeforeinput = (e) => {
530
+ if (e.target.innerHTML === '<br>' || e.target.innerHTML === '') {
531
+ e.target.innerHTML = '<div><br></div>'
532
+ }
533
+ }
534
+ contentEditableDiv.addEventListener('custom-input', (e) => {
535
+ core.onChange(e.target.innerHTML)
536
+ })
537
+ contentEditableDiv.onkeydown = (event) => {
538
+ // Check if Ctrl or Cmd key is pressed
539
+ const isCtrlOrCmd = event.ctrlKey || event.metaKey;
540
+
541
+ if (isCtrlOrCmd) {
542
+ if (event.key === 'z') {
543
+ core.history.undo()
544
+ } else if (event.key === 'y') {
545
+ core.history.redo()
546
+ } else if (event.key === 'b') {
547
+ applyTextFormat(core, 'bold')
548
+ } else if (event.key === 'u') {
549
+ applyTextFormat(core, 'underline')
550
+ } else if (event.key === 'i') {
551
+ applyTextFormat(core, 'italic')
552
+ }
553
+ }
554
+
555
+ if (event.key === 'Tab') {
556
+ event.preventDefault();
557
+
558
+ const tabNode = document.createTextNode("\u00a0\u00a0\u00a0\u00a0");
559
+ core.state.range.insertNode(tabNode);
560
+ core.state.range.setStartAfter(tabNode);
561
+ core.state.range.setEndAfter(tabNode);
562
+ core.state.selection.removeAllRanges();
563
+ core.state.selection.addRange(core.state.range);
564
+ }
565
+ keyPressDebounce()
566
+ }
567
+ contentEditableDiv.onclick = (e) => {
568
+ handleCaretPositionAndPlugin()
569
+ let element = e.target; // The clicked element
570
+
571
+ core.elements.selectedElement = e.target;
572
+
573
+ destroyImageResizer(options, core)
574
+ destroyTableEditPlugin(options, core)
575
+ destroyAnchorPopover(core)
576
+
577
+ if (element.tagName === "A") showAnchorPopover(e.target, e.pageX, e.pageY, options, core)
578
+ if (element.tagName === 'IMG') initImageResizer('image', e.target, options, core)
579
+ if (element.tagName === 'TD') initTableEditPlugin(e.target, options, core)
580
+ if (element.tagName === "FIGURE") initImageResizer('figure', e.target, options, core)
581
+ }
582
+ contentEditableDiv.onpaste = (e) => {
583
+ e.preventDefault();
584
+ const clipboardData = e.clipboardData;
585
+
586
+ if (clipboardData.items) {
587
+ for (let item of clipboardData.items) {
588
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
589
+ const file = item.getAsFile();
590
+ const reader = new FileReader();
591
+ reader.onload = async function (event) {
592
+ const img = document.createElement('img');
593
+ const base64String = event.target.result;
594
+ img.src = await core.manuplateImage('base64', base64String);
595
+ core.insertNode(img);
596
+ };
597
+ reader.readAsDataURL(file);
598
+ return;
599
+ }
600
+ }
601
+ }
602
+
603
+ const pastedData = clipboardData.getData('text/html') || clipboardData.getData('text/plain');
604
+ core.elements.iframeWindow.execCommand('insertHtml', false, cleanHTML(pastedData))
605
+ };
606
+ contentEditableDiv.ondrop = (e) => {
607
+ e.preventDefault();
608
+ const droppedData = e.dataTransfer.getData('text/html') || e.dataTransfer.getData('text/plain');
609
+ core.elements.iframeWindow.execCommand('insertHtml', false, cleanHTML(droppedData))
610
+ };
611
+ contentEditableDiv.onblur = (e) => {
612
+ core.onBlur(e.target.innerHTML)
613
+ }
614
+
615
+ iframeDoc.body.appendChild(contentEditableDiv);
616
+
617
+ if (options.labels?.length) {
618
+ const script = iframeDoc.createElement('script');
619
+ script.src = "https://cdnjs.cloudflare.com/ajax/libs/tributejs/5.1.3/tribute.js";
620
+
621
+ script.onload = () => {
622
+ const tribute = new iframe.contentWindow.Tribute({
623
+ values: options.labels.flatMap(category => {
624
+ const fields = [
625
+ { key: category.name, isCategory: true },
626
+ ...category.fields.map(field => ({
627
+ key: field.name,
628
+ value: field.value,
629
+ }))
630
+ ];
631
+
632
+ return fields
633
+ }),
634
+ noMatchTemplate: () => '<li>No match found</li>',
635
+ menuItemTemplate: (item) => {
636
+ if (item.original.isCategory) return `<div class="category">${item.original.key}</div>`;
637
+ return `<div class="item">${item.original.key}</div>`;
638
+ },
639
+ containerClass: 'tribute',
640
+ selectTemplate: function (item) {
641
+ if (item.original.isCategory) return null;
642
+ return `<span spellcheck="false" contentEditable="false">${item.original.key}</span>`;
643
+ },
644
+ });
645
+ tribute.attach(contentEditableDiv);
646
+ makeObserver()
647
+ };
648
+ iframeDoc.body.appendChild(script);
649
+ } else {
650
+ makeObserver()
651
+ }
652
+
653
+ core.elements.editor = contentEditableDiv;
654
+ core.elements.iframeWindow = iframeDoc;
655
+ core.elements.iframeContainer = iframe;
656
+ }
657
+
658
+ static #initStepper(options, core) {
659
+ const stepper = document.createElement('div');
660
+ stepper.className = `${options.classPrefix}${CLASSES.STEPPER}`;
661
+
662
+ // Create the <ul> element for the breadcrumb
663
+ const breadcrumbList = document.createElement('ul');
664
+ breadcrumbList.className = 'breadcrumb';
665
+
666
+ const status = document.createElement('span');
667
+
668
+ stepper.appendChild(breadcrumbList)
669
+ stepper.appendChild(status)
670
+
671
+ core.elements.base.appendChild(stepper);
672
+ core.elements.breadcumb = breadcrumbList
673
+ core.elements.status = status
674
+ core.elements.stepper = stepper
675
+ }
676
+
677
+ }
678
+
679
+ const convertHtmlToText = (html) => {
680
+ const div = document.createElement('div');
681
+ div.innerHTML = html;
682
+ return div.textContent
683
+ }
684
+
685
+ const isHTMLEmpty = (html) => {
686
+ const tempElement = document.createElement('div');
687
+ tempElement.innerHTML = html
688
+
689
+ return !tempElement.innerText.trim() && !tempElement.querySelector('img');
690
+ }
691
+
692
+ export default LeksyEditor
693
+ export {
694
+ convertHtmlToText,
695
+ isHTMLEmpty,
696
+ }