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