@37signals/lexxy 0.9.3-beta → 0.9.4-beta
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/dist/lexxy.esm.js +1007 -585
- package/dist/lexxy_helpers.esm.js +14 -0
- package/dist/stylesheets/lexxy-editor.css +8 -11
- package/package.json +15 -15
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,119 +1,33 @@
|
|
|
1
|
-
import '
|
|
2
|
-
|
|
3
|
-
import 'prismjs/components/prism-markup';
|
|
4
|
-
import 'prismjs/components/prism-markup-templating';
|
|
5
|
-
import 'prismjs/components/prism-ruby';
|
|
6
|
-
import 'prismjs/components/prism-php';
|
|
7
|
-
import 'prismjs/components/prism-go';
|
|
8
|
-
import 'prismjs/components/prism-bash';
|
|
9
|
-
import 'prismjs/components/prism-json';
|
|
10
|
-
import 'prismjs/components/prism-diff';
|
|
1
|
+
import { createElement, extractPlainTextFromHtml, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
|
|
2
|
+
export { highlightCode } from './lexxy_helpers.esm.js';
|
|
11
3
|
import DOMPurify from 'dompurify';
|
|
12
|
-
import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType } from '@lexical/selection';
|
|
13
|
-
import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection,
|
|
4
|
+
import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType, $forEachSelectedTextNode, $ensureForwardRangeSelection } from '@lexical/selection';
|
|
5
|
+
import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, DecoratorNode, $createParagraphNode, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, $createTextNode, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isElementNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $getEditor, $getNearestRootOrShadowRoot, $isNodeSelection, $getRoot, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, ParagraphNode, $isRootOrShadowRoot, ElementNode, $splitNode, $isParagraphNode, $createLineBreakNode, $isRootNode, $getChildCaretAtIndex, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, CLEAR_HISTORY_COMMAND, $addUpdateTag, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
14
6
|
import { buildEditorFromExtensions } from '@lexical/extension';
|
|
15
|
-
import { ListNode,
|
|
7
|
+
import { ListNode, ListItemNode, $getListDepth, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $isListItemNode, $isListNode, registerList } from '@lexical/list';
|
|
16
8
|
import { $createAutoLinkNode, $toggleLink, LinkNode, $createLinkNode, AutoLinkNode, $isLinkNode } from '@lexical/link';
|
|
17
|
-
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $descendantsMatching } from '@lexical/utils';
|
|
9
|
+
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, $descendantsMatching } from '@lexical/utils';
|
|
18
10
|
import { registerPlainText } from '@lexical/plain-text';
|
|
19
11
|
import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
20
12
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
21
13
|
import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, normalizeCodeLang, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
22
|
-
import {
|
|
14
|
+
import { TRANSFORMERS, registerMarkdownShortcuts } from '@lexical/markdown';
|
|
23
15
|
import { createEmptyHistoryState, registerHistory } from '@lexical/history';
|
|
24
|
-
import { createElement, extractPlainTextFromHtml, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
|
|
25
|
-
export { highlightCode as highlightAll, highlightCode } from './lexxy_helpers.esm.js';
|
|
26
16
|
import { INSERT_TABLE_COMMAND, $getTableCellNodeFromLexicalNode, TableCellNode, TableNode, TableRowNode, setScrollableTablesActive, registerTablePlugin, registerTableSelectionObserver, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
|
|
27
17
|
import { marked } from 'marked';
|
|
28
18
|
import { $insertDataTransferForRichText } from '@lexical/clipboard';
|
|
19
|
+
import 'prismjs';
|
|
20
|
+
import 'prismjs/components/prism-clike';
|
|
21
|
+
import 'prismjs/components/prism-markup';
|
|
22
|
+
import 'prismjs/components/prism-markup-templating';
|
|
23
|
+
import 'prismjs/components/prism-ruby';
|
|
24
|
+
import 'prismjs/components/prism-php';
|
|
25
|
+
import 'prismjs/components/prism-go';
|
|
26
|
+
import 'prismjs/components/prism-bash';
|
|
27
|
+
import 'prismjs/components/prism-json';
|
|
28
|
+
import 'prismjs/components/prism-diff';
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
// This must be set before importing prismjs
|
|
32
|
-
window.Prism = window.Prism || {};
|
|
33
|
-
window.Prism.manual = true;
|
|
34
|
-
|
|
35
|
-
function deepMerge(target, source) {
|
|
36
|
-
const result = { ...target, ...source };
|
|
37
|
-
for (const [ key, value ] of Object.entries(source)) {
|
|
38
|
-
if (arePlainHashes(target[key], value)) {
|
|
39
|
-
result[key] = deepMerge(target[key], value);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return result
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function arePlainHashes(...values) {
|
|
47
|
-
return values.every(value => value && value.constructor == Object)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
class Configuration {
|
|
51
|
-
#tree = {}
|
|
52
|
-
|
|
53
|
-
constructor(...configs) {
|
|
54
|
-
this.merge(...configs);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
merge(...configs) {
|
|
58
|
-
return this.#tree = configs.reduce(deepMerge, this.#tree)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
get(path) {
|
|
62
|
-
const keys = path.split(".");
|
|
63
|
-
return keys.reduce((node, key) => node[key], this.#tree)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function range(from, to) {
|
|
68
|
-
return [ ...Array(1 + to - from).keys() ].map(i => i + from)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const global = new Configuration({
|
|
72
|
-
attachmentTagName: "action-text-attachment",
|
|
73
|
-
attachmentContentTypeNamespace: "actiontext",
|
|
74
|
-
authenticatedUploads: false,
|
|
75
|
-
extensions: []
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const presets = new Configuration({
|
|
79
|
-
default: {
|
|
80
|
-
attachments: true,
|
|
81
|
-
markdown: true,
|
|
82
|
-
multiLine: true,
|
|
83
|
-
richText: true,
|
|
84
|
-
toolbar: {
|
|
85
|
-
upload: "both"
|
|
86
|
-
},
|
|
87
|
-
highlight: {
|
|
88
|
-
buttons: {
|
|
89
|
-
color: range(1, 9).map(n => `var(--highlight-${n})`),
|
|
90
|
-
"background-color": range(1, 9).map(n => `var(--highlight-bg-${n})`),
|
|
91
|
-
},
|
|
92
|
-
permit: {
|
|
93
|
-
color: [],
|
|
94
|
-
"background-color": []
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
var Lexxy = {
|
|
101
|
-
global,
|
|
102
|
-
presets,
|
|
103
|
-
configure({ global: newGlobal, ...newPresets }) {
|
|
104
|
-
if (newGlobal) {
|
|
105
|
-
global.merge(newGlobal);
|
|
106
|
-
}
|
|
107
|
-
presets.merge(newPresets);
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const ALLOWED_HTML_TAGS = [ "a", "b", "blockquote", "br", "code", "div", "em",
|
|
112
|
-
"figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "li", "mark", "ol", "p", "pre", "q", "s", "strong", "u", "ul", "table", "tbody", "tr", "th", "td" ];
|
|
113
|
-
|
|
114
|
-
const ALLOWED_HTML_ATTRIBUTES = [ "alt", "caption", "class", "content", "content-type", "contenteditable",
|
|
115
|
-
"data-direct-upload-id", "data-sgid", "filename", "filesize", "height", "href", "presentation",
|
|
116
|
-
"previewable", "sgid", "src", "style", "title", "url", "width" ];
|
|
30
|
+
const ALLOWED_HTML_ATTRIBUTES = [ "class", "contenteditable", "href", "src", "style", "title" ];
|
|
117
31
|
|
|
118
32
|
const ALLOWED_STYLE_PROPERTIES = [ "color", "background-color" ];
|
|
119
33
|
|
|
@@ -144,10 +58,22 @@ DOMPurify.addHook("uponSanitizeElement", (node, data) => {
|
|
|
144
58
|
}
|
|
145
59
|
});
|
|
146
60
|
|
|
147
|
-
function buildConfig() {
|
|
61
|
+
function buildConfig(allowedElements) {
|
|
62
|
+
const tagAttributes = {};
|
|
63
|
+
|
|
64
|
+
for (const element of allowedElements) {
|
|
65
|
+
if (typeof element === "string") {
|
|
66
|
+
tagAttributes[element] ||= [];
|
|
67
|
+
} else {
|
|
68
|
+
tagAttributes[element.tag] ||= [];
|
|
69
|
+
tagAttributes[element.tag].push(...element.attributes);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
148
73
|
return {
|
|
149
|
-
ALLOWED_TAGS:
|
|
74
|
+
ALLOWED_TAGS: Object.keys(tagAttributes),
|
|
150
75
|
ALLOWED_ATTR: ALLOWED_HTML_ATTRIBUTES,
|
|
76
|
+
ADD_ATTR: (attribute, tag) => tagAttributes[tag]?.includes(attribute),
|
|
151
77
|
ADD_URI_SAFE_ATTR: [ "caption", "filename" ],
|
|
152
78
|
SAFE_FOR_XML: false // So that it does not strip attributes that contains serialized HTML (like content)
|
|
153
79
|
}
|
|
@@ -158,6 +84,34 @@ function getNonce() {
|
|
|
158
84
|
return element?.content
|
|
159
85
|
}
|
|
160
86
|
|
|
87
|
+
// Register an event listener with a return function to deregister the listener. Both the element and
|
|
88
|
+
// the listener are WeakRefs so neither is pinned in memory by the deregister function.
|
|
89
|
+
function registerEventListener(element, type, listener, options) {
|
|
90
|
+
element.addEventListener(type, listener, options);
|
|
91
|
+
const elementRef = new WeakRef(element);
|
|
92
|
+
const listenerRef = new WeakRef(listener);
|
|
93
|
+
|
|
94
|
+
return function deregisterListener() {
|
|
95
|
+
const listener = listenerRef.deref();
|
|
96
|
+
if (listener) elementRef.deref()?.removeEventListener(type, listener, options);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class ListenerBin {
|
|
101
|
+
#listeners = []
|
|
102
|
+
|
|
103
|
+
track(...listeners) {
|
|
104
|
+
this.#listeners.push(...listeners);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
dispose() {
|
|
108
|
+
while (this.#listeners.length) {
|
|
109
|
+
const teardown = this.#listeners.pop();
|
|
110
|
+
teardown();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
161
115
|
function handleRollingTabIndex(elements, event) {
|
|
162
116
|
const previousActiveElement = document.activeElement;
|
|
163
117
|
|
|
@@ -293,6 +247,11 @@ var ToolbarIcons = {
|
|
|
293
247
|
<path d="M9 12C9.55228 12 10 12.4477 10 13C10 13.5523 9.55228 14 9 14H3C2.44772 14 2 13.5523 2 13C2 12.4477 2.44772 12 3 12H9ZM15 8C15.5523 8 16 8.44772 16 9C16 9.55228 15.5523 10 15 10H3C2.44772 10 2 9.55228 2 9C2 8.44772 2.44772 8 3 8H15ZM15 4C15.5523 4 16 4.44772 16 5C16 5.55228 15.5523 6 15 6H3C2.44772 6 2 5.55228 2 5C2 4.44772 2.44772 4 3 4H15Z"/>
|
|
294
248
|
</svg>`,
|
|
295
249
|
|
|
250
|
+
"clearFormatting":
|
|
251
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
252
|
+
<path d="M10.0607 2.07533C10.8417 1.29432 12.1078 1.2943 12.8888 2.07533L16.424 5.61049C17.205 6.39154 17.205 7.65854 16.424 8.43959L9.44937 15.4142C9.07435 15.7891 8.5656 16.0001 8.03531 16.0001H5.0148C4.55074 16.0001 4.10309 15.8385 3.74722 15.547L3.60074 15.4142L1.57534 13.3888C0.79431 12.6078 0.794336 11.3417 1.57534 10.5607L10.0607 2.07533ZM2.98941 11.9747L5.0148 14.0001H8.03531L9.71792 12.3165L6.18179 8.78139L2.98941 11.9747Z"/>
|
|
253
|
+
</svg>`,
|
|
254
|
+
|
|
296
255
|
"highlight":
|
|
297
256
|
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
298
257
|
<path d="M16.4564 14.4272C17.1356 15.5592 16.3204 17.0002 15.0003 17.0004C13.68 17.0004 12.864 15.5593 13.5433 14.4272L15.0003 12.0004L16.4564 14.4272ZM5.1214 1.70746C5.51192 1.31693 6.14494 1.31693 6.53546 1.70746L9.7171 4.8891L13.2532 8.42426C14.2295 9.40056 14.2295 10.9841 13.2532 11.9604L9.7171 15.4955C8.74078 16.4718 7.15822 16.4718 6.18195 15.4955L2.64679 11.9604C1.67048 10.9841 1.67048 9.40057 2.64679 8.42426L6.18195 4.8891C6.30299 4.76805 6.43323 4.66177 6.57062 4.57074L5.1214 3.12152C4.73091 2.73104 4.73099 2.09799 5.1214 1.70746ZM8.30304 6.30316C8.10776 6.10815 7.79119 6.10799 7.59601 6.30316L4.06085 9.83929L3.9964 9.91742C3.88661 10.0838 3.88645 10.3019 3.9964 10.4682L4.02277 10.5004H11.8763C12.0312 10.3043 12.02 10.0205 11.8392 9.83929L8.30304 6.30316Z"/>
|
|
@@ -366,6 +325,7 @@ var ToolbarIcons = {
|
|
|
366
325
|
|
|
367
326
|
class LexicalToolbarElement extends HTMLElement {
|
|
368
327
|
static observedAttributes = [ "connected" ]
|
|
328
|
+
#listeners = new ListenerBin()
|
|
369
329
|
|
|
370
330
|
constructor() {
|
|
371
331
|
super();
|
|
@@ -386,12 +346,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
386
346
|
}
|
|
387
347
|
|
|
388
348
|
dispose() {
|
|
389
|
-
this.#
|
|
390
|
-
this.#unbindButtons();
|
|
391
|
-
this.#unbindHotkeys();
|
|
392
|
-
this.#unbindFocusListeners();
|
|
393
|
-
this.unregisterSelectionListener?.();
|
|
394
|
-
this.unregisterHistoryListener?.();
|
|
349
|
+
this.#listeners.dispose();
|
|
395
350
|
|
|
396
351
|
this.editorElement = null;
|
|
397
352
|
this.editor = null;
|
|
@@ -450,23 +405,13 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
450
405
|
}
|
|
451
406
|
|
|
452
407
|
#installResizeObserver() {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
#uninstallResizeObserver() {
|
|
458
|
-
if (this.resizeObserver) {
|
|
459
|
-
this.resizeObserver.disconnect();
|
|
460
|
-
this.resizeObserver = null;
|
|
461
|
-
}
|
|
408
|
+
const resizeObserver = new ResizeObserver(() => this.#refreshToolbarOverflow());
|
|
409
|
+
resizeObserver.observe(this);
|
|
410
|
+
this.#listeners.track(() => resizeObserver.disconnect());
|
|
462
411
|
}
|
|
463
412
|
|
|
464
413
|
#bindButtons() {
|
|
465
|
-
this.
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
#unbindButtons() {
|
|
469
|
-
this.removeEventListener("click", this.#handleButtonClicked);
|
|
414
|
+
this.#listeners.track(registerEventListener(this, "click", this.#handleButtonClicked));
|
|
470
415
|
}
|
|
471
416
|
|
|
472
417
|
#handleButtonClicked = (event) => {
|
|
@@ -491,11 +436,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
491
436
|
}
|
|
492
437
|
|
|
493
438
|
#bindHotkeys() {
|
|
494
|
-
this.editorElement
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
#unbindHotkeys() {
|
|
498
|
-
this.editorElement?.removeEventListener("keydown", this.#handleHotkey);
|
|
439
|
+
this.#listeners.track(registerEventListener(this.editorElement, "keydown", this.#handleHotkey));
|
|
499
440
|
}
|
|
500
441
|
|
|
501
442
|
#handleHotkey = (event) => {
|
|
@@ -523,15 +464,11 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
523
464
|
}
|
|
524
465
|
|
|
525
466
|
#bindFocusListeners() {
|
|
526
|
-
this.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
#unbindFocusListeners() {
|
|
532
|
-
this.editorElement?.removeEventListener("lexxy:focus", this.#handleEditorFocus);
|
|
533
|
-
this.editorElement?.removeEventListener("lexxy:blur", this.#handleEditorBlur);
|
|
534
|
-
this.removeEventListener("keydown", this.#handleKeydown);
|
|
467
|
+
this.#listeners.track(
|
|
468
|
+
registerEventListener(this.editorElement, "lexxy:focus", this.#handleEditorFocus),
|
|
469
|
+
registerEventListener(this.editorElement, "lexxy:blur", this.#handleEditorBlur),
|
|
470
|
+
registerEventListener(this, "keydown", this.#handleKeydown)
|
|
471
|
+
);
|
|
535
472
|
}
|
|
536
473
|
|
|
537
474
|
#handleEditorFocus = () => {
|
|
@@ -554,18 +491,18 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
554
491
|
}
|
|
555
492
|
|
|
556
493
|
#monitorSelectionChanges() {
|
|
557
|
-
this.
|
|
494
|
+
this.#listeners.track(this.editor.registerUpdateListener(() => {
|
|
558
495
|
this.editor.getEditorState().read(() => {
|
|
559
496
|
this.#updateButtonStates();
|
|
560
497
|
this.#closeDropdowns();
|
|
561
498
|
});
|
|
562
|
-
});
|
|
499
|
+
}));
|
|
563
500
|
}
|
|
564
501
|
|
|
565
502
|
#monitorHistoryChanges() {
|
|
566
|
-
this.
|
|
503
|
+
this.#listeners.track(this.editor.registerUpdateListener(() => {
|
|
567
504
|
this.#updateUndoRedoButtonStates();
|
|
568
|
-
});
|
|
505
|
+
}));
|
|
569
506
|
}
|
|
570
507
|
|
|
571
508
|
#updateUndoRedoButtonStates() {
|
|
@@ -755,6 +692,10 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
755
692
|
<button type="button" name="underline" data-command="underline" title="Underline">
|
|
756
693
|
${ToolbarIcons.underline} <span>Underline</span>
|
|
757
694
|
</button>
|
|
695
|
+
<div class="lexxy-editor__toolbar-separator" role="separator"></div>
|
|
696
|
+
<button type="button" name="clear-formatting" data-command="clearFormatting" title="Clear formatting">
|
|
697
|
+
${ToolbarIcons.clearFormatting} <span>Clear formatting</span>
|
|
698
|
+
</button>
|
|
758
699
|
</div>
|
|
759
700
|
</details>
|
|
760
701
|
|
|
@@ -773,13 +714,11 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
773
714
|
${ToolbarIcons.link}
|
|
774
715
|
</summary>
|
|
775
716
|
<lexxy-link-dropdown class="lexxy-editor__toolbar-dropdown-content">
|
|
776
|
-
<
|
|
777
|
-
|
|
778
|
-
<
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
</div>
|
|
782
|
-
</form>
|
|
717
|
+
<input type="url" placeholder="Enter a URL…" class="input">
|
|
718
|
+
<div class="lexxy-editor__toolbar-dropdown-actions">
|
|
719
|
+
<button type="button" class="lexxy-editor__toolbar-button" value="link">Link</button>
|
|
720
|
+
<button type="button" class="lexxy-editor__toolbar-button" value="unlink">Unlink</button>
|
|
721
|
+
</div>
|
|
783
722
|
</lexxy-link-dropdown>
|
|
784
723
|
</details>
|
|
785
724
|
|
|
@@ -824,6 +763,96 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
824
763
|
}
|
|
825
764
|
}
|
|
826
765
|
|
|
766
|
+
class HorizontalDividerNode extends DecoratorNode {
|
|
767
|
+
static getType() {
|
|
768
|
+
return "horizontal_divider"
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
static clone(node) {
|
|
772
|
+
return new HorizontalDividerNode(node.__key)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
static importJSON(serializedNode) {
|
|
776
|
+
return new HorizontalDividerNode()
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
static importDOM() {
|
|
780
|
+
return {
|
|
781
|
+
"hr": (hr) => {
|
|
782
|
+
return {
|
|
783
|
+
conversion: () => ({
|
|
784
|
+
node: new HorizontalDividerNode()
|
|
785
|
+
}),
|
|
786
|
+
priority: 1
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
constructor(key) {
|
|
793
|
+
super(key);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
createDOM() {
|
|
797
|
+
const figure = createElement("figure", { className: "horizontal-divider" });
|
|
798
|
+
const hr = createElement("hr");
|
|
799
|
+
|
|
800
|
+
figure.appendChild(hr);
|
|
801
|
+
|
|
802
|
+
const deleteButton = createElement("lexxy-node-delete-button");
|
|
803
|
+
figure.appendChild(deleteButton);
|
|
804
|
+
|
|
805
|
+
return figure
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
updateDOM() {
|
|
809
|
+
return true
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
getTextContent() {
|
|
813
|
+
return "┄\n\n"
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
isInline() {
|
|
817
|
+
return false
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
exportDOM() {
|
|
821
|
+
const hr = createElement("hr");
|
|
822
|
+
return { element: hr }
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
exportJSON() {
|
|
826
|
+
return {
|
|
827
|
+
type: "horizontal_divider",
|
|
828
|
+
version: 1
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
decorate() {
|
|
833
|
+
return null
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const HORIZONTAL_DIVIDER = {
|
|
838
|
+
dependencies: [ HorizontalDividerNode ],
|
|
839
|
+
export: (node) => {
|
|
840
|
+
return node instanceof HorizontalDividerNode ? "---" : null
|
|
841
|
+
},
|
|
842
|
+
regExpStart: /^-{3,}\s?$/,
|
|
843
|
+
replace: (parentNode, children, match, endMatch, linesInBetween, isImport) => {
|
|
844
|
+
const hrNode = new HorizontalDividerNode();
|
|
845
|
+
parentNode.replace(hrNode);
|
|
846
|
+
|
|
847
|
+
if (!isImport) {
|
|
848
|
+
const paragraph = $createParagraphNode();
|
|
849
|
+
hrNode.insertAfter(paragraph);
|
|
850
|
+
paragraph.select();
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
type: "multiline-element"
|
|
854
|
+
};
|
|
855
|
+
|
|
827
856
|
const PUNCTUATION_OR_SPACE = /[^\w]/;
|
|
828
857
|
|
|
829
858
|
// Supplements Lexical's built-in registerMarkdownShortcuts to handle the case
|
|
@@ -1053,75 +1082,92 @@ var theme = {
|
|
|
1053
1082
|
}
|
|
1054
1083
|
};
|
|
1055
1084
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
static clone(node) {
|
|
1062
|
-
return new HorizontalDividerNode(node.__key)
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
static importJSON(serializedNode) {
|
|
1066
|
-
return new HorizontalDividerNode()
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
static importDOM() {
|
|
1070
|
-
return {
|
|
1071
|
-
"hr": (hr) => {
|
|
1072
|
-
return {
|
|
1073
|
-
conversion: () => ({
|
|
1074
|
-
node: new HorizontalDividerNode()
|
|
1075
|
-
}),
|
|
1076
|
-
priority: 1
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1085
|
+
function deepMerge(target, source) {
|
|
1086
|
+
const result = { ...target, ...source };
|
|
1087
|
+
for (const [ key, value ] of Object.entries(source)) {
|
|
1088
|
+
if (arePlainHashes(target[key], value)) {
|
|
1089
|
+
result[key] = deepMerge(target[key], value);
|
|
1079
1090
|
}
|
|
1080
1091
|
}
|
|
1081
1092
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
createDOM() {
|
|
1087
|
-
const figure = createElement("figure", { className: "horizontal-divider" });
|
|
1088
|
-
const hr = createElement("hr");
|
|
1093
|
+
return result
|
|
1094
|
+
}
|
|
1089
1095
|
|
|
1090
|
-
|
|
1096
|
+
function arePlainHashes(...values) {
|
|
1097
|
+
return values.every(value => value && value.constructor == Object)
|
|
1098
|
+
}
|
|
1091
1099
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1100
|
+
class Configuration {
|
|
1101
|
+
#tree = {}
|
|
1094
1102
|
|
|
1095
|
-
|
|
1103
|
+
constructor(...configs) {
|
|
1104
|
+
this.merge(...configs);
|
|
1096
1105
|
}
|
|
1097
1106
|
|
|
1098
|
-
|
|
1099
|
-
return
|
|
1107
|
+
merge(...configs) {
|
|
1108
|
+
return this.#tree = configs.reduce(deepMerge, this.#tree)
|
|
1100
1109
|
}
|
|
1101
1110
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1111
|
+
get(path) {
|
|
1112
|
+
const keys = path.split(".");
|
|
1113
|
+
return keys.reduce((node, key) => node[key], this.#tree)
|
|
1104
1114
|
}
|
|
1115
|
+
}
|
|
1105
1116
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1117
|
+
function range(from, to) {
|
|
1118
|
+
return [ ...Array(1 + to - from).keys() ].map(i => i + from)
|
|
1119
|
+
}
|
|
1109
1120
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1121
|
+
const global = new Configuration({
|
|
1122
|
+
attachmentTagName: "action-text-attachment",
|
|
1123
|
+
attachmentContentTypeNamespace: "actiontext",
|
|
1124
|
+
authenticatedUploads: false,
|
|
1125
|
+
extensions: []
|
|
1126
|
+
});
|
|
1114
1127
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1128
|
+
const presets = new Configuration({
|
|
1129
|
+
default: {
|
|
1130
|
+
attachments: true,
|
|
1131
|
+
markdown: true,
|
|
1132
|
+
multiLine: true,
|
|
1133
|
+
richText: true,
|
|
1134
|
+
toolbar: {
|
|
1135
|
+
upload: "both"
|
|
1136
|
+
},
|
|
1137
|
+
highlight: {
|
|
1138
|
+
buttons: {
|
|
1139
|
+
color: range(1, 9).map(n => `var(--highlight-${n})`),
|
|
1140
|
+
"background-color": range(1, 9).map(n => `var(--highlight-bg-${n})`),
|
|
1141
|
+
},
|
|
1142
|
+
permit: {
|
|
1143
|
+
color: [],
|
|
1144
|
+
"background-color": []
|
|
1145
|
+
}
|
|
1119
1146
|
}
|
|
1120
1147
|
}
|
|
1148
|
+
});
|
|
1121
1149
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1150
|
+
var Lexxy = {
|
|
1151
|
+
global,
|
|
1152
|
+
presets,
|
|
1153
|
+
configure({ global: newGlobal, ...newPresets }) {
|
|
1154
|
+
if (newGlobal) {
|
|
1155
|
+
global.merge(newGlobal);
|
|
1156
|
+
}
|
|
1157
|
+
presets.merge(newPresets);
|
|
1124
1158
|
}
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
function sanitize(html, allowedElements) {
|
|
1162
|
+
return DOMPurify.sanitize(html, buildConfig(allowedElements))
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Sanitize HTML for custom attachment content (mentions, cards, etc.).
|
|
1166
|
+
// Uses DOMPurify defaults to strip XSS vectors (scripts, event handlers)
|
|
1167
|
+
// while preserving the richer tag set that server-rendered attachment
|
|
1168
|
+
// content legitimately uses (e.g. <span>, <div>, <img>).
|
|
1169
|
+
function sanitizeAttachmentContent(html) {
|
|
1170
|
+
return DOMPurify.sanitize(html)
|
|
1125
1171
|
}
|
|
1126
1172
|
|
|
1127
1173
|
function bytesToHumanSize(bytes) {
|
|
@@ -1216,7 +1262,7 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
|
1216
1262
|
createDOM() {
|
|
1217
1263
|
const figure = createElement(this.tagName, { "content-type": this.contentType, "data-lexxy-decorator": true });
|
|
1218
1264
|
|
|
1219
|
-
figure.insertAdjacentHTML("beforeend", this.innerHtml);
|
|
1265
|
+
figure.insertAdjacentHTML("beforeend", sanitizeAttachmentContent(this.innerHtml));
|
|
1220
1266
|
|
|
1221
1267
|
const deleteButton = createElement("lexxy-node-delete-button");
|
|
1222
1268
|
figure.appendChild(deleteButton);
|
|
@@ -1514,6 +1560,10 @@ class LexxyExtension {
|
|
|
1514
1560
|
return null
|
|
1515
1561
|
}
|
|
1516
1562
|
|
|
1563
|
+
get allowedElements() {
|
|
1564
|
+
return []
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1517
1567
|
initializeToolbar(_lexxyToolbar) {
|
|
1518
1568
|
|
|
1519
1569
|
}
|
|
@@ -1736,6 +1786,13 @@ function $applyHighlightRangesToCodeNode(codeNode, highlights) {
|
|
|
1736
1786
|
const childRanges = $buildChildRanges(codeNode);
|
|
1737
1787
|
|
|
1738
1788
|
for (const { node, start: nodeStart, end: nodeEnd } of childRanges) {
|
|
1789
|
+
// Skip plain TextNodes: only CodeHighlightNodes can be split into
|
|
1790
|
+
// styled replacements here. The retokenizer normally converts any
|
|
1791
|
+
// TextNode children back to CodeHighlightNodes before this runs,
|
|
1792
|
+
// but the iteration over $buildChildRanges has to keep counting
|
|
1793
|
+
// them so character offsets stay aligned with the saved ranges.
|
|
1794
|
+
if (!$isCodeHighlightNode(node)) continue
|
|
1795
|
+
|
|
1739
1796
|
// Check if this child overlaps with the highlight range
|
|
1740
1797
|
const overlapStart = Math.max(hlStart, nodeStart);
|
|
1741
1798
|
const overlapEnd = Math.min(hlEnd, nodeEnd);
|
|
@@ -1784,7 +1841,7 @@ function $buildChildRanges(codeNode) {
|
|
|
1784
1841
|
let charOffset = 0;
|
|
1785
1842
|
|
|
1786
1843
|
for (const child of codeNode.getChildren()) {
|
|
1787
|
-
if ($isCodeHighlightNode(child)) {
|
|
1844
|
+
if ($isCodeHighlightNode(child) || $isTextNode(child)) {
|
|
1788
1845
|
const text = child.getTextContent();
|
|
1789
1846
|
childRanges.push({ node: child, start: charOffset, end: charOffset + text.length });
|
|
1790
1847
|
charOffset += text.length;
|
|
@@ -1797,6 +1854,23 @@ function $buildChildRanges(codeNode) {
|
|
|
1797
1854
|
return childRanges
|
|
1798
1855
|
}
|
|
1799
1856
|
|
|
1857
|
+
// Extract highlight ranges from the Lexical node tree of a CodeNode.
|
|
1858
|
+
// This mirrors extractHighlightRanges (which works on DOM elements during
|
|
1859
|
+
// HTML import) but reads from live CodeHighlightNode children instead.
|
|
1860
|
+
function $extractHighlightRangesFromCodeNode(codeNode) {
|
|
1861
|
+
const ranges = [];
|
|
1862
|
+
const childRanges = $buildChildRanges(codeNode);
|
|
1863
|
+
|
|
1864
|
+
for (const { node, start, end } of childRanges) {
|
|
1865
|
+
const style = node.getStyle();
|
|
1866
|
+
if (style && hasHighlightStyles(style)) {
|
|
1867
|
+
ranges.push({ start, end, style });
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
return ranges
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1800
1874
|
function buildCanonicalizers(config) {
|
|
1801
1875
|
return [
|
|
1802
1876
|
new StyleCanonicalizer("color", [ ...config.buttons.color, ...config.permit.color ]),
|
|
@@ -1824,15 +1898,23 @@ function $toggleSelectionStyles(editor, styles) {
|
|
|
1824
1898
|
function $selectionIsInCodeBlock(selection) {
|
|
1825
1899
|
const nodes = selection.getNodes();
|
|
1826
1900
|
return nodes.some((node) => {
|
|
1827
|
-
|
|
1828
|
-
|
|
1901
|
+
// A text node inside a code block may be either a CodeHighlightNode
|
|
1902
|
+
// (after retokenization) or a plain TextNode (after splitText or before
|
|
1903
|
+
// the retokenizer has run). Check the parent in both cases.
|
|
1904
|
+
if ($isCodeHighlightNode(node) || $isTextNode(node)) {
|
|
1905
|
+
return $isCodeNode(node.getParent())
|
|
1906
|
+
}
|
|
1907
|
+
return $isCodeNode(node)
|
|
1829
1908
|
})
|
|
1830
1909
|
}
|
|
1831
1910
|
|
|
1832
1911
|
function $patchCodeHighlightStyles(editor, selection, patch) {
|
|
1833
|
-
// Capture selection state and node keys before the nested update
|
|
1912
|
+
// Capture selection state and node keys before the nested update.
|
|
1913
|
+
// Accept both CodeHighlightNode and TextNode children of a CodeNode
|
|
1914
|
+
// because splitText creates TextNode instances and the retokenizer
|
|
1915
|
+
// may not have converted them back to CodeHighlightNodes yet.
|
|
1834
1916
|
const nodeKeys = selection.getNodes()
|
|
1835
|
-
.filter((node) => $isCodeHighlightNode(node))
|
|
1917
|
+
.filter((node) => ($isCodeHighlightNode(node) || $isTextNode(node)) && $isCodeNode(node.getParent()))
|
|
1836
1918
|
.map((node) => ({
|
|
1837
1919
|
key: node.getKey(),
|
|
1838
1920
|
startOffset: $getNodeSelectionOffsets(node, selection)[0],
|
|
@@ -1846,14 +1928,18 @@ function $patchCodeHighlightStyles(editor, selection, patch) {
|
|
|
1846
1928
|
// are committed before editor.focus() triggers a second update cycle
|
|
1847
1929
|
// that would re-run transforms and wipe out the styles.
|
|
1848
1930
|
editor.update(() => {
|
|
1931
|
+
const affectedCodeNodes = new Set();
|
|
1932
|
+
|
|
1849
1933
|
for (const { key, startOffset, endOffset, textSize } of nodeKeys) {
|
|
1850
1934
|
const node = $getNodeByKey(key);
|
|
1851
|
-
if (!node
|
|
1935
|
+
if (!node) continue
|
|
1852
1936
|
|
|
1853
1937
|
const parent = node.getParent();
|
|
1854
1938
|
if (!$isCodeNode(parent)) continue
|
|
1855
1939
|
if (startOffset === endOffset) continue
|
|
1856
1940
|
|
|
1941
|
+
affectedCodeNodes.add(parent);
|
|
1942
|
+
|
|
1857
1943
|
if (startOffset === 0 && endOffset === textSize) {
|
|
1858
1944
|
$applyStylePatchToNode(node, patch);
|
|
1859
1945
|
} else {
|
|
@@ -1862,6 +1948,17 @@ function $patchCodeHighlightStyles(editor, selection, patch) {
|
|
|
1862
1948
|
$applyStylePatchToNode(targetNode, patch);
|
|
1863
1949
|
}
|
|
1864
1950
|
}
|
|
1951
|
+
|
|
1952
|
+
// After applying styles, save highlight ranges for each affected CodeNode.
|
|
1953
|
+
// The code retokenizer will replace the styled nodes with fresh unstyled
|
|
1954
|
+
// tokens when transforms run. The pending highlights are picked up by the
|
|
1955
|
+
// CodeNode mutation listener and reapplied after retokenization.
|
|
1956
|
+
for (const codeNode of affectedCodeNodes) {
|
|
1957
|
+
const ranges = $extractHighlightRangesFromCodeNode(codeNode);
|
|
1958
|
+
if (ranges.length > 0) {
|
|
1959
|
+
$getPendingHighlights(editor).set(codeNode.getKey(), ranges);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1865
1962
|
}, { skipTransforms: true, discrete: true });
|
|
1866
1963
|
}
|
|
1867
1964
|
|
|
@@ -1985,6 +2082,7 @@ const COMMANDS = [
|
|
|
1985
2082
|
"setFormatHeadingMedium",
|
|
1986
2083
|
"setFormatHeadingSmall",
|
|
1987
2084
|
"setFormatParagraph",
|
|
2085
|
+
"clearFormatting",
|
|
1988
2086
|
"insertUnorderedList",
|
|
1989
2087
|
"insertOrderedList",
|
|
1990
2088
|
"insertQuoteBlock",
|
|
@@ -2002,7 +2100,7 @@ const COMMANDS = [
|
|
|
2002
2100
|
|
|
2003
2101
|
class CommandDispatcher {
|
|
2004
2102
|
#selectionBeforeDrag = null
|
|
2005
|
-
#
|
|
2103
|
+
#listeners = new ListenerBin()
|
|
2006
2104
|
|
|
2007
2105
|
static configureFor(editorElement) {
|
|
2008
2106
|
return new CommandDispatcher(editorElement)
|
|
@@ -2084,7 +2182,7 @@ class CommandDispatcher {
|
|
|
2084
2182
|
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "bullet") {
|
|
2085
2183
|
this.contents.applyParagraphFormat();
|
|
2086
2184
|
} else {
|
|
2087
|
-
this.
|
|
2185
|
+
this.contents.applyUnorderedListFormat();
|
|
2088
2186
|
}
|
|
2089
2187
|
}
|
|
2090
2188
|
|
|
@@ -2097,7 +2195,7 @@ class CommandDispatcher {
|
|
|
2097
2195
|
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "number") {
|
|
2098
2196
|
this.contents.applyParagraphFormat();
|
|
2099
2197
|
} else {
|
|
2100
|
-
this.
|
|
2198
|
+
this.contents.applyOrderedListFormat();
|
|
2101
2199
|
}
|
|
2102
2200
|
}
|
|
2103
2201
|
|
|
@@ -2195,6 +2293,10 @@ class CommandDispatcher {
|
|
|
2195
2293
|
this.contents.applyParagraphFormat();
|
|
2196
2294
|
}
|
|
2197
2295
|
|
|
2296
|
+
dispatchClearFormatting() {
|
|
2297
|
+
this.contents.clearFormatting();
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2198
2300
|
dispatchUploadImage() {
|
|
2199
2301
|
this.#dispatchUploadAttachment("image/*,video/*");
|
|
2200
2302
|
}
|
|
@@ -2236,10 +2338,7 @@ class CommandDispatcher {
|
|
|
2236
2338
|
}
|
|
2237
2339
|
|
|
2238
2340
|
dispose() {
|
|
2239
|
-
|
|
2240
|
-
const unregister = this.#unregister.pop();
|
|
2241
|
-
unregister();
|
|
2242
|
-
}
|
|
2341
|
+
this.#listeners.dispose();
|
|
2243
2342
|
}
|
|
2244
2343
|
|
|
2245
2344
|
#registerCommands() {
|
|
@@ -2252,7 +2351,7 @@ class CommandDispatcher {
|
|
|
2252
2351
|
}
|
|
2253
2352
|
|
|
2254
2353
|
#registerCommandHandler(command, priority, handler) {
|
|
2255
|
-
this.#
|
|
2354
|
+
this.#listeners.track(this.editor.registerCommand(command, handler, priority));
|
|
2256
2355
|
}
|
|
2257
2356
|
|
|
2258
2357
|
#registerKeyboardCommands() {
|
|
@@ -2277,10 +2376,13 @@ class CommandDispatcher {
|
|
|
2277
2376
|
#registerDragAndDropHandlers() {
|
|
2278
2377
|
if (this.editorElement.supportsAttachments) {
|
|
2279
2378
|
this.dragCounter = 0;
|
|
2280
|
-
this.editor.getRootElement()
|
|
2281
|
-
this
|
|
2282
|
-
|
|
2283
|
-
|
|
2379
|
+
const root = this.editor.getRootElement();
|
|
2380
|
+
this.#listeners.track(
|
|
2381
|
+
registerEventListener(root, "dragover", this.#handleDragOver.bind(this)),
|
|
2382
|
+
registerEventListener(root, "drop", this.#handleDrop.bind(this)),
|
|
2383
|
+
registerEventListener(root, "dragenter", this.#handleDragEnter.bind(this)),
|
|
2384
|
+
registerEventListener(root, "dragleave", this.#handleDragLeave.bind(this))
|
|
2385
|
+
);
|
|
2284
2386
|
}
|
|
2285
2387
|
}
|
|
2286
2388
|
|
|
@@ -2509,13 +2611,15 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2509
2611
|
return Lexxy.global.get("attachmentTagName")
|
|
2510
2612
|
}
|
|
2511
2613
|
|
|
2512
|
-
constructor({ tagName, sgid, src, previewable, altText, caption, contentType, fileName, fileSize, width, height }, key) {
|
|
2614
|
+
constructor({ tagName, sgid, src, previewSrc, previewable, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
|
|
2513
2615
|
super(key);
|
|
2514
2616
|
|
|
2515
2617
|
this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
|
|
2516
2618
|
this.sgid = sgid;
|
|
2517
2619
|
this.src = src;
|
|
2620
|
+
this.previewSrc = previewSrc;
|
|
2518
2621
|
this.previewable = parseBoolean(previewable);
|
|
2622
|
+
this.pendingPreview = pendingPreview;
|
|
2519
2623
|
this.altText = altText || "";
|
|
2520
2624
|
this.caption = caption || "";
|
|
2521
2625
|
this.contentType = contentType || "";
|
|
@@ -2523,16 +2627,23 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2523
2627
|
this.fileSize = fileSize;
|
|
2524
2628
|
this.width = width;
|
|
2525
2629
|
this.height = height;
|
|
2630
|
+
this.uploadError = uploadError;
|
|
2526
2631
|
|
|
2527
2632
|
this.editor = $getEditor();
|
|
2528
2633
|
}
|
|
2529
2634
|
|
|
2530
2635
|
createDOM() {
|
|
2636
|
+
if (this.uploadError) return this.createDOMForError()
|
|
2637
|
+
if (this.pendingPreview) return this.#createDOMForPendingPreview()
|
|
2638
|
+
|
|
2531
2639
|
const figure = this.createAttachmentFigure();
|
|
2532
2640
|
|
|
2533
2641
|
if (this.isPreviewableAttachment) {
|
|
2534
2642
|
figure.appendChild(this.#createDOMForImage());
|
|
2535
2643
|
figure.appendChild(this.#createEditableCaption());
|
|
2644
|
+
} else if (this.isVideo) {
|
|
2645
|
+
figure.appendChild(this.#createDOMForFile());
|
|
2646
|
+
figure.appendChild(this.#createEditableCaption());
|
|
2536
2647
|
} else {
|
|
2537
2648
|
figure.appendChild(this.#createDOMForFile());
|
|
2538
2649
|
figure.appendChild(this.#createDOMForNotImage());
|
|
@@ -2541,7 +2652,9 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2541
2652
|
return figure
|
|
2542
2653
|
}
|
|
2543
2654
|
|
|
2544
|
-
updateDOM(
|
|
2655
|
+
updateDOM(prevNode, dom) {
|
|
2656
|
+
if (this.uploadError !== prevNode.uploadError) return true
|
|
2657
|
+
|
|
2545
2658
|
const caption = dom.querySelector("figcaption textarea");
|
|
2546
2659
|
if (caption && this.caption) {
|
|
2547
2660
|
caption.value = this.caption;
|
|
@@ -2598,6 +2711,13 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2598
2711
|
return null
|
|
2599
2712
|
}
|
|
2600
2713
|
|
|
2714
|
+
createDOMForError() {
|
|
2715
|
+
const figure = this.createAttachmentFigure();
|
|
2716
|
+
figure.classList.add("attachment--error");
|
|
2717
|
+
figure.appendChild(createElement("div", { innerText: `Error uploading ${this.fileName || "file"}` }));
|
|
2718
|
+
return figure
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2601
2721
|
createAttachmentFigure(previewable = this.isPreviewableAttachment) {
|
|
2602
2722
|
const figure = createAttachmentFigure(this.contentType, previewable, this.fileName);
|
|
2603
2723
|
figure.draggable = true;
|
|
@@ -2617,32 +2737,147 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2617
2737
|
return isPreviewableImage(this.contentType)
|
|
2618
2738
|
}
|
|
2619
2739
|
|
|
2740
|
+
get isVideo() {
|
|
2741
|
+
return this.contentType.startsWith("video/")
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
#createDOMForPendingPreview() {
|
|
2745
|
+
const figure = this.createAttachmentFigure(false);
|
|
2746
|
+
figure.appendChild(this.#createDOMForFile());
|
|
2747
|
+
figure.appendChild(this.#createDOMForNotImage());
|
|
2748
|
+
this.#pollForPreview(figure);
|
|
2749
|
+
return figure
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2620
2752
|
#createDOMForImage(options = {}) {
|
|
2621
|
-
const
|
|
2753
|
+
const initialSrc = this.previewSrc || this.src;
|
|
2754
|
+
const img = createElement("img", { src: initialSrc, draggable: false, alt: this.altText, ...this.#imageDimensions, ...options });
|
|
2622
2755
|
|
|
2623
2756
|
if (this.previewable && !this.isPreviewableImage) {
|
|
2624
2757
|
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
2625
2758
|
}
|
|
2626
2759
|
|
|
2760
|
+
if (this.previewSrc) {
|
|
2761
|
+
this.#preloadAndSwapSrc(img);
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2627
2764
|
const container = createElement("div", { className: "attachment__container" });
|
|
2628
2765
|
container.appendChild(img);
|
|
2629
2766
|
return container
|
|
2630
2767
|
}
|
|
2631
2768
|
|
|
2769
|
+
#preloadAndSwapSrc(img) {
|
|
2770
|
+
const previewSrc = this.previewSrc;
|
|
2771
|
+
const serverImage = new Image();
|
|
2772
|
+
|
|
2773
|
+
serverImage.onload = () => this.#handleImageLoaded(img, previewSrc);
|
|
2774
|
+
serverImage.onerror = () => this.#handleImageLoadError(previewSrc);
|
|
2775
|
+
serverImage.src = this.src;
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
#handleImageLoaded(img, previewSrc) {
|
|
2779
|
+
img.src = this.src;
|
|
2780
|
+
this.editor.update(() => {
|
|
2781
|
+
if (this.isAttached()) this.getWritable().previewSrc = null;
|
|
2782
|
+
}, { tag: this.#backgroundUpdateTags });
|
|
2783
|
+
this.#revokePreviewSrc(previewSrc);
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
#handleImageLoadError(previewSrc) {
|
|
2787
|
+
this.editor.update(() => {
|
|
2788
|
+
if (this.isAttached()) {
|
|
2789
|
+
this.getWritable().previewSrc = null;
|
|
2790
|
+
this.getWritable().uploadError = true;
|
|
2791
|
+
}
|
|
2792
|
+
}, { tag: this.#backgroundUpdateTags });
|
|
2793
|
+
this.#revokePreviewSrc(previewSrc);
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
get #backgroundUpdateTags() {
|
|
2797
|
+
const rootElement = this.editor.getRootElement();
|
|
2798
|
+
const editorHasFocus = rootElement !== null && rootElement.contains(document.activeElement);
|
|
2799
|
+
|
|
2800
|
+
if (editorHasFocus) {
|
|
2801
|
+
return SILENT_UPDATE_TAGS
|
|
2802
|
+
} else {
|
|
2803
|
+
return [ ...SILENT_UPDATE_TAGS, SKIP_DOM_SELECTION_TAG ]
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
#revokePreviewSrc(previewSrc) {
|
|
2808
|
+
if (previewSrc?.startsWith("blob:")) URL.revokeObjectURL(previewSrc);
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2632
2811
|
#swapPreviewToFileDOM(img) {
|
|
2633
2812
|
const figure = img.closest("figure.attachment");
|
|
2634
2813
|
if (!figure) return
|
|
2635
2814
|
|
|
2636
|
-
figure
|
|
2815
|
+
this.#swapFigureContent(figure, "attachment--preview", "attachment--file", () => {
|
|
2816
|
+
figure.appendChild(this.#createDOMForFile());
|
|
2817
|
+
figure.appendChild(this.#createDOMForNotImage());
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
#pollForPreview(figure) {
|
|
2822
|
+
let attempt = 0;
|
|
2823
|
+
const maxAttempts = 10;
|
|
2637
2824
|
|
|
2638
|
-
const
|
|
2639
|
-
|
|
2825
|
+
const tryLoad = () => {
|
|
2826
|
+
if (!this.editor.read(() => this.isAttached())) return
|
|
2640
2827
|
|
|
2641
|
-
|
|
2642
|
-
|
|
2828
|
+
const img = new Image();
|
|
2829
|
+
const cacheBustedSrc = `${this.src}${this.src.includes("?") ? "&" : "?"}_=${Date.now()}`;
|
|
2643
2830
|
|
|
2644
|
-
|
|
2645
|
-
|
|
2831
|
+
img.onload = () => {
|
|
2832
|
+
if (!this.editor.read(() => this.isAttached())) return
|
|
2833
|
+
|
|
2834
|
+
// The placeholder is a file-type icon SVG (86×100). A real thumbnail
|
|
2835
|
+
// generated from PDF/video content is significantly larger.
|
|
2836
|
+
if (img.naturalWidth > 150 && img.naturalHeight > 150) {
|
|
2837
|
+
this.#swapToPreviewDOM(figure, cacheBustedSrc);
|
|
2838
|
+
} else {
|
|
2839
|
+
retry();
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
img.onerror = () => retry();
|
|
2843
|
+
img.src = cacheBustedSrc;
|
|
2844
|
+
};
|
|
2845
|
+
|
|
2846
|
+
const retry = () => {
|
|
2847
|
+
attempt++;
|
|
2848
|
+
if (attempt < maxAttempts && this.editor.read(() => this.isAttached())) {
|
|
2849
|
+
const delay = Math.min(2000 * Math.pow(1.5, attempt), 15000);
|
|
2850
|
+
setTimeout(tryLoad, delay);
|
|
2851
|
+
}
|
|
2852
|
+
};
|
|
2853
|
+
|
|
2854
|
+
// Give the server time to start processing before the first attempt
|
|
2855
|
+
setTimeout(tryLoad, 3000);
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
#swapToPreviewDOM(figure, previewSrc) {
|
|
2859
|
+
this.#swapFigureContent(figure, "attachment--file", "attachment--preview", () => {
|
|
2860
|
+
const img = createElement("img", { src: previewSrc, draggable: false, alt: this.altText });
|
|
2861
|
+
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
2862
|
+
const container = createElement("div", { className: "attachment__container" });
|
|
2863
|
+
container.appendChild(img);
|
|
2864
|
+
figure.appendChild(container);
|
|
2865
|
+
figure.appendChild(this.#createEditableCaption());
|
|
2866
|
+
});
|
|
2867
|
+
|
|
2868
|
+
this.editor.update(() => {
|
|
2869
|
+
if (this.isAttached()) this.getWritable().pendingPreview = false;
|
|
2870
|
+
}, { tag: this.#backgroundUpdateTags });
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
#swapFigureContent(figure, fromClass, toClass, renderContent) {
|
|
2874
|
+
figure.className = figure.className.replace(fromClass, toClass);
|
|
2875
|
+
|
|
2876
|
+
for (const child of [ ...figure.querySelectorAll(".attachment__container, .attachment__icon, figcaption") ]) {
|
|
2877
|
+
child.remove();
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
renderContent();
|
|
2646
2881
|
}
|
|
2647
2882
|
|
|
2648
2883
|
get #imageDimensions() {
|
|
@@ -2733,7 +2968,7 @@ function $isActionTextAttachmentNode(node) {
|
|
|
2733
2968
|
}
|
|
2734
2969
|
|
|
2735
2970
|
class Selection {
|
|
2736
|
-
#
|
|
2971
|
+
#listeners = new ListenerBin()
|
|
2737
2972
|
|
|
2738
2973
|
constructor(editorElement) {
|
|
2739
2974
|
this.editorElement = editorElement;
|
|
@@ -2884,6 +3119,15 @@ class Selection {
|
|
|
2884
3119
|
const anchorElement = anchorNode.getTopLevelElement();
|
|
2885
3120
|
if (!anchorElement) return false
|
|
2886
3121
|
|
|
3122
|
+
// When anchor and focus are in different block-level children of the same
|
|
3123
|
+
// top-level element (e.g. two paragraphs inside a blockquote), this is a
|
|
3124
|
+
// multi-line selection, not a single-line one.
|
|
3125
|
+
const anchorBlock = $isElementNode(anchorNode) ? anchorNode : anchorNode.getParent();
|
|
3126
|
+
const focusBlock = $isElementNode(focusNode) ? focusNode : focusNode.getParent();
|
|
3127
|
+
if (anchorBlock !== focusBlock && anchorBlock !== anchorElement) {
|
|
3128
|
+
return false
|
|
3129
|
+
}
|
|
3130
|
+
|
|
2887
3131
|
const nodes = selection.getNodes();
|
|
2888
3132
|
for (const node of nodes) {
|
|
2889
3133
|
if ($isLineBreakNode(node)) {
|
|
@@ -2997,10 +3241,7 @@ class Selection {
|
|
|
2997
3241
|
this.editor = null;
|
|
2998
3242
|
this.previouslySelectedKeys = null;
|
|
2999
3243
|
|
|
3000
|
-
|
|
3001
|
-
const unregister = this.#unregister.pop();
|
|
3002
|
-
unregister();
|
|
3003
|
-
}
|
|
3244
|
+
this.#listeners.dispose();
|
|
3004
3245
|
}
|
|
3005
3246
|
|
|
3006
3247
|
// When all inline code text is deleted, Lexical's selection retains the stale
|
|
@@ -3018,7 +3259,7 @@ class Selection {
|
|
|
3018
3259
|
// detects that stale state and clears it so newly typed text won't be
|
|
3019
3260
|
// code-formatted.
|
|
3020
3261
|
#clearStaleInlineCodeFormat() {
|
|
3021
|
-
this.#
|
|
3262
|
+
this.#listeners.track(this.editor.registerUpdateListener(({ editorState, tags }) => {
|
|
3022
3263
|
if (tags.has("history-merge") || tags.has("skip-dom-selection")) return
|
|
3023
3264
|
|
|
3024
3265
|
let isStale = false;
|
|
@@ -3066,7 +3307,7 @@ class Selection {
|
|
|
3066
3307
|
}
|
|
3067
3308
|
|
|
3068
3309
|
#processSelectionChangeCommands() {
|
|
3069
|
-
this.#
|
|
3310
|
+
this.#listeners.track(
|
|
3070
3311
|
this.editor.registerCommand(KEY_ARROW_LEFT_COMMAND, this.#selectPreviousNode.bind(this), COMMAND_PRIORITY_LOW),
|
|
3071
3312
|
this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, this.#selectNextNode.bind(this), COMMAND_PRIORITY_LOW),
|
|
3072
3313
|
this.editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#selectPreviousTopLevelNode.bind(this), COMMAND_PRIORITY_LOW),
|
|
@@ -3077,21 +3318,21 @@ class Selection {
|
|
|
3077
3318
|
this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
3078
3319
|
this.current = $getSelection();
|
|
3079
3320
|
}, COMMAND_PRIORITY_LOW)
|
|
3080
|
-
)
|
|
3321
|
+
);
|
|
3081
3322
|
}
|
|
3082
3323
|
|
|
3083
3324
|
#listenForNodeSelections() {
|
|
3084
|
-
this.#
|
|
3325
|
+
this.#listeners.track(this.editor.registerCommand(CLICK_COMMAND, ({ target }) => {
|
|
3085
3326
|
if (!isDOMNode(target)) return false
|
|
3086
3327
|
|
|
3087
3328
|
const targetNode = $getNearestNodeFromDOMNode(target);
|
|
3088
3329
|
return $isDecoratorNode(targetNode) && this.#selectInLexical(targetNode)
|
|
3089
3330
|
}, COMMAND_PRIORITY_LOW));
|
|
3090
3331
|
|
|
3091
|
-
const moveNextLineHandler = () => this.#selectOrAppendNextLine();
|
|
3092
3332
|
const rootElement = this.editor.getRootElement();
|
|
3093
|
-
|
|
3094
|
-
|
|
3333
|
+
this.#listeners.track(
|
|
3334
|
+
registerEventListener(rootElement, "lexxy:internal:move-to-next-line", () => this.#selectOrAppendNextLine())
|
|
3335
|
+
);
|
|
3095
3336
|
}
|
|
3096
3337
|
|
|
3097
3338
|
#containEditorFocus() {
|
|
@@ -3296,13 +3537,20 @@ class Selection {
|
|
|
3296
3537
|
}
|
|
3297
3538
|
|
|
3298
3539
|
// When backspace is pressed on an empty list item that has siblings,
|
|
3299
|
-
//
|
|
3300
|
-
//
|
|
3301
|
-
//
|
|
3540
|
+
// handle the deletion appropriately:
|
|
3541
|
+
//
|
|
3542
|
+
// - Middle/end items (has previous sibling): remove the empty item and
|
|
3543
|
+
// place the cursor at the end of the previous sibling. Without this,
|
|
3544
|
+
// Lexical's default collapseAtStart converts the empty item into a
|
|
3545
|
+
// paragraph above the list, causing the cursor to jump away.
|
|
3546
|
+
//
|
|
3547
|
+
// - First item (no previous sibling): convert to a paragraph above the
|
|
3548
|
+
// list, matching the standard "unwrap list formatting" behavior that
|
|
3549
|
+
// users expect from pressing backspace at the start of a list item.
|
|
3302
3550
|
//
|
|
3303
|
-
//
|
|
3304
|
-
//
|
|
3305
|
-
//
|
|
3551
|
+
// When the empty item is the last/only one in the list, we return false
|
|
3552
|
+
// and let Lexical's default (convert to paragraph) provide the standard
|
|
3553
|
+
// "exit list" behavior.
|
|
3306
3554
|
#removeEmptyListItem() {
|
|
3307
3555
|
const selection = $getSelection();
|
|
3308
3556
|
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
|
|
@@ -3319,11 +3567,17 @@ class Selection {
|
|
|
3319
3567
|
const previousSibling = listItem.getPreviousSibling();
|
|
3320
3568
|
if (previousSibling) {
|
|
3321
3569
|
previousSibling.selectEnd();
|
|
3322
|
-
|
|
3323
|
-
|
|
3570
|
+
listItem.remove();
|
|
3571
|
+
return true
|
|
3324
3572
|
}
|
|
3325
3573
|
|
|
3574
|
+
const listNode = $getNearestNodeOfType(listItem, ListNode);
|
|
3575
|
+
if (!listNode) return false
|
|
3576
|
+
|
|
3577
|
+
const paragraph = $createParagraphNode();
|
|
3578
|
+
listNode.insertBefore(paragraph);
|
|
3326
3579
|
listItem.remove();
|
|
3580
|
+
paragraph.selectStart();
|
|
3327
3581
|
return true
|
|
3328
3582
|
}
|
|
3329
3583
|
|
|
@@ -3583,10 +3837,6 @@ class Selection {
|
|
|
3583
3837
|
}
|
|
3584
3838
|
}
|
|
3585
3839
|
|
|
3586
|
-
function sanitize(html) {
|
|
3587
|
-
return DOMPurify.sanitize(html, buildConfig())
|
|
3588
|
-
}
|
|
3589
|
-
|
|
3590
3840
|
class EditorConfiguration {
|
|
3591
3841
|
#editorElement
|
|
3592
3842
|
#config
|
|
@@ -3629,6 +3879,107 @@ class EditorConfiguration {
|
|
|
3629
3879
|
}
|
|
3630
3880
|
}
|
|
3631
3881
|
|
|
3882
|
+
class ProvisionalParagraphNode extends ParagraphNode {
|
|
3883
|
+
$config() {
|
|
3884
|
+
return this.config("provisonal_paragraph", {
|
|
3885
|
+
extends: ParagraphNode,
|
|
3886
|
+
importDOM: () => null,
|
|
3887
|
+
$transform: (node) => {
|
|
3888
|
+
node.concretizeIfEdited(node);
|
|
3889
|
+
node.removeUnlessRequired(node);
|
|
3890
|
+
}
|
|
3891
|
+
})
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
static neededBetween(nodeBefore, nodeAfter) {
|
|
3895
|
+
return !$isSelectableElement(nodeBefore, "next")
|
|
3896
|
+
&& !$isSelectableElement(nodeAfter, "previous")
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
createDOM(editor) {
|
|
3900
|
+
const p = super.createDOM(editor);
|
|
3901
|
+
const selected = this.isSelected($getSelection());
|
|
3902
|
+
p.classList.add("provisional-paragraph");
|
|
3903
|
+
p.classList.toggle("hidden", !selected);
|
|
3904
|
+
return p
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
updateDOM(_prevNode, dom) {
|
|
3908
|
+
const selected = this.isSelected($getSelection());
|
|
3909
|
+
dom.classList.toggle("hidden", !selected);
|
|
3910
|
+
return false
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
getTextContent() {
|
|
3914
|
+
return ""
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
exportDOM() {
|
|
3918
|
+
return {
|
|
3919
|
+
element: null
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
// override as Lexical has an interesting view of collapsed selection in ElementNodes
|
|
3924
|
+
// https://github.com/facebook/lexical/blob/f1e4f66014377b1f2595aec2b0ee17f5b7ef4dfc/packages/lexical/src/LexicalNode.ts#L646
|
|
3925
|
+
isSelected(selection = null) {
|
|
3926
|
+
const targetSelection = selection || $getSelection();
|
|
3927
|
+
if (!targetSelection) return false
|
|
3928
|
+
|
|
3929
|
+
if (targetSelection.getNodes().some(node => node.is(this) || this.isParentOf(node))) return true
|
|
3930
|
+
|
|
3931
|
+
// A collapsed range selection on the parent element at an offset adjacent to
|
|
3932
|
+
// this node means the caret is visually at this paragraph's position. Treat it
|
|
3933
|
+
// as selected so the paragraph is visible and the caret renders correctly.
|
|
3934
|
+
//
|
|
3935
|
+
// Both the offset matching our index (cursor just before us) and index + 1
|
|
3936
|
+
// (cursor just after us) count, because the provisional paragraph is an
|
|
3937
|
+
// invisible spacer: the browser resolves both offsets to the same visual spot.
|
|
3938
|
+
if ($isRangeSelection(targetSelection) && targetSelection.isCollapsed()) {
|
|
3939
|
+
const { anchor } = targetSelection;
|
|
3940
|
+
const parent = this.getParent();
|
|
3941
|
+
if (parent && anchor.getNode().is(parent) && anchor.type === "element") {
|
|
3942
|
+
const index = this.getIndexWithinParent();
|
|
3943
|
+
return anchor.offset === index || anchor.offset === index + 1
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
return false
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
removeUnlessRequired(self = this.getLatest()) {
|
|
3951
|
+
if (!self.required) self.remove();
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
concretizeIfEdited(self = this.getLatest()) {
|
|
3955
|
+
if (self.getTextContentSize() > 0) {
|
|
3956
|
+
self.replace($createParagraphNode(), true);
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
|
|
3961
|
+
get required() {
|
|
3962
|
+
return this.isDirectRootChild && ProvisionalParagraphNode.neededBetween(...this.immediateSiblings)
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
get isDirectRootChild() {
|
|
3966
|
+
const parent = this.getParent();
|
|
3967
|
+
return $isRootOrShadowRoot(parent)
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
get immediateSiblings() {
|
|
3971
|
+
return [ this.getPreviousSibling(), this.getNextSibling() ]
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
function $isProvisionalParagraphNode(node) {
|
|
3976
|
+
return node instanceof ProvisionalParagraphNode
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
function $isSelectableElement(node, direction) {
|
|
3980
|
+
return $isElementNode(node) && (direction === "next" ? node.canInsertTextBefore() : node.canInsertTextAfter())
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3632
3983
|
async function loadFileIntoImage(file, image) {
|
|
3633
3984
|
return new Promise((resolve) => {
|
|
3634
3985
|
const reader = new FileReader();
|
|
@@ -3677,7 +4028,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3677
4028
|
}
|
|
3678
4029
|
|
|
3679
4030
|
createDOM() {
|
|
3680
|
-
if (this.uploadError) return this
|
|
4031
|
+
if (this.uploadError) return this.createDOMForError()
|
|
3681
4032
|
|
|
3682
4033
|
// This side-effect is trigged on DOM load to fire only once and avoid multiple
|
|
3683
4034
|
// uploads through cloning. The upload is guarded from restarting in case the
|
|
@@ -3737,13 +4088,6 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3737
4088
|
return this.progress !== null
|
|
3738
4089
|
}
|
|
3739
4090
|
|
|
3740
|
-
#createDOMForError() {
|
|
3741
|
-
const figure = this.createAttachmentFigure();
|
|
3742
|
-
figure.classList.add("attachment--error");
|
|
3743
|
-
figure.appendChild(createElement("div", { innerText: `Error uploading ${this.file?.name ?? "file"}` }));
|
|
3744
|
-
return figure
|
|
3745
|
-
}
|
|
3746
|
-
|
|
3747
4091
|
#createDOMForImage() {
|
|
3748
4092
|
return createElement("img")
|
|
3749
4093
|
}
|
|
@@ -3853,10 +4197,13 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3853
4197
|
}
|
|
3854
4198
|
|
|
3855
4199
|
showUploadedAttachment(blob) {
|
|
3856
|
-
const
|
|
4200
|
+
const previewSrc = this.isPreviewableImage && this.file ? URL.createObjectURL(this.file) : null;
|
|
4201
|
+
|
|
4202
|
+
const replacementNode = this.#toActionTextAttachmentNodeWith(blob, previewSrc);
|
|
4203
|
+
const shouldSelectAfterReplacement = this.#selectionIncludesUploadNode;
|
|
3857
4204
|
this.replace(replacementNode);
|
|
3858
4205
|
|
|
3859
|
-
if ($isRootOrShadowRoot(replacementNode.getParent())) {
|
|
4206
|
+
if (shouldSelectAfterReplacement && $isRootOrShadowRoot(replacementNode.getParent())) {
|
|
3860
4207
|
replacementNode.selectNext();
|
|
3861
4208
|
}
|
|
3862
4209
|
|
|
@@ -3880,8 +4227,22 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3880
4227
|
return rootElement !== null && rootElement.contains(document.activeElement)
|
|
3881
4228
|
}
|
|
3882
4229
|
|
|
3883
|
-
#
|
|
3884
|
-
const
|
|
4230
|
+
get #selectionIncludesUploadNode() {
|
|
4231
|
+
const selection = $getSelection();
|
|
4232
|
+
if (selection === null) return false
|
|
4233
|
+
|
|
4234
|
+
if (selection.getNodes().some((node) => node.is(this))) return true
|
|
4235
|
+
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
|
|
4236
|
+
|
|
4237
|
+
const anchorNode = selection.anchor.getNode();
|
|
4238
|
+
if (!$isProvisionalParagraphNode(anchorNode) || !anchorNode.isEmpty()) return false
|
|
4239
|
+
|
|
4240
|
+
const previousSibling = anchorNode.getPreviousSibling();
|
|
4241
|
+
return previousSibling !== null && previousSibling.is(this)
|
|
4242
|
+
}
|
|
4243
|
+
|
|
4244
|
+
#toActionTextAttachmentNodeWith(blob, previewSrc) {
|
|
4245
|
+
const conversion = new AttachmentNodeConversion(this, blob, previewSrc);
|
|
3885
4246
|
return conversion.toAttachmentNode()
|
|
3886
4247
|
}
|
|
3887
4248
|
|
|
@@ -3892,16 +4253,19 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3892
4253
|
}
|
|
3893
4254
|
|
|
3894
4255
|
class AttachmentNodeConversion {
|
|
3895
|
-
constructor(uploadNode, blob) {
|
|
4256
|
+
constructor(uploadNode, blob, previewSrc) {
|
|
3896
4257
|
this.uploadNode = uploadNode;
|
|
3897
4258
|
this.blob = blob;
|
|
4259
|
+
this.previewSrc = previewSrc;
|
|
3898
4260
|
}
|
|
3899
4261
|
|
|
3900
4262
|
toAttachmentNode() {
|
|
3901
4263
|
return new ActionTextAttachmentNode({
|
|
3902
4264
|
...this.uploadNode,
|
|
3903
4265
|
...this.#propertiesFromBlob,
|
|
3904
|
-
src: this.#src
|
|
4266
|
+
src: this.#src,
|
|
4267
|
+
previewSrc: this.previewSrc,
|
|
4268
|
+
pendingPreview: this.blob.previewable && !this.uploadNode.isPreviewableImage
|
|
3905
4269
|
})
|
|
3906
4270
|
}
|
|
3907
4271
|
|
|
@@ -4140,7 +4504,7 @@ class Uploader {
|
|
|
4140
4504
|
}
|
|
4141
4505
|
|
|
4142
4506
|
$insertUploadNodes() {
|
|
4143
|
-
this.
|
|
4507
|
+
this.contents.insertAtCursor(...this.nodes);
|
|
4144
4508
|
}
|
|
4145
4509
|
|
|
4146
4510
|
get #nodeUrlProperties() {
|
|
@@ -4237,43 +4601,30 @@ class Contents {
|
|
|
4237
4601
|
this.editor = null;
|
|
4238
4602
|
}
|
|
4239
4603
|
|
|
4604
|
+
get selection() { return this.editorElement.selection }
|
|
4605
|
+
|
|
4240
4606
|
insertHtml(html, { tag } = {}) {
|
|
4241
4607
|
this.insertDOM(parseHtml(html), { tag });
|
|
4242
4608
|
}
|
|
4243
4609
|
|
|
4244
4610
|
insertDOM(doc, { tag } = {}) {
|
|
4245
4611
|
this.#unwrapPlaceholderAnchors(doc);
|
|
4246
|
-
if (tag === PASTE_TAG) this.#stripTableCellColorStyles(doc);
|
|
4247
4612
|
|
|
4248
4613
|
this.editor.update(() => {
|
|
4249
|
-
|
|
4250
|
-
if (!$isRangeSelection(selection)) return
|
|
4614
|
+
if ($hasUpdateTag(PASTE_TAG)) this.#stripTableCellColorStyles(doc);
|
|
4251
4615
|
|
|
4252
4616
|
const nodes = $generateNodesFromDOM(this.editor, doc);
|
|
4253
4617
|
if (!this.#insertUploadNodes(nodes)) {
|
|
4254
|
-
|
|
4618
|
+
this.insertAtCursor(...nodes);
|
|
4255
4619
|
}
|
|
4256
4620
|
}, { tag });
|
|
4257
4621
|
}
|
|
4258
4622
|
|
|
4259
|
-
insertAtCursor(
|
|
4260
|
-
|
|
4261
|
-
const
|
|
4623
|
+
insertAtCursor(...nodes) {
|
|
4624
|
+
const selection = $getSelection() ?? $getRoot().selectEnd();
|
|
4625
|
+
const inserter = NodeInserter.for(selection);
|
|
4262
4626
|
|
|
4263
|
-
|
|
4264
|
-
const anchorNode = selection.anchor.getNode();
|
|
4265
|
-
if ($isShadowRoot(anchorNode)) {
|
|
4266
|
-
const paragraph = $createParagraphNode();
|
|
4267
|
-
anchorNode.append(paragraph);
|
|
4268
|
-
selection = paragraph.selectStart();
|
|
4269
|
-
}
|
|
4270
|
-
selection.insertNodes([ node ]);
|
|
4271
|
-
} else if ($isNodeSelection(selection) && selectedNodes.length > 0) {
|
|
4272
|
-
// Overrides Lexical's default behavior of _removing_ the currently selected nodes
|
|
4273
|
-
// https://github.com/facebook/lexical/blob/v0.38.2/packages/lexical/src/LexicalSelection.ts#L412
|
|
4274
|
-
const lastNode = selectedNodes.at(-1);
|
|
4275
|
-
lastNode.insertAfter(node);
|
|
4276
|
-
}
|
|
4627
|
+
inserter.insertNodes(nodes);
|
|
4277
4628
|
}
|
|
4278
4629
|
|
|
4279
4630
|
insertAtCursorEnsuringLineBelow(node) {
|
|
@@ -4295,11 +4646,30 @@ class Contents {
|
|
|
4295
4646
|
$setBlocksType(selection, () => $createHeadingNode(tag));
|
|
4296
4647
|
}
|
|
4297
4648
|
|
|
4298
|
-
|
|
4649
|
+
applyUnorderedListFormat() {
|
|
4650
|
+
this.#splitParagraphsAtLineBreaksUnlessInsideList();
|
|
4651
|
+
this.editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
applyOrderedListFormat() {
|
|
4655
|
+
this.#splitParagraphsAtLineBreaksUnlessInsideList();
|
|
4656
|
+
this.editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
clearFormatting() {
|
|
4299
4660
|
const selection = $getSelection();
|
|
4300
4661
|
if (!$isRangeSelection(selection)) return
|
|
4301
4662
|
|
|
4302
|
-
$
|
|
4663
|
+
$forEachSelectedTextNode(node => {
|
|
4664
|
+
node.setFormat(0);
|
|
4665
|
+
node.setStyle("");
|
|
4666
|
+
});
|
|
4667
|
+
|
|
4668
|
+
$toggleLink(null);
|
|
4669
|
+
|
|
4670
|
+
this.#topLevelElementsInSelection(selection).filter($isQuoteNode).forEach(node => this.#unwrap(node));
|
|
4671
|
+
|
|
4672
|
+
$setBlocksType(selection, () => $createParagraphNode());
|
|
4303
4673
|
}
|
|
4304
4674
|
|
|
4305
4675
|
toggleCodeBlock() {
|
|
@@ -4308,12 +4678,16 @@ class Contents {
|
|
|
4308
4678
|
|
|
4309
4679
|
if (this.#insertNodeIfRoot($createCodeNode("plain"))) return
|
|
4310
4680
|
|
|
4311
|
-
const
|
|
4681
|
+
const blockElements = this.#blockLevelElementsInSelection(selection);
|
|
4682
|
+
const allCode = blockElements.every($isCodeNode);
|
|
4312
4683
|
|
|
4313
|
-
if (
|
|
4314
|
-
this.#
|
|
4684
|
+
if (allCode) {
|
|
4685
|
+
blockElements.forEach(node => this.#unwrapCodeBlock(node));
|
|
4315
4686
|
} else {
|
|
4316
|
-
|
|
4687
|
+
const codeNode = $createCodeNode("plain");
|
|
4688
|
+
blockElements.at(-1).insertAfter(codeNode);
|
|
4689
|
+
codeNode.selectEnd();
|
|
4690
|
+
this.insertAtCursor(...blockElements);
|
|
4317
4691
|
}
|
|
4318
4692
|
}
|
|
4319
4693
|
|
|
@@ -4562,9 +4936,42 @@ class Contents {
|
|
|
4562
4936
|
return false
|
|
4563
4937
|
}
|
|
4564
4938
|
|
|
4939
|
+
#unwrapCodeBlock(codeNode) {
|
|
4940
|
+
const children = codeNode.getChildren();
|
|
4941
|
+
const groups = [ [] ];
|
|
4942
|
+
|
|
4943
|
+
for (const child of children) {
|
|
4944
|
+
if ($isLineBreakNode(child)) {
|
|
4945
|
+
groups.push([]);
|
|
4946
|
+
} else {
|
|
4947
|
+
groups[groups.length - 1].push(child.getTextContent());
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4951
|
+
for (const group of groups) {
|
|
4952
|
+
const paragraph = $createParagraphNode();
|
|
4953
|
+
const text = group.join("");
|
|
4954
|
+
if (text) {
|
|
4955
|
+
paragraph.append($createTextNode(text));
|
|
4956
|
+
}
|
|
4957
|
+
codeNode.insertBefore(paragraph);
|
|
4958
|
+
}
|
|
4959
|
+
|
|
4960
|
+
codeNode.remove();
|
|
4961
|
+
}
|
|
4962
|
+
|
|
4963
|
+
#splitParagraphsAtLineBreaksUnlessInsideList() {
|
|
4964
|
+
if (this.selection.isInsideList) return
|
|
4965
|
+
|
|
4966
|
+
const selection = $getSelection();
|
|
4967
|
+
if (!$isRangeSelection(selection)) return
|
|
4968
|
+
|
|
4969
|
+
this.#splitParagraphsAtLineBreaks(selection);
|
|
4970
|
+
}
|
|
4971
|
+
|
|
4565
4972
|
#splitParagraphsAtLineBreaks(selection) {
|
|
4566
|
-
const
|
|
4567
|
-
const
|
|
4973
|
+
const anchorTopLevel = selection.anchor.getNode().getTopLevelElement();
|
|
4974
|
+
const focusTopLevel = selection.focus.getNode().getTopLevelElement();
|
|
4568
4975
|
const topLevelElements = this.#topLevelElementsInSelection(selection);
|
|
4569
4976
|
|
|
4570
4977
|
for (const element of topLevelElements) {
|
|
@@ -4576,10 +4983,9 @@ class Contents {
|
|
|
4576
4983
|
// Check whether this paragraph needs splitting: skip only if neither
|
|
4577
4984
|
// selection endpoint is inside it (meaning it's a middle paragraph
|
|
4578
4985
|
// fully between anchor and focus with no partial lines to split off).
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
)
|
|
4582
|
-
if (!hasEndpoint) continue
|
|
4986
|
+
// Compare top-level elements so endpoints inside nested inline nodes
|
|
4987
|
+
// (e.g. text inside a LinkNode) are still recognized.
|
|
4988
|
+
if (element !== anchorTopLevel && element !== focusTopLevel) continue
|
|
4583
4989
|
|
|
4584
4990
|
const groups = [ [] ];
|
|
4585
4991
|
for (const child of children) {
|
|
@@ -4601,6 +5007,15 @@ class Contents {
|
|
|
4601
5007
|
}
|
|
4602
5008
|
}
|
|
4603
5009
|
|
|
5010
|
+
#blockLevelElementsInSelection(selection) {
|
|
5011
|
+
const blocks = new Set();
|
|
5012
|
+
for (const node of selection.getNodes()) {
|
|
5013
|
+
blocks.add($getNearestBlockElementAncestorOrThrow(node));
|
|
5014
|
+
}
|
|
5015
|
+
|
|
5016
|
+
return Array.from(blocks)
|
|
5017
|
+
}
|
|
5018
|
+
|
|
4604
5019
|
#topLevelElementsInSelection(selection) {
|
|
4605
5020
|
const elements = new Set();
|
|
4606
5021
|
for (const node of selection.getNodes()) {
|
|
@@ -4788,6 +5203,109 @@ function $isShadowRoot(node) {
|
|
|
4788
5203
|
return $isElementNode(node) && $isRootOrShadowRoot(node) && !$isRootNode(node)
|
|
4789
5204
|
}
|
|
4790
5205
|
|
|
5206
|
+
class NodeInserter {
|
|
5207
|
+
static for(selection) {
|
|
5208
|
+
const INSERTERS = [
|
|
5209
|
+
CodeNodeInserter,
|
|
5210
|
+
QuoteNodeInserter,
|
|
5211
|
+
ShadowRootNodeInserter,
|
|
5212
|
+
NodeSelectionNodeInserter
|
|
5213
|
+
];
|
|
5214
|
+
const Inserter = INSERTERS.find(inserter => inserter.handles(selection));
|
|
5215
|
+
return Inserter ? new Inserter(selection) : selection
|
|
5216
|
+
}
|
|
5217
|
+
|
|
5218
|
+
constructor(selection) {
|
|
5219
|
+
this.selection = selection;
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
|
|
5223
|
+
class CodeNodeInserter extends NodeInserter {
|
|
5224
|
+
static handles(selection) {
|
|
5225
|
+
return $getNearestNodeOfType(selection.anchor?.getNode(), CodeNode)
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
insertNodes(nodes) {
|
|
5229
|
+
if (!this.selection.isCollapsed()) { this.selection.removeText(); }
|
|
5230
|
+
|
|
5231
|
+
$ensureForwardRangeSelection(this.selection);
|
|
5232
|
+
const focusNode = this.selection.focus.getNode();
|
|
5233
|
+
const codeNode = $getNearestNodeOfType(focusNode, CodeNode);
|
|
5234
|
+
const insertionIndex = focusNode.is(codeNode) ? 0 : focusNode.getIndexWithinParent();
|
|
5235
|
+
|
|
5236
|
+
const caret = $getChildCaretAtIndex(codeNode, insertionIndex + 1, "previous");
|
|
5237
|
+
|
|
5238
|
+
for (const node of nodes) {
|
|
5239
|
+
if (!node.isAttached()) continue
|
|
5240
|
+
if (caret.getNodeAtCaret() && $isElementNode(node)) { caret.insert($createLineBreakNode()); }
|
|
5241
|
+
|
|
5242
|
+
caret.insert(this.#convertNodeToCodeChild(node));
|
|
5243
|
+
}
|
|
5244
|
+
|
|
5245
|
+
caret.getNodeAtCaret().selectEnd();
|
|
5246
|
+
}
|
|
5247
|
+
|
|
5248
|
+
#convertNodeToCodeChild(node) {
|
|
5249
|
+
if ($isLineBreakNode(node)) {
|
|
5250
|
+
return node
|
|
5251
|
+
} else {
|
|
5252
|
+
node.remove();
|
|
5253
|
+
return $createTextNode(node.getTextContent())
|
|
5254
|
+
}
|
|
5255
|
+
}
|
|
5256
|
+
|
|
5257
|
+
}
|
|
5258
|
+
|
|
5259
|
+
// Lexical will split a QuoteNode when inserting other Elements - we want them simply inserted as-is
|
|
5260
|
+
class QuoteNodeInserter extends NodeInserter {
|
|
5261
|
+
static handles(selection) {
|
|
5262
|
+
return $getNearestNodeOfType(selection.anchor?.getNode(), QuoteNode)
|
|
5263
|
+
}
|
|
5264
|
+
|
|
5265
|
+
insertNodes(nodes) {
|
|
5266
|
+
if (!this.selection.isCollapsed()) { this.selection.removeText(); }
|
|
5267
|
+
|
|
5268
|
+
$ensureForwardRangeSelection(this.selection);
|
|
5269
|
+
let lastNode = this.selection.focus.getNode();
|
|
5270
|
+
for (const node of nodes) {
|
|
5271
|
+
lastNode = lastNode.insertAfter(node);
|
|
5272
|
+
}
|
|
5273
|
+
|
|
5274
|
+
lastNode.selectEnd();
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
|
|
5278
|
+
class ShadowRootNodeInserter extends NodeInserter {
|
|
5279
|
+
static handles(selection) {
|
|
5280
|
+
return $isShadowRoot(selection?.anchor.getNode())
|
|
5281
|
+
}
|
|
5282
|
+
|
|
5283
|
+
insertNodes(nodes) {
|
|
5284
|
+
const anchorNode = this.selection.anchor.getNode();
|
|
5285
|
+
const paragraph = $createParagraphNode();
|
|
5286
|
+
anchorNode.append(paragraph);
|
|
5287
|
+
|
|
5288
|
+
paragraph.selectStart().insertNodes(nodes);
|
|
5289
|
+
}
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
class NodeSelectionNodeInserter extends NodeInserter {
|
|
5293
|
+
static handles(selection) {
|
|
5294
|
+
return $isNodeSelection(selection)
|
|
5295
|
+
}
|
|
5296
|
+
|
|
5297
|
+
insertNodes(nodes) {
|
|
5298
|
+
const selectedNodes = this.selection.getNodes();
|
|
5299
|
+
|
|
5300
|
+
// Overrides Lexical's default behavior of _removing_ the currently selected nodes
|
|
5301
|
+
// https://github.com/facebook/lexical/blob/v0.38.2/packages/lexical/src/LexicalSelection.ts#L412
|
|
5302
|
+
let lastNode = selectedNodes.at(-1);
|
|
5303
|
+
for (const node of nodes) {
|
|
5304
|
+
lastNode = lastNode.insertAfter(node);
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
|
|
4791
5309
|
class Clipboard {
|
|
4792
5310
|
constructor(editorElement) {
|
|
4793
5311
|
this.editorElement = editorElement;
|
|
@@ -4967,9 +5485,31 @@ class Extensions {
|
|
|
4967
5485
|
}
|
|
4968
5486
|
|
|
4969
5487
|
initializeToolbars() {
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
5488
|
+
const toolbar = this.#lexxyToolbar;
|
|
5489
|
+
if (!toolbar) return
|
|
5490
|
+
|
|
5491
|
+
this.#clearPreviousExtensionToolbarButtons(toolbar);
|
|
5492
|
+
this.#addExtensionToolbarButtons(toolbar);
|
|
5493
|
+
}
|
|
5494
|
+
|
|
5495
|
+
#clearPreviousExtensionToolbarButtons(toolbar) {
|
|
5496
|
+
toolbar.querySelectorAll("[data-lexxy-extension]").forEach(el => el.remove());
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5499
|
+
#addExtensionToolbarButtons(toolbar) {
|
|
5500
|
+
this.enabledExtensions.forEach(ext => {
|
|
5501
|
+
const childrenBefore = new Set(toolbar.children);
|
|
5502
|
+
ext.initializeToolbar(toolbar);
|
|
5503
|
+
for (const child of toolbar.children) {
|
|
5504
|
+
if (!childrenBefore.has(child)) {
|
|
5505
|
+
child.setAttribute("data-lexxy-extension", "");
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
});
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5511
|
+
get allowedElements() {
|
|
5512
|
+
return this.enabledExtensions.flatMap(ext => ext.allowedElements)
|
|
4973
5513
|
}
|
|
4974
5514
|
|
|
4975
5515
|
get #lexxyToolbar() {
|
|
@@ -5005,7 +5545,7 @@ class BrowserAdapter {
|
|
|
5005
5545
|
}
|
|
5006
5546
|
}
|
|
5007
5547
|
|
|
5008
|
-
// Custom TextNode exportDOM that avoids redundant
|
|
5548
|
+
// Custom TextNode exportDOM that avoids redundant wrapping.
|
|
5009
5549
|
//
|
|
5010
5550
|
// Lexical's built-in TextNode.exportDOM() calls createDOM() which produces semantic tags
|
|
5011
5551
|
// like <strong> for bold and <em> for italic, then unconditionally wraps the result
|
|
@@ -5015,6 +5555,9 @@ class BrowserAdapter {
|
|
|
5015
5555
|
// This custom export skips <b> when <strong> is already present and <i> when <em> is
|
|
5016
5556
|
// already present, while preserving <s> and <u> wrappers which have no semantic equivalents
|
|
5017
5557
|
// in createDOM's output.
|
|
5558
|
+
//
|
|
5559
|
+
// Any <span> elements produced by createDOM() are unwrapped, since they only carry
|
|
5560
|
+
// editor classes that aren't meaningful in exported HTML.
|
|
5018
5561
|
|
|
5019
5562
|
function exportTextNodeDOM(editor, textNode) {
|
|
5020
5563
|
const element = textNode.createDOM(editor._config, editor);
|
|
@@ -5043,7 +5586,7 @@ function exportTextNodeDOM(editor, textNode) {
|
|
|
5043
5586
|
result = wrapWith(result, "u");
|
|
5044
5587
|
}
|
|
5045
5588
|
|
|
5046
|
-
return { element: result }
|
|
5589
|
+
return { element: unwrapSpans(result) }
|
|
5047
5590
|
}
|
|
5048
5591
|
|
|
5049
5592
|
function containsTag(element, tagName) {
|
|
@@ -5059,105 +5602,14 @@ function wrapWith(element, tag) {
|
|
|
5059
5602
|
return wrapper
|
|
5060
5603
|
}
|
|
5061
5604
|
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
return this.config("provisonal_paragraph", {
|
|
5065
|
-
extends: ParagraphNode,
|
|
5066
|
-
importDOM: () => null,
|
|
5067
|
-
$transform: (node) => {
|
|
5068
|
-
node.concretizeIfEdited(node);
|
|
5069
|
-
node.removeUnlessRequired(node);
|
|
5070
|
-
}
|
|
5071
|
-
})
|
|
5072
|
-
}
|
|
5073
|
-
|
|
5074
|
-
static neededBetween(nodeBefore, nodeAfter) {
|
|
5075
|
-
return !$isSelectableElement(nodeBefore, "next")
|
|
5076
|
-
&& !$isSelectableElement(nodeAfter, "previous")
|
|
5077
|
-
}
|
|
5078
|
-
|
|
5079
|
-
createDOM(editor) {
|
|
5080
|
-
const p = super.createDOM(editor);
|
|
5081
|
-
const selected = this.isSelected($getSelection());
|
|
5082
|
-
p.classList.add("provisional-paragraph");
|
|
5083
|
-
p.classList.toggle("hidden", !selected);
|
|
5084
|
-
return p
|
|
5085
|
-
}
|
|
5086
|
-
|
|
5087
|
-
updateDOM(_prevNode, dom) {
|
|
5088
|
-
const selected = this.isSelected($getSelection());
|
|
5089
|
-
dom.classList.toggle("hidden", !selected);
|
|
5090
|
-
return false
|
|
5091
|
-
}
|
|
5092
|
-
|
|
5093
|
-
getTextContent() {
|
|
5094
|
-
return ""
|
|
5095
|
-
}
|
|
5096
|
-
|
|
5097
|
-
exportDOM() {
|
|
5098
|
-
return {
|
|
5099
|
-
element: null
|
|
5100
|
-
}
|
|
5101
|
-
}
|
|
5102
|
-
|
|
5103
|
-
// override as Lexical has an interesting view of collapsed selection in ElementNodes
|
|
5104
|
-
// https://github.com/facebook/lexical/blob/f1e4f66014377b1f2595aec2b0ee17f5b7ef4dfc/packages/lexical/src/LexicalNode.ts#L646
|
|
5105
|
-
isSelected(selection = null) {
|
|
5106
|
-
const targetSelection = selection || $getSelection();
|
|
5107
|
-
if (!targetSelection) return false
|
|
5108
|
-
|
|
5109
|
-
if (targetSelection.getNodes().some(node => node.is(this) || this.isParentOf(node))) return true
|
|
5110
|
-
|
|
5111
|
-
// A collapsed range selection on the parent element at an offset adjacent to
|
|
5112
|
-
// this node means the caret is visually at this paragraph's position. Treat it
|
|
5113
|
-
// as selected so the paragraph is visible and the caret renders correctly.
|
|
5114
|
-
//
|
|
5115
|
-
// Both the offset matching our index (cursor just before us) and index + 1
|
|
5116
|
-
// (cursor just after us) count, because the provisional paragraph is an
|
|
5117
|
-
// invisible spacer: the browser resolves both offsets to the same visual spot.
|
|
5118
|
-
if ($isRangeSelection(targetSelection) && targetSelection.isCollapsed()) {
|
|
5119
|
-
const { anchor } = targetSelection;
|
|
5120
|
-
const parent = this.getParent();
|
|
5121
|
-
if (parent && anchor.getNode().is(parent) && anchor.type === "element") {
|
|
5122
|
-
const index = this.getIndexWithinParent();
|
|
5123
|
-
return anchor.offset === index || anchor.offset === index + 1
|
|
5124
|
-
}
|
|
5125
|
-
}
|
|
5126
|
-
|
|
5127
|
-
return false
|
|
5128
|
-
}
|
|
5129
|
-
|
|
5130
|
-
removeUnlessRequired(self = this.getLatest()) {
|
|
5131
|
-
if (!self.required) self.remove();
|
|
5132
|
-
}
|
|
5133
|
-
|
|
5134
|
-
concretizeIfEdited(self = this.getLatest()) {
|
|
5135
|
-
if (self.getTextContentSize() > 0) {
|
|
5136
|
-
self.replace($createParagraphNode(), true);
|
|
5137
|
-
}
|
|
5138
|
-
}
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
get required() {
|
|
5142
|
-
return this.isDirectRootChild && ProvisionalParagraphNode.neededBetween(...this.immediateSiblings)
|
|
5143
|
-
}
|
|
5144
|
-
|
|
5145
|
-
get isDirectRootChild() {
|
|
5146
|
-
const parent = this.getParent();
|
|
5147
|
-
return $isRootOrShadowRoot(parent)
|
|
5148
|
-
}
|
|
5605
|
+
function unwrapSpans(element) {
|
|
5606
|
+
if (element.tagName === "SPAN") return element.firstChild
|
|
5149
5607
|
|
|
5150
|
-
|
|
5151
|
-
|
|
5608
|
+
for (const span of element.querySelectorAll("span")) {
|
|
5609
|
+
span.replaceWith(...span.childNodes);
|
|
5152
5610
|
}
|
|
5153
|
-
}
|
|
5154
|
-
|
|
5155
|
-
function $isProvisionalParagraphNode(node) {
|
|
5156
|
-
return node instanceof ProvisionalParagraphNode
|
|
5157
|
-
}
|
|
5158
5611
|
|
|
5159
|
-
|
|
5160
|
-
return $isElementNode(node) && (direction === "next" ? node.canInsertTextBefore() : node.canInsertTextAfter())
|
|
5612
|
+
return element
|
|
5161
5613
|
}
|
|
5162
5614
|
|
|
5163
5615
|
class ProvisionalParagraphExtension extends LexxyExtension {
|
|
@@ -5310,6 +5762,10 @@ class TablesExtension extends LexxyExtension {
|
|
|
5310
5762
|
return this.editorElement.supportsRichText
|
|
5311
5763
|
}
|
|
5312
5764
|
|
|
5765
|
+
get allowedElements() {
|
|
5766
|
+
return [ "figure", "tbody" ]
|
|
5767
|
+
}
|
|
5768
|
+
|
|
5313
5769
|
get lexicalExtension() {
|
|
5314
5770
|
return defineExtension({
|
|
5315
5771
|
name: "lexxy/tables",
|
|
@@ -5413,21 +5869,21 @@ class AttachmentDragAndDrop {
|
|
|
5413
5869
|
#draggedNodeKey = null
|
|
5414
5870
|
#rafId = null
|
|
5415
5871
|
#draggingRafId = null
|
|
5416
|
-
#
|
|
5872
|
+
#listeners = new ListenerBin()
|
|
5417
5873
|
|
|
5418
5874
|
constructor(editor) {
|
|
5419
5875
|
this.#editor = editor;
|
|
5420
5876
|
|
|
5421
5877
|
// Register Lexical commands at HIGH priority to intercept before the
|
|
5422
5878
|
// base @lexical/rich-text handlers (which return true and consume the events).
|
|
5423
|
-
this.#
|
|
5879
|
+
this.#listeners.track(
|
|
5424
5880
|
editor.registerCommand(DRAGSTART_COMMAND, (event) => this.#handleDragStart(event), COMMAND_PRIORITY_HIGH),
|
|
5425
5881
|
editor.registerCommand(DROP_COMMAND, (event) => this.#handleDrop(event), COMMAND_PRIORITY_HIGH),
|
|
5426
5882
|
);
|
|
5427
5883
|
|
|
5428
5884
|
// Use a root listener to register DOM-level dragover/dragend handlers
|
|
5429
5885
|
// (these events need throttled rAF handling that works better as DOM listeners).
|
|
5430
|
-
|
|
5886
|
+
this.#listeners.track(editor.registerRootListener((root, prevRoot) => {
|
|
5431
5887
|
if (prevRoot) {
|
|
5432
5888
|
prevRoot.removeEventListener("dragover", this.#onDragOver);
|
|
5433
5889
|
prevRoot.removeEventListener("dragend", this.#onDragEnd);
|
|
@@ -5436,14 +5892,12 @@ class AttachmentDragAndDrop {
|
|
|
5436
5892
|
root.addEventListener("dragover", this.#onDragOver);
|
|
5437
5893
|
root.addEventListener("dragend", this.#onDragEnd);
|
|
5438
5894
|
}
|
|
5439
|
-
});
|
|
5440
|
-
this.#cleanupFns.push(unregister);
|
|
5895
|
+
}));
|
|
5441
5896
|
}
|
|
5442
5897
|
|
|
5443
5898
|
destroy() {
|
|
5444
5899
|
this.#cleanup();
|
|
5445
|
-
|
|
5446
|
-
this.#cleanupFns = [];
|
|
5900
|
+
this.#listeners.dispose();
|
|
5447
5901
|
}
|
|
5448
5902
|
|
|
5449
5903
|
// -- Event handlers --------------------------------------------------------
|
|
@@ -5765,11 +6219,18 @@ class AttachmentDragAndDrop {
|
|
|
5765
6219
|
}
|
|
5766
6220
|
}
|
|
5767
6221
|
|
|
6222
|
+
const ATTACHMENT_ATTRIBUTES = [ "alt", "caption", "content", "content-type", "data-direct-upload-id",
|
|
6223
|
+
"data-sgid", "filename", "filesize", "height", "presentation", "previewable", "sgid", "url", "width" ];
|
|
6224
|
+
|
|
5768
6225
|
class AttachmentsExtension extends LexxyExtension {
|
|
5769
6226
|
get enabled() {
|
|
5770
6227
|
return this.editorElement.supportsAttachments
|
|
5771
6228
|
}
|
|
5772
6229
|
|
|
6230
|
+
get allowedElements() {
|
|
6231
|
+
return [ { tag: ActionTextAttachmentNode.TAG_NAME, attributes: ATTACHMENT_ATTRIBUTES } ]
|
|
6232
|
+
}
|
|
6233
|
+
|
|
5773
6234
|
get lexicalExtension() {
|
|
5774
6235
|
return defineExtension({
|
|
5775
6236
|
name: "lexxy/action-text-attachments",
|
|
@@ -6057,6 +6518,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6057
6518
|
#initialValue = ""
|
|
6058
6519
|
#validationTextArea = document.createElement("textarea")
|
|
6059
6520
|
#editorInitializedRafId = null
|
|
6521
|
+
#listeners = new ListenerBin()
|
|
6060
6522
|
#disposables = []
|
|
6061
6523
|
|
|
6062
6524
|
constructor() {
|
|
@@ -6072,6 +6534,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6072
6534
|
|
|
6073
6535
|
this.editor = this.#createEditor();
|
|
6074
6536
|
this.#disposables.push(this.editor);
|
|
6537
|
+
this.#disposables.push(this.#listeners);
|
|
6075
6538
|
|
|
6076
6539
|
this.contents = new Contents(this);
|
|
6077
6540
|
this.#disposables.push(this.contents);
|
|
@@ -6235,7 +6698,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6235
6698
|
get value() {
|
|
6236
6699
|
if (!this.cachedValue) {
|
|
6237
6700
|
this.editor?.getEditorState().read(() => {
|
|
6238
|
-
this.cachedValue = sanitize($generateHtmlFromNodes(this.editor, null));
|
|
6701
|
+
this.cachedValue = sanitize($generateHtmlFromNodes(this.editor, null), this.#allowedElements);
|
|
6239
6702
|
});
|
|
6240
6703
|
}
|
|
6241
6704
|
|
|
@@ -6308,7 +6771,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6308
6771
|
theme: theme,
|
|
6309
6772
|
nodes: this.#lexicalNodes,
|
|
6310
6773
|
html: {
|
|
6311
|
-
export: new Map([ [ TextNode, exportTextNodeDOM ] ])
|
|
6774
|
+
export: new Map([ [ TextNode, exportTextNodeDOM ], [ CodeHighlightNode, exportTextNodeDOM ] ])
|
|
6312
6775
|
}
|
|
6313
6776
|
},
|
|
6314
6777
|
...this.extensions.lexicalExtensions
|
|
@@ -6392,7 +6855,9 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6392
6855
|
}
|
|
6393
6856
|
|
|
6394
6857
|
#resetBeforeTurboCaches() {
|
|
6395
|
-
|
|
6858
|
+
this.#listeners.track(
|
|
6859
|
+
registerEventListener(document, "turbo:before-cache", this.#handleTurboBeforeCache)
|
|
6860
|
+
);
|
|
6396
6861
|
}
|
|
6397
6862
|
|
|
6398
6863
|
#handleTurboBeforeCache = (event) => {
|
|
@@ -6400,7 +6865,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6400
6865
|
}
|
|
6401
6866
|
|
|
6402
6867
|
#synchronizeWithChanges() {
|
|
6403
|
-
this.#
|
|
6868
|
+
this.#listeners.track(this.editor.registerUpdateListener(({ editorState }) => {
|
|
6404
6869
|
this.#clearCachedValues();
|
|
6405
6870
|
this.#internalFormValue = this.value;
|
|
6406
6871
|
this.#toggleEmptyStatus();
|
|
@@ -6414,18 +6879,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6414
6879
|
this.cachedStringValue = null;
|
|
6415
6880
|
}
|
|
6416
6881
|
|
|
6417
|
-
#addUnregisterHandler(handler) {
|
|
6418
|
-
this.unregisterHandlers = this.unregisterHandlers || [];
|
|
6419
|
-
this.unregisterHandlers.push(handler);
|
|
6420
|
-
}
|
|
6421
|
-
|
|
6422
|
-
#unregisterHandlers() {
|
|
6423
|
-
this.unregisterHandlers?.forEach((handler) => {
|
|
6424
|
-
handler();
|
|
6425
|
-
});
|
|
6426
|
-
this.unregisterHandlers = null;
|
|
6427
|
-
}
|
|
6428
|
-
|
|
6429
6882
|
#registerComponents() {
|
|
6430
6883
|
const registered = [];
|
|
6431
6884
|
|
|
@@ -6437,9 +6890,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6437
6890
|
this.#registerTableComponents();
|
|
6438
6891
|
this.#registerCodeHiglightingComponents();
|
|
6439
6892
|
if (this.supportsMarkdown) {
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6893
|
+
const transformers = [ ...TRANSFORMERS, HORIZONTAL_DIVIDER ];
|
|
6894
|
+
registered.push(
|
|
6895
|
+
registerMarkdownShortcuts(this.editor, transformers),
|
|
6896
|
+
registerMarkdownLeadingTagHandler(this.editor, transformers)
|
|
6443
6897
|
);
|
|
6444
6898
|
}
|
|
6445
6899
|
} else {
|
|
@@ -6448,7 +6902,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6448
6902
|
this.historyState = createEmptyHistoryState();
|
|
6449
6903
|
registered.push(registerHistory(this.editor, this.historyState, 20));
|
|
6450
6904
|
|
|
6451
|
-
this.#
|
|
6905
|
+
this.#listeners.track(...registered);
|
|
6452
6906
|
}
|
|
6453
6907
|
|
|
6454
6908
|
#registerTableComponents() {
|
|
@@ -6468,7 +6922,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6468
6922
|
|
|
6469
6923
|
#handleEnter() {
|
|
6470
6924
|
// We can't prevent these externally using regular keydown because Lexical handles it first.
|
|
6471
|
-
this.#
|
|
6925
|
+
this.#listeners.track(this.editor.registerCommand(
|
|
6472
6926
|
KEY_ENTER_COMMAND,
|
|
6473
6927
|
(event) => {
|
|
6474
6928
|
// Prevent CTRL+ENTER
|
|
@@ -6490,13 +6944,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6490
6944
|
}
|
|
6491
6945
|
|
|
6492
6946
|
#registerFocusEvents() {
|
|
6493
|
-
this.
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
this.removeEventListener("focusin", this.#handleFocusIn);
|
|
6498
|
-
this.removeEventListener("focusout", this.#handleFocusOut);
|
|
6499
|
-
});
|
|
6947
|
+
this.#listeners.track(
|
|
6948
|
+
registerEventListener(this, "focusin", this.#handleFocusIn),
|
|
6949
|
+
registerEventListener(this, "focusout", this.#handleFocusOut)
|
|
6950
|
+
);
|
|
6500
6951
|
}
|
|
6501
6952
|
|
|
6502
6953
|
#handleFocusIn(event) {
|
|
@@ -6582,6 +7033,15 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6582
7033
|
}
|
|
6583
7034
|
}
|
|
6584
7035
|
|
|
7036
|
+
get #allowedElements() {
|
|
7037
|
+
return this.#importableTags.concat(this.extensions.allowedElements)
|
|
7038
|
+
}
|
|
7039
|
+
|
|
7040
|
+
get #importableTags() {
|
|
7041
|
+
const tags = Array.from(this.editor._htmlConversions.keys());
|
|
7042
|
+
return tags.filter(tag => !tag.startsWith("#"))
|
|
7043
|
+
}
|
|
7044
|
+
|
|
6585
7045
|
#dispatchAttributesChange() {
|
|
6586
7046
|
let attributes = null;
|
|
6587
7047
|
let linkHref = null;
|
|
@@ -6698,10 +7158,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6698
7158
|
}
|
|
6699
7159
|
|
|
6700
7160
|
#dispose() {
|
|
6701
|
-
this.#unregisterHandlers();
|
|
6702
|
-
this.adapter = null;
|
|
6703
|
-
document.removeEventListener("turbo:before-cache", this.#handleTurboBeforeCache);
|
|
6704
|
-
|
|
6705
7161
|
while (this.#disposables.length) {
|
|
6706
7162
|
this.#disposables.pop().dispose();
|
|
6707
7163
|
}
|
|
@@ -6742,18 +7198,21 @@ function $getReadableTextContent(node) {
|
|
|
6742
7198
|
}
|
|
6743
7199
|
|
|
6744
7200
|
class ToolbarDropdown extends HTMLElement {
|
|
7201
|
+
#listeners = new ListenerBin()
|
|
7202
|
+
|
|
6745
7203
|
connectedCallback() {
|
|
6746
7204
|
this.container = this.closest("details");
|
|
6747
7205
|
|
|
6748
|
-
this.
|
|
6749
|
-
|
|
7206
|
+
this.#listeners.track(
|
|
7207
|
+
registerEventListener(this.container, "toggle", this.#handleToggle),
|
|
7208
|
+
registerEventListener(this.container, "keydown", this.#handleKeyDown)
|
|
7209
|
+
);
|
|
6750
7210
|
|
|
6751
7211
|
this.#onToolbarEditor(this.initialize.bind(this));
|
|
6752
7212
|
}
|
|
6753
7213
|
|
|
6754
7214
|
disconnectedCallback() {
|
|
6755
|
-
this.
|
|
6756
|
-
this.container?.removeEventListener("keydown", this.#handleKeyDown);
|
|
7215
|
+
this.#listeners.dispose();
|
|
6757
7216
|
}
|
|
6758
7217
|
|
|
6759
7218
|
get toolbar() {
|
|
@@ -6768,6 +7227,10 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
6768
7227
|
return this.toolbar.editor
|
|
6769
7228
|
}
|
|
6770
7229
|
|
|
7230
|
+
track(...listeners) {
|
|
7231
|
+
this.#listeners.track(...listeners);
|
|
7232
|
+
}
|
|
7233
|
+
|
|
6771
7234
|
initialize() {
|
|
6772
7235
|
// Any post-editor initialization
|
|
6773
7236
|
}
|
|
@@ -6819,18 +7282,23 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
6819
7282
|
class LinkDropdown extends ToolbarDropdown {
|
|
6820
7283
|
connectedCallback() {
|
|
6821
7284
|
super.connectedCallback();
|
|
7285
|
+
|
|
6822
7286
|
this.input = this.querySelector("input");
|
|
6823
7287
|
|
|
6824
|
-
this.
|
|
6825
|
-
|
|
6826
|
-
|
|
7288
|
+
this.track(
|
|
7289
|
+
registerEventListener(this.container, "toggle", this.#handleToggle),
|
|
7290
|
+
registerEventListener(this.input, "keydown", this.#handleEnter),
|
|
7291
|
+
registerEventListener(this.linkButton, "click", this.#handleLink),
|
|
7292
|
+
registerEventListener(this.unlinkButton, "click", this.#handleUnlink)
|
|
7293
|
+
);
|
|
6827
7294
|
}
|
|
6828
7295
|
|
|
6829
|
-
|
|
6830
|
-
this.
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
7296
|
+
get linkButton() {
|
|
7297
|
+
return this.querySelector("[value='link']")
|
|
7298
|
+
}
|
|
7299
|
+
|
|
7300
|
+
get unlinkButton() {
|
|
7301
|
+
return this.querySelector("[value='unlink']")
|
|
6834
7302
|
}
|
|
6835
7303
|
|
|
6836
7304
|
#handleToggle = ({ newState }) => {
|
|
@@ -6838,9 +7306,21 @@ class LinkDropdown extends ToolbarDropdown {
|
|
|
6838
7306
|
this.input.required = newState === "open";
|
|
6839
7307
|
}
|
|
6840
7308
|
|
|
6841
|
-
#
|
|
6842
|
-
|
|
6843
|
-
|
|
7309
|
+
#handleEnter = (event) => {
|
|
7310
|
+
if (event.key === "Enter") {
|
|
7311
|
+
event.preventDefault();
|
|
7312
|
+
event.stopPropagation();
|
|
7313
|
+
this.#handleLink(event);
|
|
7314
|
+
}
|
|
7315
|
+
}
|
|
7316
|
+
|
|
7317
|
+
#handleLink = () => {
|
|
7318
|
+
if (!this.input.checkValidity()) {
|
|
7319
|
+
this.input.reportValidity();
|
|
7320
|
+
return
|
|
7321
|
+
}
|
|
7322
|
+
|
|
7323
|
+
this.editor.dispatchCommand("link", this.input.value);
|
|
6844
7324
|
this.close();
|
|
6845
7325
|
}
|
|
6846
7326
|
|
|
@@ -6850,23 +7330,10 @@ class LinkDropdown extends ToolbarDropdown {
|
|
|
6850
7330
|
}
|
|
6851
7331
|
|
|
6852
7332
|
get #selectedLinkUrl() {
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
if (!$isRangeSelection(selection)) return
|
|
6858
|
-
|
|
6859
|
-
let node = selection.getNodes()[0];
|
|
6860
|
-
while (node && node.getParent()) {
|
|
6861
|
-
if ($isLinkNode(node)) {
|
|
6862
|
-
url = node.getURL();
|
|
6863
|
-
break
|
|
6864
|
-
}
|
|
6865
|
-
node = node.getParent();
|
|
6866
|
-
}
|
|
6867
|
-
});
|
|
6868
|
-
|
|
6869
|
-
return url
|
|
7333
|
+
return this.editor.getEditorState().read(() => {
|
|
7334
|
+
const linkNode = this.editorElement.selection.nearestNodeOfType(LinkNode);
|
|
7335
|
+
return linkNode?.getUrl() ?? null
|
|
7336
|
+
})
|
|
6870
7337
|
}
|
|
6871
7338
|
}
|
|
6872
7339
|
|
|
@@ -6886,23 +7353,14 @@ class HighlightDropdown extends ToolbarDropdown {
|
|
|
6886
7353
|
|
|
6887
7354
|
connectedCallback() {
|
|
6888
7355
|
super.connectedCallback();
|
|
6889
|
-
this.container
|
|
6890
|
-
}
|
|
6891
|
-
|
|
6892
|
-
disconnectedCallback() {
|
|
6893
|
-
this.container?.removeEventListener("toggle", this.#handleToggle);
|
|
6894
|
-
this.#removeButtonHandlers();
|
|
6895
|
-
super.disconnectedCallback();
|
|
7356
|
+
this.track(registerEventListener(this.container, "toggle", this.#handleToggle));
|
|
6896
7357
|
}
|
|
6897
7358
|
|
|
6898
7359
|
#registerButtonHandlers() {
|
|
6899
|
-
this.#colorButtons.forEach(button =>
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
#removeButtonHandlers() {
|
|
6904
|
-
this.#colorButtons.forEach(button => button.removeEventListener("click", this.#handleColorButtonClick));
|
|
6905
|
-
this.querySelector(REMOVE_HIGHLIGHT_SELECTOR)?.removeEventListener("click", this.#handleRemoveHighlightClick);
|
|
7360
|
+
this.#colorButtons.forEach(button => {
|
|
7361
|
+
this.track(registerEventListener(button, "click", this.#handleColorButtonClick));
|
|
7362
|
+
});
|
|
7363
|
+
this.track(registerEventListener(this.querySelector(REMOVE_HIGHLIGHT_SELECTOR), "click", this.#handleRemoveHighlightClick));
|
|
6906
7364
|
}
|
|
6907
7365
|
|
|
6908
7366
|
#setUpButtons() {
|
|
@@ -7124,9 +7582,11 @@ class RemoteFilterSource extends BaseSource {
|
|
|
7124
7582
|
const NOTHING_FOUND_DEFAULT_MESSAGE = "Nothing found";
|
|
7125
7583
|
|
|
7126
7584
|
class LexicalPromptElement extends HTMLElement {
|
|
7585
|
+
#globalListeners = new ListenerBin()
|
|
7586
|
+
#popoverListeners = new ListenerBin()
|
|
7587
|
+
|
|
7127
7588
|
constructor() {
|
|
7128
7589
|
super();
|
|
7129
|
-
this.keyListeners = [];
|
|
7130
7590
|
this.showPopoverId = 0;
|
|
7131
7591
|
}
|
|
7132
7592
|
|
|
@@ -7140,6 +7600,8 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7140
7600
|
}
|
|
7141
7601
|
|
|
7142
7602
|
disconnectedCallback() {
|
|
7603
|
+
this.#popoverListeners.dispose();
|
|
7604
|
+
this.#globalListeners.dispose();
|
|
7143
7605
|
this.source = null;
|
|
7144
7606
|
this.popoverElement = null;
|
|
7145
7607
|
}
|
|
@@ -7189,7 +7651,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7189
7651
|
}
|
|
7190
7652
|
|
|
7191
7653
|
#addTriggerListener() {
|
|
7192
|
-
|
|
7654
|
+
this.#popoverListeners.track(this.#editor.registerUpdateListener(({ editorState }) => {
|
|
7193
7655
|
editorState.read(() => {
|
|
7194
7656
|
if (this.#selection.isInsideCodeBlock) return
|
|
7195
7657
|
|
|
@@ -7212,18 +7674,18 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7212
7674
|
const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
|
|
7213
7675
|
|
|
7214
7676
|
if (isAtStart || isPrecededBySpaceOrNewline) {
|
|
7215
|
-
|
|
7677
|
+
this.#popoverListeners.dispose();
|
|
7216
7678
|
this.#showPopover();
|
|
7217
7679
|
}
|
|
7218
7680
|
}
|
|
7219
7681
|
}
|
|
7220
7682
|
}
|
|
7221
7683
|
});
|
|
7222
|
-
});
|
|
7684
|
+
}));
|
|
7223
7685
|
}
|
|
7224
7686
|
|
|
7225
7687
|
#addCursorPositionListener() {
|
|
7226
|
-
this.
|
|
7688
|
+
this.#popoverListeners.track(this.#editor.registerUpdateListener(({ editorState }) => {
|
|
7227
7689
|
if (this.closed) return
|
|
7228
7690
|
|
|
7229
7691
|
editorState.read(() => {
|
|
@@ -7250,14 +7712,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7250
7712
|
this.#hidePopover();
|
|
7251
7713
|
}
|
|
7252
7714
|
});
|
|
7253
|
-
});
|
|
7254
|
-
}
|
|
7255
|
-
|
|
7256
|
-
#removeCursorPositionListener() {
|
|
7257
|
-
if (this.cursorPositionListener) {
|
|
7258
|
-
this.cursorPositionListener();
|
|
7259
|
-
this.cursorPositionListener = null;
|
|
7260
|
-
}
|
|
7715
|
+
}));
|
|
7261
7716
|
}
|
|
7262
7717
|
|
|
7263
7718
|
get #editor() {
|
|
@@ -7284,8 +7739,10 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7284
7739
|
this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", true);
|
|
7285
7740
|
this.#selectFirstOption();
|
|
7286
7741
|
|
|
7287
|
-
this.#
|
|
7288
|
-
|
|
7742
|
+
this.#popoverListeners.track(
|
|
7743
|
+
registerEventListener(this.#editorElement, "keydown", this.#handleKeydownOnPopover),
|
|
7744
|
+
registerEventListener(this.#editorElement, "lexxy:change", this.#filterOptions)
|
|
7745
|
+
);
|
|
7289
7746
|
|
|
7290
7747
|
this.#registerKeyListeners();
|
|
7291
7748
|
this.#addCursorPositionListener();
|
|
@@ -7293,16 +7750,20 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7293
7750
|
|
|
7294
7751
|
#registerKeyListeners() {
|
|
7295
7752
|
// We can't use a regular keydown for Enter as Lexical handles it first
|
|
7296
|
-
this
|
|
7297
|
-
|
|
7753
|
+
this.#popoverListeners.track(
|
|
7754
|
+
this.#editor.registerCommand(KEY_ENTER_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_CRITICAL),
|
|
7755
|
+
this.#editor.registerCommand(KEY_TAB_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_CRITICAL)
|
|
7756
|
+
);
|
|
7298
7757
|
|
|
7299
7758
|
if (this.#doesSpaceSelect) {
|
|
7300
|
-
this.
|
|
7759
|
+
this.#popoverListeners.track(this.#editor.registerCommand(KEY_SPACE_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_CRITICAL));
|
|
7301
7760
|
}
|
|
7302
7761
|
|
|
7303
7762
|
// Register arrow keys with CRITICAL priority to prevent Lexical's selection handlers from running
|
|
7304
|
-
this
|
|
7305
|
-
|
|
7763
|
+
this.#popoverListeners.track(
|
|
7764
|
+
this.#editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#handleArrowUp.bind(this), COMMAND_PRIORITY_CRITICAL),
|
|
7765
|
+
this.#editor.registerCommand(KEY_ARROW_DOWN_COMMAND, this.#handleArrowDown.bind(this), COMMAND_PRIORITY_CRITICAL)
|
|
7766
|
+
);
|
|
7306
7767
|
}
|
|
7307
7768
|
|
|
7308
7769
|
#handleArrowUp(event) {
|
|
@@ -7394,21 +7855,12 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7394
7855
|
this.showPopoverId++;
|
|
7395
7856
|
this.#clearSelection();
|
|
7396
7857
|
this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", false);
|
|
7397
|
-
this.#
|
|
7398
|
-
this.#editorElement.removeEventListener("keydown", this.#handleKeydownOnPopover);
|
|
7399
|
-
|
|
7400
|
-
this.#unregisterKeyListeners();
|
|
7401
|
-
this.#removeCursorPositionListener();
|
|
7858
|
+
this.#popoverListeners.dispose();
|
|
7402
7859
|
|
|
7403
7860
|
await nextFrame();
|
|
7404
7861
|
this.#addTriggerListener();
|
|
7405
7862
|
}
|
|
7406
7863
|
|
|
7407
|
-
#unregisterKeyListeners() {
|
|
7408
|
-
this.keyListeners.forEach((unregister) => unregister());
|
|
7409
|
-
this.keyListeners = [];
|
|
7410
|
-
}
|
|
7411
|
-
|
|
7412
7864
|
#filterOptions = async () => {
|
|
7413
7865
|
if (this.initialPrompt) {
|
|
7414
7866
|
this.initialPrompt = false;
|
|
@@ -7585,7 +8037,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7585
8037
|
popoverContainer.style.position = "absolute";
|
|
7586
8038
|
popoverContainer.setAttribute("nonce", getNonce());
|
|
7587
8039
|
popoverContainer.append(...await this.source.buildListItems());
|
|
7588
|
-
|
|
8040
|
+
this.#globalListeners.track(registerEventListener(popoverContainer, "click", this.#handlePopoverClick));
|
|
7589
8041
|
this.#editorElement.appendChild(popoverContainer);
|
|
7590
8042
|
return popoverContainer
|
|
7591
8043
|
}
|
|
@@ -7606,12 +8058,14 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7606
8058
|
|
|
7607
8059
|
class CodeLanguagePicker extends HTMLElement {
|
|
7608
8060
|
#abortController = null
|
|
8061
|
+
#listeners = new ListenerBin()
|
|
7609
8062
|
|
|
7610
8063
|
connectedCallback() {
|
|
7611
8064
|
this.editorElement = this.closest("lexxy-editor");
|
|
7612
8065
|
this.editor = this.editorElement.editor;
|
|
7613
8066
|
this.classList.add("lexxy-floating-controls");
|
|
7614
8067
|
this.#abortController = new AbortController();
|
|
8068
|
+
this.#listeners.track(() => this.#abortController?.abort());
|
|
7615
8069
|
|
|
7616
8070
|
this.#attachLanguagePicker();
|
|
7617
8071
|
this.#hide();
|
|
@@ -7623,10 +8077,7 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
7623
8077
|
}
|
|
7624
8078
|
|
|
7625
8079
|
dispose() {
|
|
7626
|
-
this.#
|
|
7627
|
-
this.#abortController = null;
|
|
7628
|
-
this.unregisterUpdateListener?.();
|
|
7629
|
-
this.unregisterUpdateListener = null;
|
|
8080
|
+
this.#listeners.dispose();
|
|
7630
8081
|
}
|
|
7631
8082
|
|
|
7632
8083
|
#attachLanguagePicker() {
|
|
@@ -7634,13 +8085,13 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
7634
8085
|
|
|
7635
8086
|
const signal = this.#abortController.signal;
|
|
7636
8087
|
|
|
7637
|
-
this.languagePickerElement
|
|
8088
|
+
this.#listeners.track(registerEventListener(this.languagePickerElement, "change", () => {
|
|
7638
8089
|
this.#updateCodeBlockLanguage(this.languagePickerElement.value);
|
|
7639
|
-
}, { signal });
|
|
8090
|
+
}, { signal }));
|
|
7640
8091
|
|
|
7641
|
-
this.languagePickerElement
|
|
8092
|
+
this.#listeners.track(registerEventListener(this.languagePickerElement, "mousedown", (event) => {
|
|
7642
8093
|
this.#dispatchOpenEvent(event);
|
|
7643
|
-
}, { signal });
|
|
8094
|
+
}, { signal }));
|
|
7644
8095
|
|
|
7645
8096
|
this.languagePickerElement.setAttribute("nonce", getNonce());
|
|
7646
8097
|
this.appendChild(this.languagePickerElement);
|
|
@@ -7666,20 +8117,18 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
7666
8117
|
get #languages() {
|
|
7667
8118
|
const languages = { ...CODE_LANGUAGE_FRIENDLY_NAME_MAP };
|
|
7668
8119
|
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
const sortedEntries = Object.entries(languages)
|
|
7677
|
-
.sort(([ , a ], [ , b ]) => a.localeCompare(b));
|
|
8120
|
+
languages.ruby ||= "Ruby";
|
|
8121
|
+
languages.php ||= "PHP";
|
|
8122
|
+
languages.go ||= "Go";
|
|
8123
|
+
languages.bash ||= "Bash";
|
|
8124
|
+
languages.json ||= "JSON";
|
|
8125
|
+
languages.diff ||= "Diff";
|
|
7678
8126
|
|
|
7679
8127
|
// Place the "plain" entry first, then the rest of language sorted alphabetically
|
|
7680
|
-
|
|
7681
|
-
const
|
|
7682
|
-
|
|
8128
|
+
delete languages.plain;
|
|
8129
|
+
const sortedEntries = Object.entries(languages)
|
|
8130
|
+
.sort((a, b) => a[1].localeCompare(b[1]));
|
|
8131
|
+
return { plain: "Plain text", ...Object.fromEntries(sortedEntries) }
|
|
7683
8132
|
}
|
|
7684
8133
|
|
|
7685
8134
|
#dispatchOpenEvent(event) {
|
|
@@ -7708,8 +8157,8 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
7708
8157
|
}
|
|
7709
8158
|
|
|
7710
8159
|
#monitorForCodeBlockSelection() {
|
|
7711
|
-
this.
|
|
7712
|
-
|
|
8160
|
+
this.#listeners.track(this.editor.registerUpdateListener(({ editorState }) => {
|
|
8161
|
+
editorState.read(() => {
|
|
7713
8162
|
const codeNode = this.#getCurrentCodeNode();
|
|
7714
8163
|
|
|
7715
8164
|
if (codeNode) {
|
|
@@ -7718,26 +8167,11 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
7718
8167
|
this.#hide();
|
|
7719
8168
|
}
|
|
7720
8169
|
});
|
|
7721
|
-
});
|
|
8170
|
+
}));
|
|
7722
8171
|
}
|
|
7723
8172
|
|
|
7724
8173
|
#getCurrentCodeNode() {
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
if (!$isRangeSelection(selection)) {
|
|
7728
|
-
return null
|
|
7729
|
-
}
|
|
7730
|
-
|
|
7731
|
-
const anchorNode = selection.anchor.getNode();
|
|
7732
|
-
const parentNode = anchorNode.getParent();
|
|
7733
|
-
|
|
7734
|
-
if ($isCodeNode(anchorNode)) {
|
|
7735
|
-
return anchorNode
|
|
7736
|
-
} else if ($isCodeNode(parentNode)) {
|
|
7737
|
-
return parentNode
|
|
7738
|
-
}
|
|
7739
|
-
|
|
7740
|
-
return null
|
|
8174
|
+
return this.editorElement.selection.nearestNodeOfType(CodeNode)
|
|
7741
8175
|
}
|
|
7742
8176
|
|
|
7743
8177
|
#codeNodeWasSelected(codeNode) {
|
|
@@ -7824,6 +8258,8 @@ class NodeDeleteButton extends HTMLElement {
|
|
|
7824
8258
|
}
|
|
7825
8259
|
|
|
7826
8260
|
class TableController {
|
|
8261
|
+
#listeners = new ListenerBin()
|
|
8262
|
+
|
|
7827
8263
|
constructor(editorElement) {
|
|
7828
8264
|
this.editor = editorElement.editor;
|
|
7829
8265
|
this.contents = editorElement.contents;
|
|
@@ -7839,7 +8275,7 @@ class TableController {
|
|
|
7839
8275
|
this.currentTableNodeKey = null;
|
|
7840
8276
|
this.currentCellKey = null;
|
|
7841
8277
|
|
|
7842
|
-
this.#
|
|
8278
|
+
this.#listeners.dispose();
|
|
7843
8279
|
}
|
|
7844
8280
|
|
|
7845
8281
|
get currentCell() {
|
|
@@ -8121,16 +8557,10 @@ class TableController {
|
|
|
8121
8557
|
|
|
8122
8558
|
#registerKeyHandlers() {
|
|
8123
8559
|
// We can't prevent these externally using regular keydown because Lexical handles it first.
|
|
8124
|
-
this.
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
#unregisterKeyHandlers() {
|
|
8129
|
-
this.unregisterBackspaceKeyHandler?.();
|
|
8130
|
-
this.unregisterEnterKeyHandler?.();
|
|
8131
|
-
|
|
8132
|
-
this.unregisterBackspaceKeyHandler = null;
|
|
8133
|
-
this.unregisterEnterKeyHandler = null;
|
|
8560
|
+
this.#listeners.track(
|
|
8561
|
+
this.editor.registerCommand(KEY_BACKSPACE_COMMAND, (event) => this.#handleBackspaceKey(event), COMMAND_PRIORITY_HIGH),
|
|
8562
|
+
this.editor.registerCommand(KEY_ENTER_COMMAND, (event) => this.#handleEnterKey(event), COMMAND_PRIORITY_HIGH)
|
|
8563
|
+
);
|
|
8134
8564
|
}
|
|
8135
8565
|
|
|
8136
8566
|
#handleBackspaceKey(event) {
|
|
@@ -8224,6 +8654,8 @@ var TableIcons = {
|
|
|
8224
8654
|
};
|
|
8225
8655
|
|
|
8226
8656
|
class TableTools extends HTMLElement {
|
|
8657
|
+
#listeners = new ListenerBin()
|
|
8658
|
+
|
|
8227
8659
|
connectedCallback() {
|
|
8228
8660
|
this.tableController = new TableController(this.#editorElement);
|
|
8229
8661
|
this.classList.add("lexxy-floating-controls");
|
|
@@ -8239,12 +8671,7 @@ class TableTools extends HTMLElement {
|
|
|
8239
8671
|
}
|
|
8240
8672
|
|
|
8241
8673
|
dispose() {
|
|
8242
|
-
this.#
|
|
8243
|
-
|
|
8244
|
-
this.unregisterUpdateListener?.();
|
|
8245
|
-
this.unregisterUpdateListener = null;
|
|
8246
|
-
|
|
8247
|
-
this.removeEventListener("keydown", this.#handleToolsKeydown);
|
|
8674
|
+
this.#listeners.dispose();
|
|
8248
8675
|
|
|
8249
8676
|
this.tableController?.destroy();
|
|
8250
8677
|
this.tableController = null;
|
|
@@ -8269,7 +8696,7 @@ class TableTools extends HTMLElement {
|
|
|
8269
8696
|
this.appendChild(this.#createColumnButtonsContainer());
|
|
8270
8697
|
|
|
8271
8698
|
this.appendChild(this.#createDeleteTableButton());
|
|
8272
|
-
this.
|
|
8699
|
+
this.#listeners.track(registerEventListener(this, "keydown", this.#handleToolsKeydown));
|
|
8273
8700
|
}
|
|
8274
8701
|
|
|
8275
8702
|
#createButtonsContainer(childType, setCountProperty, moreMenu) {
|
|
@@ -8362,12 +8789,7 @@ class TableTools extends HTMLElement {
|
|
|
8362
8789
|
}
|
|
8363
8790
|
|
|
8364
8791
|
#registerKeyboardShortcuts() {
|
|
8365
|
-
this.
|
|
8366
|
-
}
|
|
8367
|
-
|
|
8368
|
-
#unregisterKeyboardShortcuts() {
|
|
8369
|
-
this.unregisterKeyboardShortcuts?.();
|
|
8370
|
-
this.unregisterKeyboardShortcuts = null;
|
|
8792
|
+
this.#listeners.track(this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleAccessibilityShortcutKey, COMMAND_PRIORITY_HIGH));
|
|
8371
8793
|
}
|
|
8372
8794
|
|
|
8373
8795
|
#handleAccessibilityShortcutKey = (event) => {
|
|
@@ -8437,7 +8859,7 @@ class TableTools extends HTMLElement {
|
|
|
8437
8859
|
}
|
|
8438
8860
|
|
|
8439
8861
|
#monitorForTableSelection() {
|
|
8440
|
-
this.
|
|
8862
|
+
this.#listeners.track(this.#editor.registerUpdateListener(() => {
|
|
8441
8863
|
this.tableController.updateSelectedTable();
|
|
8442
8864
|
|
|
8443
8865
|
const tableNode = this.tableController.currentTableNode;
|
|
@@ -8446,7 +8868,7 @@ class TableTools extends HTMLElement {
|
|
|
8446
8868
|
} else {
|
|
8447
8869
|
this.#hide();
|
|
8448
8870
|
}
|
|
8449
|
-
});
|
|
8871
|
+
}));
|
|
8450
8872
|
}
|
|
8451
8873
|
|
|
8452
8874
|
#executeTableCommand(command) {
|