leksy-editor 2.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js DELETED
@@ -1,1028 +0,0 @@
1
- import './style.css'
2
- import { CLASSES, CSS, CSS_VARIABLES, ERRORS, REGEX, SVG } from "./constant"
3
- import PLUGINS, { applyTextFormat } from './plugin';
4
- import { showAnchorPopover, changeAllToolbarState, changeToolbarStateByName, changeToolbarValueByName, cleanHTML, debounce, destroyImageResizer, destroyTableEditPlugin, initImageResizer, initTableEditPlugin, makeToolbarButton, makeToolbarColor, makeToolbarDropdown, makeToolbarSelect, rgbToHex, updateTableResizerPosition, destroyAnchorPopover, changeToolbarHtmlByName, showRemoteCursor, syncRemoteChangesDebounce, applyRemoteChanges, updateCursorPositionDebounce, buildTributeValues, updateHeight, getTableGrid, getCellPosition, makeUnorderedList, makeOrderedList, makeHeading, makeBlockQuote, makeStrikethrough, makeCodeBlock, makeSublist, showTooltip, makeToolbarButtonSelect, navigateToHeading, updateOutlineItems, highlightActiveOutline, findAndReplace, handleBackspaceInList, trackListSelectionDeletion, cleanupListSelectionDeletion } from './utilities';
5
- import { initTabs, findTab, renderTabs, prepareTabs } from './tab';
6
- import { v4 as uuidv4 } from 'uuid';
7
-
8
- class LeksyEditor {
9
-
10
- /**
11
- * @typedef {Object} EditorOptions
12
- * @property {String} classPrefix
13
- * @property {String} placeholder
14
- * @property {Array<String>} plugins
15
- * @property {Array<Object>} labels
16
- * @property {Array<Object>} customPlugins
17
- * @property {Boolean} spellcheck
18
- * @property {Boolean} closePluginOnClick
19
- * @property {Array<Object>} value
20
- * @property {Boolean} hideNavigation
21
- * @property {String} giphyApiKey
22
- * @property {String} pexelsApiKey
23
- * @property {String} tenorApiKey
24
- * @property {Object} cssVariables
25
- * @property {Boolean} disablePastedColorStyles
26
- * @property {Boolean} removeColorsWhilePasting
27
- * @property {Boolean} autoHeight
28
- * @property {String} height
29
- * @property {String} maxHeight
30
- * @property {String} minHeight
31
- * @property {String} iframeStyle
32
- * @property {Boolean} disabled
33
- * @property {Function} onFullScreen
34
- * @property {Boolean} showTabs
35
- * @property {Boolean} enableFindAndReplace
36
- * @property {Array<Object>} customTooltip
37
- * @property {Array<Object>} customIcon
38
- */
39
- /**
40
- *
41
- * @param {HTMLElement} baseElement
42
- * @param {EditorOptions} options
43
- */
44
- static create(baseElement, options = {}) {
45
- /******************************* validations ******************************* */
46
- if (!(baseElement instanceof HTMLElement)) throw Error(ERRORS.INVALID_ELEMENT);
47
-
48
- /****************************** for development **************************** */
49
- baseElement.innerHTML = ''
50
-
51
- /******************************* initializing ****************************** */
52
- if (!options.classPrefix) options.classPrefix = CLASSES.PREFIX
53
- if (!options.placeholder) options.placeholder = ''
54
- if (options.closePluginOnClick !== false) options.closePluginOnClick = true
55
-
56
- // Initialize Tabs Data
57
- let initialTabs = prepareTabs(options.value)
58
- const initialCurrentTabId = initialTabs[0].tabId;
59
-
60
- /**
61
- * For Internal Use
62
- */
63
- const core = {
64
- elements: {
65
- base: baseElement,
66
- toolbar: {},
67
- tabsContainer: null,
68
- tabs: {},
69
- },
70
- state: { isCodeViewOpen: false, page: 1, totalPages: null, next: null, tribute: null, tabs: initialTabs, currentTabId: initialCurrentTabId },
71
- html: initialTabs[0].content,
72
- cursor: {
73
- startIndex: 0,
74
- endIndex: 0,
75
- cursorIndex: 0,
76
- isCollapsed: false,
77
- },
78
- history: {
79
- tabs: {
80
- [initialCurrentTabId]: {
81
- stack: [],
82
- currentIndex: -1 // Keeps track of the current position in the history stack
83
- }
84
- },
85
- push: debounce(() => { core.history.saveHistory() }, 300),
86
- saveHistory: () => {
87
- // Save the caret position using the selection range
88
- const selection = core.elements.iframeWindow.getSelection();
89
- let caretOffset = null;
90
-
91
- if (selection?.rangeCount > 0) {
92
- const range = selection.getRangeAt(0);
93
- const preCaretRange = range.cloneRange();
94
- preCaretRange.selectNodeContents(core.elements.editor);
95
- preCaretRange.setEnd(range.endContainer, range.endOffset);
96
- caretOffset = preCaretRange.toString().length; // Save the character offset of the caret
97
- }
98
- const html = core.elements.editor.innerHTML;
99
-
100
- const currentTabHistory = core.history.tabs[core.state.currentTabId] || { stack: [], currentIndex: -1 };
101
- if (!core.history.tabs[core.state.currentTabId]) core.history.tabs[core.state.currentTabId] = currentTabHistory;
102
-
103
- if (currentTabHistory.stack[currentTabHistory.currentIndex]?.html !== html) {
104
- // If new state is pushed, remove redo states if present
105
- currentTabHistory.stack = currentTabHistory.stack.slice(0, currentTabHistory.currentIndex + 1);
106
- currentTabHistory.stack.push({ caretOffset, html });
107
- currentTabHistory.currentIndex++;
108
- }
109
- },
110
- undo: () => {
111
- const currentTabHistory = core.history.tabs[core.state.currentTabId];
112
- if (!currentTabHistory) return;
113
-
114
- if (currentTabHistory.currentIndex > 0) {
115
- currentTabHistory.currentIndex--;
116
-
117
- const { html, caretOffset } = currentTabHistory.stack[currentTabHistory.currentIndex];
118
- core.elements.editor.innerHTML = html;
119
- core.history.restoreCaretPosition(caretOffset);
120
- }
121
- },
122
- redo: () => {
123
- const currentTabHistory = core.history.tabs[core.state.currentTabId];
124
- if (!currentTabHistory) return;
125
-
126
- if (currentTabHistory.currentIndex < currentTabHistory.stack.length - 1) {
127
- currentTabHistory.currentIndex++;
128
-
129
- const { html, caretOffset } = currentTabHistory.stack[currentTabHistory.currentIndex];
130
- core.elements.editor.innerHTML = html;
131
- core.history.restoreCaretPosition(caretOffset);
132
- }
133
- },
134
- restoreCaretPosition: (caretOffset) => {
135
- const selection = core.elements.iframeWindow.getSelection();
136
- const range = document.createRange();
137
- let charCount = 0;
138
-
139
- function setCaret(node) {
140
- if (node.nodeType === 3) { // Text node
141
- const length = node.length;
142
- if (charCount + length >= caretOffset) {
143
- range.setStart(node, caretOffset - charCount);
144
- range.setEnd(node, caretOffset - charCount);
145
- selection.removeAllRanges();
146
- selection.addRange(range);
147
- return true;
148
- } else {
149
- charCount += length;
150
- }
151
- } else if (node.nodeType === 1) { // Element node
152
- for (const element of node.childNodes) {
153
- if (setCaret(element)) {
154
- return true;
155
- }
156
- }
157
- }
158
- return false;
159
- }
160
- setCaret(core.elements.editor);
161
- }
162
- },
163
- onChange: (html = core.elements.editor.innerHTML) => {
164
- updateHeight(core, options)
165
- updateTableResizerPosition(options, core)
166
- if (html === '<br>' || html === '<div><br></div>') html = ''
167
- core.html = html;
168
- // Update current tab content
169
- const currentTab = findTab(core.state.tabs, core.state.currentTabId);
170
- if (currentTab) currentTab.content = html;
171
-
172
- // Return array of tabs if showTabs is true, otherwise just html string?
173
- if (typeof editorRef.onChange === 'function') {
174
- editorRef.onChange(structuredClone(core.state.tabs));
175
- }
176
- if (html.trim()) core.hidePlaceholder()
177
- else core.showPlaceholder()
178
- core.history.push()
179
-
180
- syncRemoteChangesDebounce(core)
181
- updateOutlineItems(core, options)
182
- },
183
- onSave: () => {
184
- if (editorRef.onSave instanceof Function) editorRef.onSave()
185
- },
186
- onBlur: (html) => {
187
- if (editorRef.onBlur instanceof Function) editorRef.onBlur(html)
188
- },
189
- onAttachment: (files) => {
190
- if (editorRef.onAttachment instanceof Function) editorRef.onAttachment(files)
191
- },
192
- onLinkClick: (href) => {
193
- if (editorRef.onLinkClick instanceof Function) editorRef.onLinkClick(href)
194
- },
195
- manuplateImage: async (type, content) => {
196
- if (editorRef.manuplateImage instanceof Function) {
197
- core.changeStatus('Loading...')
198
- const _content = (await editorRef.manuplateImage(type, content)) ?? content
199
- core.changeStatus('')
200
- return _content
201
- }
202
- return content
203
- },
204
- handleFilePicker: async (mimeType, type) => {
205
- if (editorRef.handleFilePicker instanceof Function) { return await editorRef.handleFilePicker(mimeType, type) }
206
- return null
207
-
208
- },
209
- updateBreadcumb: (parentTags) => {
210
- if (!options.hideNavigation && Array.isArray(parentTags)) {
211
- core.elements.breadcumb.innerHTML = ''
212
- parentTags.forEach(tag => {
213
- const li = document.createElement('li');
214
- li.innerText = tag;
215
- core.elements.breadcumb.appendChild(li)
216
- })
217
- }
218
- },
219
- changeStatus: (text) => {
220
- if (!options.hideNavigation) {
221
- core.elements.status.innerText = text
222
- }
223
- },
224
- insertNode: (node) => {
225
- if (core.state.range) {
226
- core.state.range.deleteContents();
227
- core.state.range.insertNode(node);
228
- //cursor at the last with this
229
- core.state.range.collapse(false);
230
- core.state.selection.removeAllRanges();
231
- core.state.selection.addRange(core.state.range);
232
- } else {
233
- core.elements.editor.appendChild(node);
234
- }
235
- },
236
- uploadVideo: async (file) => {
237
- if (editorRef.uploadVideo instanceof Function) {
238
- core.changeStatus('Loading...')
239
- changeToolbarHtmlByName(core, 'video', SVG.LOADER)
240
- const url = await editorRef.uploadVideo(file)
241
- core.changeStatus('')
242
- changeToolbarHtmlByName(core, 'video', SVG.VIDEO)
243
- return url
244
- }
245
- },
246
- showPlaceholder: () => {
247
- core.elements.placeholder.style.display = 'block'
248
- },
249
- hidePlaceholder: () => {
250
- core.elements.placeholder.style.display = 'none'
251
- },
252
- updateCaretPosition: () => {
253
- const selection = core.elements.iframeWindow.getSelection();
254
- let parentTags = [];
255
- let parentElements = [];
256
- if (selection?.rangeCount !== 0) {
257
- const range = selection.getRangeAt(0);
258
- core.state.selection = selection;
259
- core.state.range = range;
260
- core.state.parentNode = range.commonAncestorContainer.parentNode;
261
-
262
- let element = range.commonAncestorContainer;
263
- // If the parent is a text node, get the parent element
264
- if (element.nodeType === Node.TEXT_NODE) {
265
- element = element.parentNode;
266
- }
267
-
268
- while (element && element !== core.elements.editor) {
269
- parentElements.push(element)
270
- parentTags.push(element.tagName);
271
- element = element.parentElement;
272
- }
273
- parentTags = parentTags.reverse()
274
- parentElements = parentElements.reverse()
275
- core.updateBreadcumb(parentTags)
276
- }
277
- return { parentTags, parentElements }
278
- },
279
- updateCursorPosition: () => {
280
- updateCursorPositionDebounce(core, editorRef)
281
- },
282
- onLocalSyncChanges: (diff) => {
283
- if (editorRef.onLocalSyncChanges instanceof Function) editorRef.onLocalSyncChanges(diff)
284
- },
285
- }
286
-
287
-
288
- /**
289
- * For External Use
290
- */
291
- const editorRef = {
292
- setContents: (content) => {
293
- const tabs = prepareTabs(content)
294
- core.state.tabs = tabs;
295
- core.state.currentTabId = tabs[0].tabId;
296
- core.html = tabs[0].content;
297
- renderTabs(core, options);
298
-
299
- core.elements.editor.innerHTML = core.html;
300
- updateHeight(core, options)
301
- },
302
- getContents: () => core.state.tabs,
303
- onChange: () => { },
304
- onSave: () => { },
305
- onBlur: () => { },
306
- onAttachment: () => { },
307
- onLinkClick: (href) => {
308
- window.open(href, '_blank', 'noopener noreferrer')
309
- },
310
- manuplateImage: async () => { },
311
- handleFilePicker: async () => { },
312
- uploadVideo: async () => { },
313
- focus: () => { core.elements.editor.focus() },
314
- getCore: () => core,
315
- updateHeight: (height) => {
316
- core.elements.iframeContainer.style.setProperty('height', height);
317
- },
318
- setDisabled: (disabled) => {
319
- options.disabled = disabled;
320
- core.elements.editor.contentEditable = !disabled;
321
- if (disabled) changeAllToolbarState(core, 'disabled')
322
- else changeAllToolbarState(core, 'enabled')
323
- renderTabs(core, options)
324
- },
325
- updateCssVariables: (newVariables) => {
326
- const variableMap = {
327
- primary: '--primary',
328
- baseWhite: '--base-white',
329
- whiteDark: '--base-white-dark',
330
- shadow: '--shadow',
331
- resizer: '--resizer',
332
- resizerBackground: '--resizer-background',
333
- mention: '--mention',
334
- mentionHighlight: '--mention-highlight',
335
- dashedBorder: '--dashed-border',
336
- tableResizer: '--table-resizer',
337
- tableResizerOutline: '--table-resizer-outline',
338
- textColor: '--text-color',
339
- resizerPosition: '--resizer-position',
340
- resizerPositionBackground: '--resizer-position-background',
341
- linkColor: '--link-color',
342
- scrollbarThumb: '--scrollbar-thumb',
343
- scrollbarTrack: '--scrollbar-track',
344
- };
345
-
346
- const mergedVariables = {
347
- ...CSS_VARIABLES,
348
- ...newVariables
349
- };
350
-
351
- Object.keys(variableMap).forEach(key => {
352
- const cssVar = variableMap[key];
353
- const value = mergedVariables[key];
354
-
355
- if (value !== undefined) {
356
- core.elements.iframeWindow.documentElement.style.setProperty(cssVar, value);
357
- }
358
- });
359
- },
360
- updateLabels: (labels = []) => {
361
- if (!core.state.tribute) return;
362
-
363
- const values = buildTributeValues(labels);
364
- core.state.tribute.append(0, values, true);
365
- },
366
- getLocalCursor: () => core.cursor,
367
- setRemoteCursor: (user, cursor) => showRemoteCursor(core, user, cursor),
368
- onLocalCursorChange: () => { },
369
- onLocalSyncChanges: () => { },
370
- applyRemoteSyncChanges: (diff) => { applyRemoteChanges(core, diff) },
371
- getCurrentTab: () => findTab(core.state.tabs, core.state.currentTabId),
372
- parseTabFromHTML: (html) => ({
373
- tabTitle: 'Tab 1',
374
- tabId: uuidv4(),
375
- content: typeof html === 'string' ? html : '<div><br></div>',
376
- children: []
377
- }),
378
- compare: (tabs1, tabs2) => {
379
- const normalizeTabs = (tabs) => {
380
- return (tabs || []).map(tab => ({
381
- tabTitle: tab.tabTitle,
382
- tabId: tab.tabId,
383
- content: tab.content || '<div><br></div>',
384
- children: normalizeTabs(tab.children)
385
- }));
386
- }
387
- return JSON.stringify(normalizeTabs(tabs1)) === JSON.stringify(normalizeTabs(tabs2))
388
- }
389
- }
390
-
391
- core.elements.base.className = `${options.classPrefix}${CLASSES.CONTAINER}`
392
-
393
- if (Array.isArray(options.plugins) && options.plugins.length) this.#initToolbar(options, core)
394
-
395
- this.#initPlaceholder(options, core)
396
- this.#initCodeviewContainer(options, core);
397
- this.#initEditableContainer(options, core);
398
- if (!options.hideNavigation) this.#initStepper(options, core);
399
-
400
- if (options.value) core.hidePlaceholder()
401
- core.history.push()
402
-
403
- return editorRef
404
- }
405
-
406
- static #initToolbar(options, core) {
407
- const toolbarContainer = document.createElement('div')
408
- toolbarContainer.className = `${options.classPrefix}${CLASSES.TOOLBAR}`
409
-
410
- /**
411
- * @param {String} plugin
412
- * @returns {HTMLElement}
413
- */
414
- const initToolbarItems = (plugin) => {
415
- /************************* to creating button sets ************************* */
416
- if (Array.isArray(plugin)) {
417
- const buttonSets = document.createElement('div');
418
- buttonSets.className = `${options.classPrefix}${CLASSES.TOOLBAR_ITEMS}`
419
- plugin.forEach(plugin => {
420
- const button = initToolbarItems(plugin)
421
- buttonSets.appendChild(button)
422
- })
423
- return buttonSets
424
- }
425
-
426
- const _plugin = PLUGINS[plugin] ?? options.customPlugins?.[plugin];
427
- _plugin.pluginId = plugin
428
- /******************************* validations ******************************* */
429
- if (!_plugin) throw Error(ERRORS.INVALID_PLUGIN);
430
- if (_plugin.title === 'GIPHY' && !options.giphyApiKey) throw Error(ERRORS.GIPHY_KEY_NOT_FOUND);
431
- if (_plugin.title === 'Pexels' && !options.pexelsApiKey) throw Error(ERRORS.PEXELS_KEY_NOT_FOUND);
432
- if (_plugin.title === 'Tenor' && !options.tenorApiKey) throw Error(ERRORS.TENOR_KEY_NOT_FOUND);
433
-
434
- let toolbarPlugin
435
- if (_plugin.type === 'button' || _plugin.type === 'reset-color') {
436
- toolbarPlugin = makeToolbarButton(_plugin, options, core)
437
- } else if (_plugin.type === 'dropdown' || _plugin.type === 'gallery' || _plugin.type === 'category' || _plugin.type === 'mention' || _plugin.type === 'table') {
438
- toolbarPlugin = makeToolbarDropdown(_plugin, options, core)
439
- } else if (_plugin.type === 'select') {
440
- toolbarPlugin = makeToolbarSelect(_plugin, options, core)
441
- } else if (_plugin.type === 'color') {
442
- toolbarPlugin = makeToolbarColor(_plugin, options, core)
443
- } else if (_plugin.type === 'button-select') {
444
- toolbarPlugin = makeToolbarButtonSelect(_plugin, options, core)
445
- }
446
- if (core.elements.toolbar[plugin])
447
- core.elements.toolbar[plugin].push(toolbarPlugin)
448
- else
449
- core.elements.toolbar[plugin] = [toolbarPlugin]
450
-
451
- return toolbarPlugin
452
- }
453
-
454
- options.plugins.forEach(plugin => {
455
- const toolbarButtonSets = initToolbarItems(plugin)
456
- toolbarContainer.appendChild(toolbarButtonSets)
457
- })
458
-
459
- if (options.disabled) changeAllToolbarState(core, 'disabled')
460
- showTooltip(options)
461
-
462
- core.elements.toolbarContainer = toolbarContainer;
463
- core.elements.base.appendChild(toolbarContainer);
464
- }
465
-
466
- static #initPlaceholder(options, core) {
467
- const placeholderContainer = document.createElement('div');
468
- const placeholder = document.createElement('span');
469
- placeholder.className = `${options.classPrefix}${CLASSES.PLACEHOLDER}`
470
- placeholder.innerText = options.placeholder ?? ''
471
- placeholderContainer.appendChild(placeholder)
472
- placeholder.onclick = () => core.elements.editor.focus()
473
- core.elements.placeholder = placeholder
474
- core.elements.base.appendChild(placeholderContainer)
475
- }
476
-
477
- static #initCodeviewContainer(options, core) {
478
- const textarea = document.createElement('textarea');
479
- textarea.className = `${options.classPrefix}${CLASSES.CODEVIEW}`
480
- textarea.spellcheck = false
481
- core.elements.codeview = textarea
482
- core.elements.base.appendChild(textarea)
483
- }
484
-
485
- static #initEditableContainer(options, core) {
486
-
487
- const handleCaretPositionAndPlugin = () => {
488
- try {
489
- const { parentTags, parentElements } = core.updateCaretPosition()
490
-
491
- changeAllToolbarState(core, 'inactive', [])
492
- const activePlugin = []
493
- if (parentTags.includes('A')) activePlugin.push('link')
494
- if (core.elements.iframeWindow.queryCommandState('bold')) activePlugin.push('bold')
495
- if (core.elements.iframeWindow.queryCommandState('underline')) activePlugin.push('underline')
496
- if (core.elements.iframeWindow.queryCommandState('italic')) activePlugin.push('italic')
497
- if (core.elements.iframeWindow.queryCommandState('strikeThrough')) activePlugin.push('strike')
498
- if (core.elements.iframeWindow.queryCommandState('subscript')) activePlugin.push('subscript')
499
- if (core.elements.iframeWindow.queryCommandState('superscript')) activePlugin.push('superscript')
500
- if (core.elements.iframeWindow.queryCommandState('insertOrderedList')) activePlugin.push('ordered_list')
501
- if (core.elements.iframeWindow.queryCommandState('insertUnorderedList')) activePlugin.push('unordered_list')
502
-
503
- let format = ''
504
- if (parentTags.includes('H1')) format = 'h1'
505
- if (parentTags.includes('H2')) format = 'h2'
506
- if (parentTags.includes('H3')) format = 'h3'
507
- if (parentTags.includes('H4')) format = 'h4'
508
- if (parentTags.includes('H5')) format = 'h5'
509
- if (parentTags.includes('H6')) format = 'h6'
510
- if (parentTags.includes('P')) format = 'P'
511
- changeToolbarValueByName(core, 'format-block', format)
512
-
513
- let selectedNode = core.state.range.commonAncestorContainer;
514
- if (core.state.range) {
515
-
516
- // Traverse up to find the element node if selectedNode is a text node
517
- if (selectedNode.nodeType === Node.TEXT_NODE) {
518
- selectedNode = selectedNode.parentNode;
519
- }
520
-
521
- // Get computed styles for the element where the text is contained
522
- const computedStyle = window.getComputedStyle(selectedNode);
523
- const align = computedStyle.textAlign;
524
- if (align) {
525
- if (align === 'justify')
526
- activePlugin.push('align_justify')
527
- else if (align === 'start' || align === 'left' || align === '-moz-left')
528
- activePlugin.push('align_left')
529
- else if (align === 'end' || align === 'right' || align === '-moz-right')
530
- activePlugin.push('align_right')
531
- else if (align === 'center' || align === '-moz-center')
532
- activePlugin.push('align_center')
533
- }
534
-
535
- const styles = window.getComputedStyle(selectedNode);
536
- changeToolbarValueByName(core, 'highlight_color', rgbToHex(styles.backgroundColor))
537
- }
538
-
539
- changeToolbarStateByName(core, 'active', activePlugin)
540
- const styles = window.getComputedStyle(selectedNode);
541
-
542
- const font = parentElements.find(element => element.tagName === 'FONT')
543
-
544
- if (font && font.face) {
545
- changeToolbarValueByName(core, 'font', font.face)
546
- } else if (styles.fontFamily) {
547
- const firstFont = styles.fontFamily.split(",")[0].trim().replace(/['"]/g, "");;
548
- const formattedFont = styles.fontFamily.includes(",") ? `${firstFont}...` : firstFont;
549
- changeToolbarValueByName(core, 'font', `<p style='user-select: none;'>${formattedFont}</p>`)
550
- } else {
551
- changeToolbarValueByName(core, 'font', '')
552
- }
553
-
554
- if (font && font.size) {
555
- changeToolbarValueByName(core, 'font-size', font.size)
556
- } else if (styles.fontSize) {
557
- const match = styles.fontSize.match(/^([\d.]+)([a-z%]*)$/);
558
- const numericValue = parseFloat(match[1]);
559
- const unit = match[2];
560
- const fontSize = (numericValue % 1 === 0 ? numericValue.toString() : numericValue.toFixed(2)) + unit;
561
- changeToolbarValueByName(core, 'font-size', `<p style='user-select: none;'>${fontSize}</p>`)
562
- } else {
563
- changeToolbarValueByName(core, 'font-size', '')
564
- }
565
-
566
- if (font && font.color) {
567
- changeToolbarValueByName(core, 'font_color', font.color)
568
- } else if (styles.color) {
569
- changeToolbarValueByName(core, 'font_color', rgbToHex(styles.color))
570
- } else {
571
- changeToolbarValueByName(core, 'font_color', '')
572
- }
573
- } catch (error) {
574
- console.error(error)
575
- }
576
-
577
- }
578
- const keyPressDebounce = debounce(handleCaretPositionAndPlugin, 100)
579
- const makeObserver = () => {
580
- const observer = new MutationObserver(() => {
581
- // Handle mutation and push state to undo stack
582
- const inputEvent = new Event('custom-input', { bubbles: true });
583
- contentEditableDiv.dispatchEvent(inputEvent);
584
- });
585
-
586
- // Start observing the editor for attribute changes (like resizing)
587
- observer.observe(contentEditableDiv, {
588
- attributes: true,
589
- childList: true,
590
- subtree: true
591
- });
592
-
593
- // font / layout reflow
594
- const ro = new ResizeObserver(() => {
595
- updateHeight(core, options)
596
- });
597
- ro.observe(contentEditableDiv);
598
- }
599
-
600
- /***************************** for css theme variables ********************************* */
601
- if (options.cssVariables) {
602
- const cssVariable = options.cssVariables;
603
- const _CSS_VARIABLES = structuredClone(CSS_VARIABLES)
604
- Object.keys(CSS_VARIABLES).forEach((key) => {
605
- if (cssVariable[key] !== undefined) {
606
- _CSS_VARIABLES[key] = cssVariable[key];
607
- }
608
- });
609
-
610
- CSS.IFRAME_VARIABLES = `
611
- :root {
612
- --primary: ${_CSS_VARIABLES.primary};
613
- --base-white: ${_CSS_VARIABLES.baseWhite};
614
- --base-white-dark: ${_CSS_VARIABLES.whiteDark};
615
- --shadow: ${_CSS_VARIABLES.shadow};
616
- --resizer: ${_CSS_VARIABLES.resizer};
617
- --resizer-background: ${_CSS_VARIABLES.resizerBackground};
618
- --mention: ${_CSS_VARIABLES.mention};
619
- --mention-highlight: ${_CSS_VARIABLES.mentionHighlight};
620
- --dashed-border: ${_CSS_VARIABLES.dashedBorder};
621
- --table-resizer: ${_CSS_VARIABLES.tableResizer};
622
- --table-resizer-outline: ${_CSS_VARIABLES.tableResizerOutline};
623
- --text-color: ${_CSS_VARIABLES.textColor};
624
- --resizer-position: ${_CSS_VARIABLES.resizerPosition};
625
- --resizer-position-background: ${_CSS_VARIABLES.resizerPositionBackground};
626
- --link-color: ${_CSS_VARIABLES.linkColor};
627
- --scrollbar-thumb: ${_CSS_VARIABLES.scrollbarThumb};
628
- --scrollbar-track: ${_CSS_VARIABLES.scrollbarTrack};
629
- }
630
- `;
631
- }
632
-
633
- const iframe = document.createElement('iframe');
634
- iframe.style.flex = '1 1 auto';
635
-
636
- if (options.autoHeight) iframe.style.height = 'auto';
637
- else iframe.style.height = options.minHeight ?? options.height ?? '250px';
638
- iframe.style.minHeight = options.minHeight ?? options.height ?? '250px';
639
- if (options.maxHeight) iframe.style.maxHeight = options.maxHeight;
640
-
641
- iframe.style.border = '0';
642
- core.elements.base.appendChild(iframe)
643
-
644
- const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
645
- // Write the basic HTML structure to the iframe's document
646
- iframeDoc.open();
647
-
648
- iframeDoc.write(`
649
- <!DOCTYPE html>
650
- <html lang='en'>
651
- <head>
652
- <style>
653
- ${CSS.IFRAME_VARIABLES}
654
- ${CSS.IFRAME_BODY}
655
- ${CSS.IFRAME_TRIBUTE}
656
- ${CSS.IFRAME_EDITOR}
657
- ${CSS.IFRAME_RESIZER}
658
- ${CSS.IFRAME_TAB}
659
- ${CSS.IFRAME_MEDIA_QUERY}
660
- ${options.iframeStyle || ''}
661
- </style>
662
- </head>
663
- <body></body>
664
- </html>
665
- `);
666
-
667
- iframeDoc.close();
668
-
669
- iframeDoc.onkeydown = (event) => {
670
- // Check if Ctrl or Cmd key is pressed
671
- const isCtrlOrCmd = event.ctrlKey || event.metaKey;
672
-
673
- // Check for Ctrl + Z (undo) or Ctrl + Y (redo)
674
- if (isCtrlOrCmd && ['z', 'y', 'b', 'i', 'u', 'f', 'h', 's'].includes(event.key)) {
675
- return event.preventDefault(); // Prevent the default undo/redo action
676
- }
677
- }
678
-
679
- const contentEditableDiv = iframeDoc.createElement('div');
680
- contentEditableDiv.contentEditable = !options.disabled;
681
- contentEditableDiv.spellcheck = !!options.spellcheck
682
- contentEditableDiv.innerHTML = core.html;
683
- contentEditableDiv.className = 'content-editable';
684
-
685
- contentEditableDiv.oninput = (e) => {
686
- cleanupListSelectionDeletion(core);
687
- core.onChange(e.target.innerHTML)
688
- }
689
- contentEditableDiv.onbeforeinput = (e) => {
690
- if (e.target.innerHTML === '<br>' || e.target.innerHTML === '') {
691
- e.target.innerHTML = '<div><br></div>'
692
- }
693
- }
694
- contentEditableDiv.addEventListener('custom-input', (e) => {
695
- core.onChange(e.target.innerHTML)
696
- })
697
- contentEditableDiv.onkeydown = (event) => {
698
- // Check if Ctrl or Cmd key is pressed
699
- const isCtrlOrCmd = event.ctrlKey || event.metaKey;
700
- let isLinkCreated = false
701
-
702
- if (isCtrlOrCmd) {
703
- if (event.key === 'z') {
704
- core.history.undo()
705
- } else if (event.key === 'y') {
706
- core.history.redo()
707
- } else if (event.key === 'b') {
708
- applyTextFormat(core, 'bold')
709
- } else if (event.key === 'u') {
710
- applyTextFormat(core, 'underline')
711
- } else if (event.key === 'i') {
712
- applyTextFormat(core, 'italic')
713
- } else if (event.key === 'f' || event.key === 'h') {
714
- findAndReplace(core, options, event)
715
- } else if (event.key === 's') {
716
- core.onSave();
717
- }
718
- }
719
-
720
- if (event.shiftKey && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
721
- const selection = core.elements.iframeWindow.getSelection();
722
- if (selection.rangeCount > 0) {
723
- let element = selection.focusNode;
724
- if (element.nodeType === Node.TEXT_NODE) element = element.parentElement;
725
- const td = element.closest('td, th');
726
-
727
- if (td) {
728
- const table = td.closest('table');
729
- if (table) {
730
- const grid = getTableGrid(table);
731
- const pos = getCellPosition(grid, td);
732
-
733
- if (pos) {
734
- let { r, c } = pos;
735
-
736
- if (event.key === 'ArrowLeft') {
737
- const targetCell = grid[r][c - 1];
738
- if (c === 0) return;
739
- if (targetCell && targetCell !== td &&
740
- (targetCell.rowSpan || 1) === (td.rowSpan || 1) &&
741
- getCellPosition(grid, targetCell).r === r
742
- ) {
743
- if (targetCell.innerHTML.trim() !== '') {
744
- td.innerHTML = targetCell.innerHTML + '<br/>' + td.innerHTML;
745
- }
746
- td.colSpan = (td.colSpan || 1) + (targetCell.colSpan || 1);
747
- targetCell.parentNode.insertBefore(td, targetCell);
748
- targetCell.remove();
749
- }
750
- } else if (event.key === 'ArrowRight') {
751
- const targetCell = grid[r][c + (td.colSpan || 1)];
752
- if (targetCell && targetCell !== td &&
753
- (targetCell.rowSpan || 1) === (td.rowSpan || 1) &&
754
- getCellPosition(grid, targetCell).r === r
755
- ) {
756
- if (targetCell.innerHTML.trim() !== '') {
757
- td.innerHTML += '<br/>' + targetCell.innerHTML;
758
- }
759
- td.colSpan = (td.colSpan || 1) + (targetCell.colSpan || 1);
760
- targetCell.remove();
761
- }
762
- } else if (event.key === 'ArrowUp') {
763
- if (r === 0) return;
764
- const targetCell = grid[r - 1][c];
765
-
766
- if (targetCell && targetCell !== td &&
767
- (targetCell.colSpan || 1) === (td.colSpan || 1) &&
768
- getCellPosition(grid, targetCell).c === c
769
- ) {
770
- if (targetCell.innerHTML.trim() !== '') {
771
- td.innerHTML = targetCell.innerHTML + '<br/>' + td.innerHTML;
772
- }
773
- td.rowSpan = (td.rowSpan || 1) + (targetCell.rowSpan || 1);
774
- targetCell.parentNode.insertBefore(td, targetCell);
775
- targetCell.remove();
776
- }
777
- } else if (event.key === 'ArrowDown') {
778
- const targetRowIdx = r + (td.rowSpan || 1);
779
- if (targetRowIdx >= grid.length) return;
780
- const targetCell = grid[targetRowIdx][c];
781
-
782
- if (targetCell && targetCell !== td &&
783
- (targetCell.colSpan || 1) === (td.colSpan || 1) &&
784
- getCellPosition(grid, targetCell).c === c
785
- ) {
786
- if (targetCell.innerHTML.trim() !== '') {
787
- td.innerHTML += '<br/>' + targetCell.innerHTML;
788
- }
789
- td.rowSpan = (td.rowSpan || 1) + (targetCell.rowSpan || 1);
790
- targetCell.remove();
791
- }
792
- }
793
- core.updateCaretPosition();
794
- }
795
- }
796
- }
797
- }
798
- }
799
-
800
- if (event.key === 'Tab') {
801
- makeSublist(event, core);
802
- if (event.defaultPrevented) return;
803
-
804
- event.preventDefault();
805
-
806
- const tabNode = document.createTextNode("\u00a0\u00a0\u00a0\u00a0");
807
- core.state.range.insertNode(tabNode);
808
- core.state.range.setStartAfter(tabNode);
809
- core.state.range.setEndAfter(tabNode);
810
- core.state.selection.removeAllRanges();
811
- core.state.selection.addRange(core.state.range);
812
- }
813
- if (event.key === ' ' || event.key === 'Enter') {
814
- const sel = core.state.selection;
815
- const node = sel?.anchorNode;
816
-
817
- // Only text nodes, not already inside a link
818
- if (node && node.nodeType === Node.TEXT_NODE && !node.parentElement.closest('a')) {
819
- const caretOffset = sel.focusOffset;
820
- const textBeforeCaret = node.textContent.slice(0, caretOffset);
821
- const match = textBeforeCaret.match(REGEX.TEXT_URL);
822
- if (match) {
823
- const url = match[0];
824
-
825
- // Optional: trim trailing punctuation
826
- const cleanUrl = url.replace(/[.,)]$/, '');
827
-
828
- // Replace URL text with <a> element
829
- const before = textBeforeCaret.slice(0, match.index);
830
- const after = node.textContent.slice(caretOffset);
831
-
832
- const a = document.createElement('a');
833
- a.href = cleanUrl.toLowerCase().startsWith('http') ? cleanUrl : 'https://' + cleanUrl;
834
- a.textContent = cleanUrl;
835
- a.target = '_blank';
836
-
837
- const frag = document.createDocumentFragment();
838
- if (before) frag.appendChild(document.createTextNode(before));
839
- frag.appendChild(a);
840
- if (after) frag.appendChild(document.createTextNode(after));
841
-
842
- node.replaceWith(frag);
843
-
844
- // Move caret after the link
845
- const range = document.createRange();
846
- range.setStartAfter(a);
847
- range.collapse(true);
848
- sel.removeAllRanges();
849
- sel.addRange(range);
850
- isLinkCreated = true
851
- core.elements.lastCreatedLink = a;
852
- }
853
- }
854
- }
855
-
856
- if (core.elements.lastCreatedLink && event.key === 'Backspace') {
857
- event.preventDefault(); // stop normal backspace
858
-
859
- // Unwrap the link
860
- const textNode = document.createTextNode(core.elements.lastCreatedLink.textContent);
861
- core.elements.lastCreatedLink.replaceWith(textNode);
862
-
863
- // Move caret to end
864
- const range = document.createRange();
865
- range.setStart(textNode, textNode.length);
866
- range.collapse(true);
867
- }
868
-
869
- handleBackspaceInList(event, core);
870
- trackListSelectionDeletion(event, core);
871
-
872
- if (!isLinkCreated) core.elements.lastCreatedLink = null
873
-
874
- makeUnorderedList(event, core)
875
- makeHeading(event, core)
876
- makeBlockQuote(event, core)
877
- makeStrikethrough(event, core)
878
- makeCodeBlock(event, core)
879
-
880
- keyPressDebounce()
881
- }
882
- contentEditableDiv.onkeyup = () => {
883
- makeOrderedList(event, core)
884
- }
885
- contentEditableDiv.onclick = (e) => {
886
- if (options.disabled) return
887
- handleCaretPositionAndPlugin()
888
- let element = e.target; // The clicked element
889
-
890
- core.elements.selectedElement = e.target;
891
-
892
- destroyImageResizer(options, core)
893
- destroyTableEditPlugin(options, core)
894
- destroyAnchorPopover(core)
895
-
896
- if (e.target.closest("a")?.tagName === 'A') {
897
- const anchor = e.target.closest("a");
898
- if (anchor && element.tagName !== 'IMG') {
899
- core.elements.selectedElement = anchor;
900
- showAnchorPopover(anchor, e.pageX, e.pageY, options, core);
901
- }
902
-
903
- }
904
- if (element.tagName === 'IMG') initImageResizer('image', e.target, options, core)
905
- if (element.tagName === 'TD' || element.tagName === 'TH') initTableEditPlugin(e.target, options, core)
906
- if (element.tagName === "FIGURE") initImageResizer('figure', e.target, options, core)
907
- if (element.tagName === 'LI' && element.dataset.leksyTocHeadingId) navigateToHeading(core, element.dataset.leksyTocHeadingId)
908
- highlightActiveOutline(core, options)
909
- }
910
- contentEditableDiv.onpaste = (e) => {
911
- e.preventDefault();
912
- const clipboardData = e.clipboardData;
913
-
914
- if (clipboardData.items) {
915
- for (let item of clipboardData.items) {
916
- if (item.kind === 'file' && item.type.startsWith('image/')) {
917
- const file = item.getAsFile();
918
- const reader = new FileReader();
919
- reader.onload = async function (event) {
920
- const img = document.createElement('img');
921
- const base64String = event.target.result;
922
- img.src = await core.manuplateImage('base64', base64String);
923
- core.insertNode(img);
924
- };
925
- reader.readAsDataURL(file);
926
- return;
927
- }
928
- }
929
- }
930
-
931
- const pastedData = clipboardData.getData('text/html') || clipboardData.getData('text/plain');
932
- core.elements.iframeWindow.execCommand('insertHtml', false, cleanHTML(pastedData, options, core))
933
- };
934
- contentEditableDiv.ondrop = (e) => {
935
- e.preventDefault();
936
- const droppedData = e.dataTransfer.getData('text/html') || e.dataTransfer.getData('text/plain');
937
- core.elements.iframeWindow.execCommand('insertHtml', false, cleanHTML(droppedData, options, core))
938
- };
939
- contentEditableDiv.onblur = (e) => {
940
- core.onBlur(e.target.innerHTML)
941
- }
942
-
943
- iframeDoc.body.appendChild(contentEditableDiv);
944
-
945
- const editorCursor = document.createElement('div')
946
- editorCursor.style.position = 'absolute'
947
- editorCursor.style.top = '0'
948
- iframeDoc.body.appendChild(editorCursor);
949
-
950
- if (options.labels?.length) {
951
- const script = iframeDoc.createElement('script');
952
- script.src = "https://cdnjs.cloudflare.com/ajax/libs/tributejs/5.1.3/tribute.js";
953
-
954
- script.onload = () => {
955
- core.state.tribute = new iframe.contentWindow.Tribute({
956
- values: buildTributeValues(options.labels),
957
- noMatchTemplate: () => '<li>No match found</li>',
958
- menuItemTemplate: (item) => {
959
- if (item.original.isCategory) return `<div class="category">${item.original.key}</div>`;
960
- return `<div class="item">${item.original.key}</div>`;
961
- },
962
- containerClass: 'tribute',
963
- selectTemplate: function (item) {
964
- if (item.original.isCategory) return null;
965
- return `<span spellcheck="false" contentEditable="false" data-id="${item.original.value}">${item.original.key}</span>`;
966
- },
967
- });
968
- core.state.tribute.attach(contentEditableDiv);
969
- makeObserver()
970
- };
971
- iframeDoc.body.appendChild(script);
972
- } else {
973
- makeObserver()
974
- }
975
-
976
- iframeDoc.addEventListener('keydown', core.updateCursorPosition)
977
- iframeDoc.addEventListener('keyup', () => {
978
- highlightActiveOutline(core, options)
979
- })
980
- iframeDoc.addEventListener('mouseup', core.updateCursorPosition)
981
- iframeDoc.addEventListener('mousedown', core.updateCursorPosition)
982
- iframeDoc.addEventListener('selectionchange', core.updateCursorPosition)
983
-
984
- core.elements.editor = contentEditableDiv;
985
- core.elements.editorCursor = editorCursor;
986
- core.elements.iframeWindow = iframeDoc;
987
- core.elements.iframeContainer = iframe;
988
- initTabs(core, options);
989
- }
990
-
991
- static #initStepper(options, core) {
992
- const stepper = document.createElement('div');
993
- stepper.className = `${options.classPrefix}${CLASSES.STEPPER}`;
994
-
995
- // Create the <ul> element for the breadcrumb
996
- const breadcrumbList = document.createElement('ul');
997
- breadcrumbList.className = 'breadcrumb';
998
-
999
- const status = document.createElement('span');
1000
-
1001
- stepper.appendChild(breadcrumbList)
1002
- stepper.appendChild(status)
1003
-
1004
- core.elements.base.appendChild(stepper);
1005
- core.elements.breadcumb = breadcrumbList
1006
- core.elements.status = status
1007
- core.elements.stepper = stepper
1008
- }
1009
-
1010
- }
1011
-
1012
- const convertHtmlToText = (html) => {
1013
- const div = document.createElement('div');
1014
- div.innerHTML = html;
1015
- return div.textContent
1016
- }
1017
-
1018
- const isHTMLEmpty = (html) => {
1019
- const tempElement = document.createElement('div');
1020
- tempElement.innerHTML = html
1021
- return !tempElement.innerText.trim() && !tempElement.querySelector('img , ol, ul, blockquote, h1, h2, h3');
1022
- }
1023
-
1024
- export default LeksyEditor
1025
- export {
1026
- convertHtmlToText,
1027
- isHTMLEmpty,
1028
- }