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/LICENSE +21 -0
- package/README.md +173 -0
- package/constant.js +605 -0
- package/gallery.js +107 -0
- package/index.js +696 -0
- package/package.json +21 -0
- package/plugin.js +1055 -0
- package/style.css +558 -0
- package/utilities.js +1798 -0
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
|
+
}
|