@37signals/lexxy 0.9.2-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 +1413 -639
- 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';
|
|
9
|
+
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, $descendantsMatching } from '@lexical/utils';
|
|
17
10
|
import { registerPlainText } from '@lexical/plain-text';
|
|
18
11
|
import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
19
12
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
20
13
|
import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, normalizeCodeLang, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
21
|
-
import {
|
|
14
|
+
import { TRANSFORMERS, registerMarkdownShortcuts } from '@lexical/markdown';
|
|
22
15
|
import { createEmptyHistoryState, registerHistory } from '@lexical/history';
|
|
23
|
-
import { createElement, extractPlainTextFromHtml, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
|
|
24
|
-
export { highlightCode as highlightAll, highlightCode } from './lexxy_helpers.esm.js';
|
|
25
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';
|
|
26
|
-
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $descendantsMatching } from '@lexical/utils';
|
|
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
|
-
|
|
1121
|
+
const global = new Configuration({
|
|
1122
|
+
attachmentTagName: "action-text-attachment",
|
|
1123
|
+
attachmentContentTypeNamespace: "actiontext",
|
|
1124
|
+
authenticatedUploads: false,
|
|
1125
|
+
extensions: []
|
|
1126
|
+
});
|
|
1127
|
+
|
|
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
|
+
}
|
|
1146
|
+
}
|
|
1113
1147
|
}
|
|
1148
|
+
});
|
|
1114
1149
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1150
|
+
var Lexxy = {
|
|
1151
|
+
global,
|
|
1152
|
+
presets,
|
|
1153
|
+
configure({ global: newGlobal, ...newPresets }) {
|
|
1154
|
+
if (newGlobal) {
|
|
1155
|
+
global.merge(newGlobal);
|
|
1119
1156
|
}
|
|
1157
|
+
presets.merge(newPresets);
|
|
1120
1158
|
}
|
|
1159
|
+
};
|
|
1121
1160
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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);
|
|
@@ -1409,6 +1455,24 @@ function isSelectionHighlighted(selection) {
|
|
|
1409
1455
|
}
|
|
1410
1456
|
}
|
|
1411
1457
|
|
|
1458
|
+
function getHighlightStyles(selection) {
|
|
1459
|
+
if (!$isRangeSelection(selection)) return null
|
|
1460
|
+
|
|
1461
|
+
let styles = getStyleObjectFromCSS(selection.style);
|
|
1462
|
+
if (!styles.color && !styles["background-color"]) {
|
|
1463
|
+
const anchorNode = selection.anchor.getNode();
|
|
1464
|
+
if ($isTextNode(anchorNode)) {
|
|
1465
|
+
styles = getStyleObjectFromCSS(anchorNode.getStyle());
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
const color = styles.color || null;
|
|
1470
|
+
const backgroundColor = styles["background-color"] || null;
|
|
1471
|
+
if (!color && !backgroundColor) return null
|
|
1472
|
+
|
|
1473
|
+
return { color, backgroundColor }
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1412
1476
|
function hasHighlightStyles(cssOrStyles) {
|
|
1413
1477
|
const styles = typeof cssOrStyles === "string" ? getStyleObjectFromCSS(cssOrStyles) : cssOrStyles;
|
|
1414
1478
|
return !!(styles.color || styles["background-color"])
|
|
@@ -1496,6 +1560,10 @@ class LexxyExtension {
|
|
|
1496
1560
|
return null
|
|
1497
1561
|
}
|
|
1498
1562
|
|
|
1563
|
+
get allowedElements() {
|
|
1564
|
+
return []
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1499
1567
|
initializeToolbar(_lexxyToolbar) {
|
|
1500
1568
|
|
|
1501
1569
|
}
|
|
@@ -1718,6 +1786,13 @@ function $applyHighlightRangesToCodeNode(codeNode, highlights) {
|
|
|
1718
1786
|
const childRanges = $buildChildRanges(codeNode);
|
|
1719
1787
|
|
|
1720
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
|
+
|
|
1721
1796
|
// Check if this child overlaps with the highlight range
|
|
1722
1797
|
const overlapStart = Math.max(hlStart, nodeStart);
|
|
1723
1798
|
const overlapEnd = Math.min(hlEnd, nodeEnd);
|
|
@@ -1766,7 +1841,7 @@ function $buildChildRanges(codeNode) {
|
|
|
1766
1841
|
let charOffset = 0;
|
|
1767
1842
|
|
|
1768
1843
|
for (const child of codeNode.getChildren()) {
|
|
1769
|
-
if ($isCodeHighlightNode(child)) {
|
|
1844
|
+
if ($isCodeHighlightNode(child) || $isTextNode(child)) {
|
|
1770
1845
|
const text = child.getTextContent();
|
|
1771
1846
|
childRanges.push({ node: child, start: charOffset, end: charOffset + text.length });
|
|
1772
1847
|
charOffset += text.length;
|
|
@@ -1779,6 +1854,23 @@ function $buildChildRanges(codeNode) {
|
|
|
1779
1854
|
return childRanges
|
|
1780
1855
|
}
|
|
1781
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
|
+
|
|
1782
1874
|
function buildCanonicalizers(config) {
|
|
1783
1875
|
return [
|
|
1784
1876
|
new StyleCanonicalizer("color", [ ...config.buttons.color, ...config.permit.color ]),
|
|
@@ -1806,15 +1898,23 @@ function $toggleSelectionStyles(editor, styles) {
|
|
|
1806
1898
|
function $selectionIsInCodeBlock(selection) {
|
|
1807
1899
|
const nodes = selection.getNodes();
|
|
1808
1900
|
return nodes.some((node) => {
|
|
1809
|
-
|
|
1810
|
-
|
|
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)
|
|
1811
1908
|
})
|
|
1812
1909
|
}
|
|
1813
1910
|
|
|
1814
1911
|
function $patchCodeHighlightStyles(editor, selection, patch) {
|
|
1815
|
-
// 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.
|
|
1816
1916
|
const nodeKeys = selection.getNodes()
|
|
1817
|
-
.filter((node) => $isCodeHighlightNode(node))
|
|
1917
|
+
.filter((node) => ($isCodeHighlightNode(node) || $isTextNode(node)) && $isCodeNode(node.getParent()))
|
|
1818
1918
|
.map((node) => ({
|
|
1819
1919
|
key: node.getKey(),
|
|
1820
1920
|
startOffset: $getNodeSelectionOffsets(node, selection)[0],
|
|
@@ -1828,14 +1928,18 @@ function $patchCodeHighlightStyles(editor, selection, patch) {
|
|
|
1828
1928
|
// are committed before editor.focus() triggers a second update cycle
|
|
1829
1929
|
// that would re-run transforms and wipe out the styles.
|
|
1830
1930
|
editor.update(() => {
|
|
1931
|
+
const affectedCodeNodes = new Set();
|
|
1932
|
+
|
|
1831
1933
|
for (const { key, startOffset, endOffset, textSize } of nodeKeys) {
|
|
1832
1934
|
const node = $getNodeByKey(key);
|
|
1833
|
-
if (!node
|
|
1935
|
+
if (!node) continue
|
|
1834
1936
|
|
|
1835
1937
|
const parent = node.getParent();
|
|
1836
1938
|
if (!$isCodeNode(parent)) continue
|
|
1837
1939
|
if (startOffset === endOffset) continue
|
|
1838
1940
|
|
|
1941
|
+
affectedCodeNodes.add(parent);
|
|
1942
|
+
|
|
1839
1943
|
if (startOffset === 0 && endOffset === textSize) {
|
|
1840
1944
|
$applyStylePatchToNode(node, patch);
|
|
1841
1945
|
} else {
|
|
@@ -1844,6 +1948,17 @@ function $patchCodeHighlightStyles(editor, selection, patch) {
|
|
|
1844
1948
|
$applyStylePatchToNode(targetNode, patch);
|
|
1845
1949
|
}
|
|
1846
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
|
+
}
|
|
1847
1962
|
}, { skipTransforms: true, discrete: true });
|
|
1848
1963
|
}
|
|
1849
1964
|
|
|
@@ -1967,10 +2082,12 @@ const COMMANDS = [
|
|
|
1967
2082
|
"setFormatHeadingMedium",
|
|
1968
2083
|
"setFormatHeadingSmall",
|
|
1969
2084
|
"setFormatParagraph",
|
|
2085
|
+
"clearFormatting",
|
|
1970
2086
|
"insertUnorderedList",
|
|
1971
2087
|
"insertOrderedList",
|
|
1972
2088
|
"insertQuoteBlock",
|
|
1973
2089
|
"insertCodeBlock",
|
|
2090
|
+
"setCodeLanguage",
|
|
1974
2091
|
"insertHorizontalDivider",
|
|
1975
2092
|
"uploadImage",
|
|
1976
2093
|
"uploadFile",
|
|
@@ -1983,7 +2100,7 @@ const COMMANDS = [
|
|
|
1983
2100
|
|
|
1984
2101
|
class CommandDispatcher {
|
|
1985
2102
|
#selectionBeforeDrag = null
|
|
1986
|
-
#
|
|
2103
|
+
#listeners = new ListenerBin()
|
|
1987
2104
|
|
|
1988
2105
|
static configureFor(editorElement) {
|
|
1989
2106
|
return new CommandDispatcher(editorElement)
|
|
@@ -2046,7 +2163,14 @@ class CommandDispatcher {
|
|
|
2046
2163
|
}
|
|
2047
2164
|
|
|
2048
2165
|
dispatchUnlink() {
|
|
2049
|
-
this
|
|
2166
|
+
this.editor.update(() => {
|
|
2167
|
+
// Let adapters signal whether unlink should target a frozen link key.
|
|
2168
|
+
if (this.editorElement.adapter.unlinkFrozenNode?.()) {
|
|
2169
|
+
return
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
$toggleLink(null);
|
|
2173
|
+
});
|
|
2050
2174
|
}
|
|
2051
2175
|
|
|
2052
2176
|
dispatchInsertUnorderedList() {
|
|
@@ -2058,7 +2182,7 @@ class CommandDispatcher {
|
|
|
2058
2182
|
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "bullet") {
|
|
2059
2183
|
this.contents.applyParagraphFormat();
|
|
2060
2184
|
} else {
|
|
2061
|
-
this.
|
|
2185
|
+
this.contents.applyUnorderedListFormat();
|
|
2062
2186
|
}
|
|
2063
2187
|
}
|
|
2064
2188
|
|
|
@@ -2071,7 +2195,7 @@ class CommandDispatcher {
|
|
|
2071
2195
|
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "number") {
|
|
2072
2196
|
this.contents.applyParagraphFormat();
|
|
2073
2197
|
} else {
|
|
2074
|
-
this.
|
|
2198
|
+
this.contents.applyOrderedListFormat();
|
|
2075
2199
|
}
|
|
2076
2200
|
}
|
|
2077
2201
|
|
|
@@ -2137,6 +2261,17 @@ class CommandDispatcher {
|
|
|
2137
2261
|
}
|
|
2138
2262
|
}
|
|
2139
2263
|
|
|
2264
|
+
dispatchSetCodeLanguage(language) {
|
|
2265
|
+
this.editor.update(() => {
|
|
2266
|
+
if (!this.selection.isInsideCodeBlock) return
|
|
2267
|
+
|
|
2268
|
+
const codeNode = this.selection.nearestNodeOfType(CodeNode);
|
|
2269
|
+
if (!codeNode) return
|
|
2270
|
+
|
|
2271
|
+
codeNode.setLanguage(language);
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2140
2275
|
dispatchInsertHorizontalDivider() {
|
|
2141
2276
|
this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode());
|
|
2142
2277
|
this.editor.focus();
|
|
@@ -2158,6 +2293,10 @@ class CommandDispatcher {
|
|
|
2158
2293
|
this.contents.applyParagraphFormat();
|
|
2159
2294
|
}
|
|
2160
2295
|
|
|
2296
|
+
dispatchClearFormatting() {
|
|
2297
|
+
this.contents.clearFormatting();
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2161
2300
|
dispatchUploadImage() {
|
|
2162
2301
|
this.#dispatchUploadAttachment("image/*,video/*");
|
|
2163
2302
|
}
|
|
@@ -2199,10 +2338,7 @@ class CommandDispatcher {
|
|
|
2199
2338
|
}
|
|
2200
2339
|
|
|
2201
2340
|
dispose() {
|
|
2202
|
-
|
|
2203
|
-
const unregister = this.#unregister.pop();
|
|
2204
|
-
unregister();
|
|
2205
|
-
}
|
|
2341
|
+
this.#listeners.dispose();
|
|
2206
2342
|
}
|
|
2207
2343
|
|
|
2208
2344
|
#registerCommands() {
|
|
@@ -2215,7 +2351,7 @@ class CommandDispatcher {
|
|
|
2215
2351
|
}
|
|
2216
2352
|
|
|
2217
2353
|
#registerCommandHandler(command, priority, handler) {
|
|
2218
|
-
this.#
|
|
2354
|
+
this.#listeners.track(this.editor.registerCommand(command, handler, priority));
|
|
2219
2355
|
}
|
|
2220
2356
|
|
|
2221
2357
|
#registerKeyboardCommands() {
|
|
@@ -2240,10 +2376,13 @@ class CommandDispatcher {
|
|
|
2240
2376
|
#registerDragAndDropHandlers() {
|
|
2241
2377
|
if (this.editorElement.supportsAttachments) {
|
|
2242
2378
|
this.dragCounter = 0;
|
|
2243
|
-
this.editor.getRootElement()
|
|
2244
|
-
this
|
|
2245
|
-
|
|
2246
|
-
|
|
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
|
+
);
|
|
2247
2386
|
}
|
|
2248
2387
|
}
|
|
2249
2388
|
|
|
@@ -2335,16 +2474,6 @@ class CommandDispatcher {
|
|
|
2335
2474
|
return $isRangeSelection(selection) && selection.isCollapsed()
|
|
2336
2475
|
}
|
|
2337
2476
|
|
|
2338
|
-
// Not using TOGGLE_LINK_COMMAND because it's not handled unless you use React/LinkPlugin
|
|
2339
|
-
#toggleLink(url) {
|
|
2340
|
-
this.editor.update(() => {
|
|
2341
|
-
if (url === null) {
|
|
2342
|
-
$toggleLink(null);
|
|
2343
|
-
} else {
|
|
2344
|
-
$toggleLink(url);
|
|
2345
|
-
}
|
|
2346
|
-
});
|
|
2347
|
-
}
|
|
2348
2477
|
}
|
|
2349
2478
|
|
|
2350
2479
|
function capitalize(str) {
|
|
@@ -2482,13 +2611,15 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2482
2611
|
return Lexxy.global.get("attachmentTagName")
|
|
2483
2612
|
}
|
|
2484
2613
|
|
|
2485
|
-
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) {
|
|
2486
2615
|
super(key);
|
|
2487
2616
|
|
|
2488
2617
|
this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
|
|
2489
2618
|
this.sgid = sgid;
|
|
2490
2619
|
this.src = src;
|
|
2620
|
+
this.previewSrc = previewSrc;
|
|
2491
2621
|
this.previewable = parseBoolean(previewable);
|
|
2622
|
+
this.pendingPreview = pendingPreview;
|
|
2492
2623
|
this.altText = altText || "";
|
|
2493
2624
|
this.caption = caption || "";
|
|
2494
2625
|
this.contentType = contentType || "";
|
|
@@ -2496,16 +2627,23 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2496
2627
|
this.fileSize = fileSize;
|
|
2497
2628
|
this.width = width;
|
|
2498
2629
|
this.height = height;
|
|
2630
|
+
this.uploadError = uploadError;
|
|
2499
2631
|
|
|
2500
2632
|
this.editor = $getEditor();
|
|
2501
2633
|
}
|
|
2502
2634
|
|
|
2503
2635
|
createDOM() {
|
|
2636
|
+
if (this.uploadError) return this.createDOMForError()
|
|
2637
|
+
if (this.pendingPreview) return this.#createDOMForPendingPreview()
|
|
2638
|
+
|
|
2504
2639
|
const figure = this.createAttachmentFigure();
|
|
2505
2640
|
|
|
2506
2641
|
if (this.isPreviewableAttachment) {
|
|
2507
2642
|
figure.appendChild(this.#createDOMForImage());
|
|
2508
2643
|
figure.appendChild(this.#createEditableCaption());
|
|
2644
|
+
} else if (this.isVideo) {
|
|
2645
|
+
figure.appendChild(this.#createDOMForFile());
|
|
2646
|
+
figure.appendChild(this.#createEditableCaption());
|
|
2509
2647
|
} else {
|
|
2510
2648
|
figure.appendChild(this.#createDOMForFile());
|
|
2511
2649
|
figure.appendChild(this.#createDOMForNotImage());
|
|
@@ -2514,7 +2652,9 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2514
2652
|
return figure
|
|
2515
2653
|
}
|
|
2516
2654
|
|
|
2517
|
-
updateDOM(
|
|
2655
|
+
updateDOM(prevNode, dom) {
|
|
2656
|
+
if (this.uploadError !== prevNode.uploadError) return true
|
|
2657
|
+
|
|
2518
2658
|
const caption = dom.querySelector("figcaption textarea");
|
|
2519
2659
|
if (caption && this.caption) {
|
|
2520
2660
|
caption.value = this.caption;
|
|
@@ -2571,8 +2711,15 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2571
2711
|
return null
|
|
2572
2712
|
}
|
|
2573
2713
|
|
|
2574
|
-
|
|
2575
|
-
const figure = createAttachmentFigure(
|
|
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
|
+
|
|
2721
|
+
createAttachmentFigure(previewable = this.isPreviewableAttachment) {
|
|
2722
|
+
const figure = createAttachmentFigure(this.contentType, previewable, this.fileName);
|
|
2576
2723
|
figure.draggable = true;
|
|
2577
2724
|
figure.dataset.lexicalNodeKey = this.__key;
|
|
2578
2725
|
|
|
@@ -2590,32 +2737,147 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2590
2737
|
return isPreviewableImage(this.contentType)
|
|
2591
2738
|
}
|
|
2592
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
|
+
|
|
2593
2752
|
#createDOMForImage(options = {}) {
|
|
2594
|
-
const
|
|
2753
|
+
const initialSrc = this.previewSrc || this.src;
|
|
2754
|
+
const img = createElement("img", { src: initialSrc, draggable: false, alt: this.altText, ...this.#imageDimensions, ...options });
|
|
2595
2755
|
|
|
2596
2756
|
if (this.previewable && !this.isPreviewableImage) {
|
|
2597
2757
|
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
2598
2758
|
}
|
|
2599
2759
|
|
|
2760
|
+
if (this.previewSrc) {
|
|
2761
|
+
this.#preloadAndSwapSrc(img);
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2600
2764
|
const container = createElement("div", { className: "attachment__container" });
|
|
2601
2765
|
container.appendChild(img);
|
|
2602
2766
|
return container
|
|
2603
2767
|
}
|
|
2604
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
|
+
|
|
2605
2811
|
#swapPreviewToFileDOM(img) {
|
|
2606
2812
|
const figure = img.closest("figure.attachment");
|
|
2607
2813
|
if (!figure) return
|
|
2608
2814
|
|
|
2609
|
-
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;
|
|
2610
2824
|
|
|
2611
|
-
const
|
|
2612
|
-
|
|
2825
|
+
const tryLoad = () => {
|
|
2826
|
+
if (!this.editor.read(() => this.isAttached())) return
|
|
2613
2827
|
|
|
2614
|
-
|
|
2615
|
-
|
|
2828
|
+
const img = new Image();
|
|
2829
|
+
const cacheBustedSrc = `${this.src}${this.src.includes("?") ? "&" : "?"}_=${Date.now()}`;
|
|
2616
2830
|
|
|
2617
|
-
|
|
2618
|
-
|
|
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();
|
|
2619
2881
|
}
|
|
2620
2882
|
|
|
2621
2883
|
get #imageDimensions() {
|
|
@@ -2706,7 +2968,7 @@ function $isActionTextAttachmentNode(node) {
|
|
|
2706
2968
|
}
|
|
2707
2969
|
|
|
2708
2970
|
class Selection {
|
|
2709
|
-
#
|
|
2971
|
+
#listeners = new ListenerBin()
|
|
2710
2972
|
|
|
2711
2973
|
constructor(editorElement) {
|
|
2712
2974
|
this.editorElement = editorElement;
|
|
@@ -2857,6 +3119,15 @@ class Selection {
|
|
|
2857
3119
|
const anchorElement = anchorNode.getTopLevelElement();
|
|
2858
3120
|
if (!anchorElement) return false
|
|
2859
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
|
+
|
|
2860
3131
|
const nodes = selection.getNodes();
|
|
2861
3132
|
for (const node of nodes) {
|
|
2862
3133
|
if ($isLineBreakNode(node)) {
|
|
@@ -2970,10 +3241,7 @@ class Selection {
|
|
|
2970
3241
|
this.editor = null;
|
|
2971
3242
|
this.previouslySelectedKeys = null;
|
|
2972
3243
|
|
|
2973
|
-
|
|
2974
|
-
const unregister = this.#unregister.pop();
|
|
2975
|
-
unregister();
|
|
2976
|
-
}
|
|
3244
|
+
this.#listeners.dispose();
|
|
2977
3245
|
}
|
|
2978
3246
|
|
|
2979
3247
|
// When all inline code text is deleted, Lexical's selection retains the stale
|
|
@@ -2991,7 +3259,7 @@ class Selection {
|
|
|
2991
3259
|
// detects that stale state and clears it so newly typed text won't be
|
|
2992
3260
|
// code-formatted.
|
|
2993
3261
|
#clearStaleInlineCodeFormat() {
|
|
2994
|
-
this.#
|
|
3262
|
+
this.#listeners.track(this.editor.registerUpdateListener(({ editorState, tags }) => {
|
|
2995
3263
|
if (tags.has("history-merge") || tags.has("skip-dom-selection")) return
|
|
2996
3264
|
|
|
2997
3265
|
let isStale = false;
|
|
@@ -3039,7 +3307,7 @@ class Selection {
|
|
|
3039
3307
|
}
|
|
3040
3308
|
|
|
3041
3309
|
#processSelectionChangeCommands() {
|
|
3042
|
-
this.#
|
|
3310
|
+
this.#listeners.track(
|
|
3043
3311
|
this.editor.registerCommand(KEY_ARROW_LEFT_COMMAND, this.#selectPreviousNode.bind(this), COMMAND_PRIORITY_LOW),
|
|
3044
3312
|
this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, this.#selectNextNode.bind(this), COMMAND_PRIORITY_LOW),
|
|
3045
3313
|
this.editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#selectPreviousTopLevelNode.bind(this), COMMAND_PRIORITY_LOW),
|
|
@@ -3050,21 +3318,21 @@ class Selection {
|
|
|
3050
3318
|
this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
3051
3319
|
this.current = $getSelection();
|
|
3052
3320
|
}, COMMAND_PRIORITY_LOW)
|
|
3053
|
-
)
|
|
3321
|
+
);
|
|
3054
3322
|
}
|
|
3055
3323
|
|
|
3056
3324
|
#listenForNodeSelections() {
|
|
3057
|
-
this.#
|
|
3325
|
+
this.#listeners.track(this.editor.registerCommand(CLICK_COMMAND, ({ target }) => {
|
|
3058
3326
|
if (!isDOMNode(target)) return false
|
|
3059
3327
|
|
|
3060
3328
|
const targetNode = $getNearestNodeFromDOMNode(target);
|
|
3061
3329
|
return $isDecoratorNode(targetNode) && this.#selectInLexical(targetNode)
|
|
3062
3330
|
}, COMMAND_PRIORITY_LOW));
|
|
3063
3331
|
|
|
3064
|
-
const moveNextLineHandler = () => this.#selectOrAppendNextLine();
|
|
3065
3332
|
const rootElement = this.editor.getRootElement();
|
|
3066
|
-
|
|
3067
|
-
|
|
3333
|
+
this.#listeners.track(
|
|
3334
|
+
registerEventListener(rootElement, "lexxy:internal:move-to-next-line", () => this.#selectOrAppendNextLine())
|
|
3335
|
+
);
|
|
3068
3336
|
}
|
|
3069
3337
|
|
|
3070
3338
|
#containEditorFocus() {
|
|
@@ -3269,13 +3537,20 @@ class Selection {
|
|
|
3269
3537
|
}
|
|
3270
3538
|
|
|
3271
3539
|
// When backspace is pressed on an empty list item that has siblings,
|
|
3272
|
-
//
|
|
3273
|
-
//
|
|
3274
|
-
//
|
|
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.
|
|
3275
3546
|
//
|
|
3276
|
-
//
|
|
3277
|
-
//
|
|
3278
|
-
// the
|
|
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.
|
|
3550
|
+
//
|
|
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.
|
|
3279
3554
|
#removeEmptyListItem() {
|
|
3280
3555
|
const selection = $getSelection();
|
|
3281
3556
|
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
|
|
@@ -3292,11 +3567,17 @@ class Selection {
|
|
|
3292
3567
|
const previousSibling = listItem.getPreviousSibling();
|
|
3293
3568
|
if (previousSibling) {
|
|
3294
3569
|
previousSibling.selectEnd();
|
|
3295
|
-
|
|
3296
|
-
|
|
3570
|
+
listItem.remove();
|
|
3571
|
+
return true
|
|
3297
3572
|
}
|
|
3298
3573
|
|
|
3574
|
+
const listNode = $getNearestNodeOfType(listItem, ListNode);
|
|
3575
|
+
if (!listNode) return false
|
|
3576
|
+
|
|
3577
|
+
const paragraph = $createParagraphNode();
|
|
3578
|
+
listNode.insertBefore(paragraph);
|
|
3299
3579
|
listItem.remove();
|
|
3580
|
+
paragraph.selectStart();
|
|
3300
3581
|
return true
|
|
3301
3582
|
}
|
|
3302
3583
|
|
|
@@ -3556,10 +3837,6 @@ class Selection {
|
|
|
3556
3837
|
}
|
|
3557
3838
|
}
|
|
3558
3839
|
|
|
3559
|
-
function sanitize(html) {
|
|
3560
|
-
return DOMPurify.sanitize(html, buildConfig())
|
|
3561
|
-
}
|
|
3562
|
-
|
|
3563
3840
|
class EditorConfiguration {
|
|
3564
3841
|
#editorElement
|
|
3565
3842
|
#config
|
|
@@ -3602,6 +3879,107 @@ class EditorConfiguration {
|
|
|
3602
3879
|
}
|
|
3603
3880
|
}
|
|
3604
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
|
+
|
|
3605
3983
|
async function loadFileIntoImage(file, image) {
|
|
3606
3984
|
return new Promise((resolve) => {
|
|
3607
3985
|
const reader = new FileReader();
|
|
@@ -3650,16 +4028,19 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3650
4028
|
}
|
|
3651
4029
|
|
|
3652
4030
|
createDOM() {
|
|
3653
|
-
if (this.uploadError) return this
|
|
4031
|
+
if (this.uploadError) return this.createDOMForError()
|
|
3654
4032
|
|
|
3655
4033
|
// This side-effect is trigged on DOM load to fire only once and avoid multiple
|
|
3656
4034
|
// uploads through cloning. The upload is guarded from restarting in case the
|
|
3657
4035
|
// node is reloaded from saved state such as from history.
|
|
3658
4036
|
this.#startUploadIfNeeded();
|
|
3659
4037
|
|
|
3660
|
-
|
|
4038
|
+
// Bridge-managed uploads (uploadUrl is null) don't have file data to show
|
|
4039
|
+
// an image preview, so always show the file icon during upload.
|
|
4040
|
+
const canPreviewFile = this.isPreviewableAttachment && this.uploadUrl != null;
|
|
4041
|
+
const figure = this.createAttachmentFigure(canPreviewFile);
|
|
3661
4042
|
|
|
3662
|
-
if (
|
|
4043
|
+
if (canPreviewFile) {
|
|
3663
4044
|
const img = figure.appendChild(this.#createDOMForImage());
|
|
3664
4045
|
|
|
3665
4046
|
// load file locally to set dimensions and prevent vertical shifting
|
|
@@ -3707,13 +4088,6 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3707
4088
|
return this.progress !== null
|
|
3708
4089
|
}
|
|
3709
4090
|
|
|
3710
|
-
#createDOMForError() {
|
|
3711
|
-
const figure = this.createAttachmentFigure();
|
|
3712
|
-
figure.classList.add("attachment--error");
|
|
3713
|
-
figure.appendChild(createElement("div", { innerText: `Error uploading ${this.file?.name ?? "file"}` }));
|
|
3714
|
-
return figure
|
|
3715
|
-
}
|
|
3716
|
-
|
|
3717
4091
|
#createDOMForImage() {
|
|
3718
4092
|
return createElement("img")
|
|
3719
4093
|
}
|
|
@@ -3759,6 +4133,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3759
4133
|
|
|
3760
4134
|
async #startUploadIfNeeded() {
|
|
3761
4135
|
if (this.#uploadStarted) return
|
|
4136
|
+
if (!this.uploadUrl) return // Bridge-managed upload — skip DirectUpload
|
|
3762
4137
|
|
|
3763
4138
|
this.#setUploadStarted();
|
|
3764
4139
|
|
|
@@ -3775,7 +4150,9 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3775
4150
|
this.#handleUploadError(error);
|
|
3776
4151
|
} else {
|
|
3777
4152
|
this.#dispatchEvent("lexxy:upload-end", { file: this.file, error: null });
|
|
3778
|
-
this
|
|
4153
|
+
this.editor.update(() => {
|
|
4154
|
+
this.showUploadedAttachment(blob);
|
|
4155
|
+
}, { tag: this.#backgroundUpdateTags });
|
|
3779
4156
|
}
|
|
3780
4157
|
});
|
|
3781
4158
|
}
|
|
@@ -3819,17 +4196,18 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3819
4196
|
}, { tag: this.#backgroundUpdateTags });
|
|
3820
4197
|
}
|
|
3821
4198
|
|
|
3822
|
-
|
|
3823
|
-
const
|
|
4199
|
+
showUploadedAttachment(blob) {
|
|
4200
|
+
const previewSrc = this.isPreviewableImage && this.file ? URL.createObjectURL(this.file) : null;
|
|
3824
4201
|
|
|
3825
|
-
this
|
|
3826
|
-
|
|
3827
|
-
|
|
4202
|
+
const replacementNode = this.#toActionTextAttachmentNodeWith(blob, previewSrc);
|
|
4203
|
+
const shouldSelectAfterReplacement = this.#selectionIncludesUploadNode;
|
|
4204
|
+
this.replace(replacementNode);
|
|
3828
4205
|
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
4206
|
+
if (shouldSelectAfterReplacement && $isRootOrShadowRoot(replacementNode.getParent())) {
|
|
4207
|
+
replacementNode.selectNext();
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4210
|
+
return replacementNode.getKey()
|
|
3833
4211
|
}
|
|
3834
4212
|
|
|
3835
4213
|
// Upload lifecycle methods (progress, completion, errors) run asynchronously and may
|
|
@@ -3849,8 +4227,22 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3849
4227
|
return rootElement !== null && rootElement.contains(document.activeElement)
|
|
3850
4228
|
}
|
|
3851
4229
|
|
|
3852
|
-
#
|
|
3853
|
-
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);
|
|
3854
4246
|
return conversion.toAttachmentNode()
|
|
3855
4247
|
}
|
|
3856
4248
|
|
|
@@ -3861,16 +4253,19 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
3861
4253
|
}
|
|
3862
4254
|
|
|
3863
4255
|
class AttachmentNodeConversion {
|
|
3864
|
-
constructor(uploadNode, blob) {
|
|
4256
|
+
constructor(uploadNode, blob, previewSrc) {
|
|
3865
4257
|
this.uploadNode = uploadNode;
|
|
3866
4258
|
this.blob = blob;
|
|
4259
|
+
this.previewSrc = previewSrc;
|
|
3867
4260
|
}
|
|
3868
4261
|
|
|
3869
4262
|
toAttachmentNode() {
|
|
3870
4263
|
return new ActionTextAttachmentNode({
|
|
3871
4264
|
...this.uploadNode,
|
|
3872
4265
|
...this.#propertiesFromBlob,
|
|
3873
|
-
src: this.#src
|
|
4266
|
+
src: this.#src,
|
|
4267
|
+
previewSrc: this.previewSrc,
|
|
4268
|
+
pendingPreview: this.blob.previewable && !this.uploadNode.isPreviewableImage
|
|
3874
4269
|
})
|
|
3875
4270
|
}
|
|
3876
4271
|
|
|
@@ -4109,7 +4504,7 @@ class Uploader {
|
|
|
4109
4504
|
}
|
|
4110
4505
|
|
|
4111
4506
|
$insertUploadNodes() {
|
|
4112
|
-
this.
|
|
4507
|
+
this.contents.insertAtCursor(...this.nodes);
|
|
4113
4508
|
}
|
|
4114
4509
|
|
|
4115
4510
|
get #nodeUrlProperties() {
|
|
@@ -4206,43 +4601,30 @@ class Contents {
|
|
|
4206
4601
|
this.editor = null;
|
|
4207
4602
|
}
|
|
4208
4603
|
|
|
4604
|
+
get selection() { return this.editorElement.selection }
|
|
4605
|
+
|
|
4209
4606
|
insertHtml(html, { tag } = {}) {
|
|
4210
4607
|
this.insertDOM(parseHtml(html), { tag });
|
|
4211
4608
|
}
|
|
4212
4609
|
|
|
4213
4610
|
insertDOM(doc, { tag } = {}) {
|
|
4214
4611
|
this.#unwrapPlaceholderAnchors(doc);
|
|
4215
|
-
if (tag === PASTE_TAG) this.#stripTableCellColorStyles(doc);
|
|
4216
4612
|
|
|
4217
4613
|
this.editor.update(() => {
|
|
4218
|
-
|
|
4219
|
-
if (!$isRangeSelection(selection)) return
|
|
4614
|
+
if ($hasUpdateTag(PASTE_TAG)) this.#stripTableCellColorStyles(doc);
|
|
4220
4615
|
|
|
4221
4616
|
const nodes = $generateNodesFromDOM(this.editor, doc);
|
|
4222
4617
|
if (!this.#insertUploadNodes(nodes)) {
|
|
4223
|
-
|
|
4618
|
+
this.insertAtCursor(...nodes);
|
|
4224
4619
|
}
|
|
4225
4620
|
}, { tag });
|
|
4226
4621
|
}
|
|
4227
4622
|
|
|
4228
|
-
insertAtCursor(
|
|
4229
|
-
|
|
4230
|
-
const
|
|
4623
|
+
insertAtCursor(...nodes) {
|
|
4624
|
+
const selection = $getSelection() ?? $getRoot().selectEnd();
|
|
4625
|
+
const inserter = NodeInserter.for(selection);
|
|
4231
4626
|
|
|
4232
|
-
|
|
4233
|
-
const anchorNode = selection.anchor.getNode();
|
|
4234
|
-
if ($isShadowRoot(anchorNode)) {
|
|
4235
|
-
const paragraph = $createParagraphNode();
|
|
4236
|
-
anchorNode.append(paragraph);
|
|
4237
|
-
selection = paragraph.selectStart();
|
|
4238
|
-
}
|
|
4239
|
-
selection.insertNodes([ node ]);
|
|
4240
|
-
} else if ($isNodeSelection(selection) && selectedNodes.length > 0) {
|
|
4241
|
-
// Overrides Lexical's default behavior of _removing_ the currently selected nodes
|
|
4242
|
-
// https://github.com/facebook/lexical/blob/v0.38.2/packages/lexical/src/LexicalSelection.ts#L412
|
|
4243
|
-
const lastNode = selectedNodes.at(-1);
|
|
4244
|
-
lastNode.insertAfter(node);
|
|
4245
|
-
}
|
|
4627
|
+
inserter.insertNodes(nodes);
|
|
4246
4628
|
}
|
|
4247
4629
|
|
|
4248
4630
|
insertAtCursorEnsuringLineBelow(node) {
|
|
@@ -4264,11 +4646,30 @@ class Contents {
|
|
|
4264
4646
|
$setBlocksType(selection, () => $createHeadingNode(tag));
|
|
4265
4647
|
}
|
|
4266
4648
|
|
|
4267
|
-
|
|
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() {
|
|
4268
4660
|
const selection = $getSelection();
|
|
4269
4661
|
if (!$isRangeSelection(selection)) return
|
|
4270
4662
|
|
|
4271
|
-
$
|
|
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());
|
|
4272
4673
|
}
|
|
4273
4674
|
|
|
4274
4675
|
toggleCodeBlock() {
|
|
@@ -4277,12 +4678,16 @@ class Contents {
|
|
|
4277
4678
|
|
|
4278
4679
|
if (this.#insertNodeIfRoot($createCodeNode("plain"))) return
|
|
4279
4680
|
|
|
4280
|
-
const
|
|
4681
|
+
const blockElements = this.#blockLevelElementsInSelection(selection);
|
|
4682
|
+
const allCode = blockElements.every($isCodeNode);
|
|
4281
4683
|
|
|
4282
|
-
if (
|
|
4283
|
-
this.#
|
|
4684
|
+
if (allCode) {
|
|
4685
|
+
blockElements.forEach(node => this.#unwrapCodeBlock(node));
|
|
4284
4686
|
} else {
|
|
4285
|
-
|
|
4687
|
+
const codeNode = $createCodeNode("plain");
|
|
4688
|
+
blockElements.at(-1).insertAfter(codeNode);
|
|
4689
|
+
codeNode.selectEnd();
|
|
4690
|
+
this.insertAtCursor(...blockElements);
|
|
4286
4691
|
}
|
|
4287
4692
|
}
|
|
4288
4693
|
|
|
@@ -4431,6 +4836,53 @@ class Contents {
|
|
|
4431
4836
|
});
|
|
4432
4837
|
}
|
|
4433
4838
|
|
|
4839
|
+
insertPendingAttachment(file) {
|
|
4840
|
+
if (!this.editorElement.supportsAttachments) return null
|
|
4841
|
+
|
|
4842
|
+
let nodeKey = null;
|
|
4843
|
+
this.editor.update(() => {
|
|
4844
|
+
const uploadNode = new ActionTextAttachmentUploadNode({
|
|
4845
|
+
file,
|
|
4846
|
+
uploadUrl: null,
|
|
4847
|
+
blobUrlTemplate: this.editorElement.blobUrlTemplate,
|
|
4848
|
+
editor: this.editor
|
|
4849
|
+
});
|
|
4850
|
+
this.insertAtCursor(uploadNode);
|
|
4851
|
+
nodeKey = uploadNode.getKey();
|
|
4852
|
+
}, { tag: HISTORY_MERGE_TAG });
|
|
4853
|
+
|
|
4854
|
+
if (!nodeKey) return null
|
|
4855
|
+
|
|
4856
|
+
const editor = this.editor;
|
|
4857
|
+
return {
|
|
4858
|
+
setAttributes(blob) {
|
|
4859
|
+
editor.update(() => {
|
|
4860
|
+
const node = $getNodeByKey(nodeKey);
|
|
4861
|
+
if (!(node instanceof ActionTextAttachmentUploadNode)) return
|
|
4862
|
+
|
|
4863
|
+
const replacementNodeKey = node.showUploadedAttachment(blob);
|
|
4864
|
+
if (replacementNodeKey) {
|
|
4865
|
+
nodeKey = replacementNodeKey;
|
|
4866
|
+
}
|
|
4867
|
+
}, { tag: HISTORY_MERGE_TAG });
|
|
4868
|
+
},
|
|
4869
|
+
setUploadProgress(progress) {
|
|
4870
|
+
editor.update(() => {
|
|
4871
|
+
const node = $getNodeByKey(nodeKey);
|
|
4872
|
+
if (!(node instanceof ActionTextAttachmentUploadNode)) return
|
|
4873
|
+
|
|
4874
|
+
node.getWritable().progress = progress;
|
|
4875
|
+
}, { tag: HISTORY_MERGE_TAG });
|
|
4876
|
+
},
|
|
4877
|
+
remove() {
|
|
4878
|
+
editor.update(() => {
|
|
4879
|
+
const node = $getNodeByKey(nodeKey);
|
|
4880
|
+
if (node) node.remove();
|
|
4881
|
+
});
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4434
4886
|
replaceNodeWithHTML(nodeKey, html, options = {}) {
|
|
4435
4887
|
this.editor.update(() => {
|
|
4436
4888
|
const node = $getNodeByKey(nodeKey);
|
|
@@ -4484,9 +4936,42 @@ class Contents {
|
|
|
4484
4936
|
return false
|
|
4485
4937
|
}
|
|
4486
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
|
+
|
|
4487
4972
|
#splitParagraphsAtLineBreaks(selection) {
|
|
4488
|
-
const
|
|
4489
|
-
const
|
|
4973
|
+
const anchorTopLevel = selection.anchor.getNode().getTopLevelElement();
|
|
4974
|
+
const focusTopLevel = selection.focus.getNode().getTopLevelElement();
|
|
4490
4975
|
const topLevelElements = this.#topLevelElementsInSelection(selection);
|
|
4491
4976
|
|
|
4492
4977
|
for (const element of topLevelElements) {
|
|
@@ -4498,10 +4983,9 @@ class Contents {
|
|
|
4498
4983
|
// Check whether this paragraph needs splitting: skip only if neither
|
|
4499
4984
|
// selection endpoint is inside it (meaning it's a middle paragraph
|
|
4500
4985
|
// fully between anchor and focus with no partial lines to split off).
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
)
|
|
4504
|
-
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
|
|
4505
4989
|
|
|
4506
4990
|
const groups = [ [] ];
|
|
4507
4991
|
for (const child of children) {
|
|
@@ -4523,6 +5007,15 @@ class Contents {
|
|
|
4523
5007
|
}
|
|
4524
5008
|
}
|
|
4525
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
|
+
|
|
4526
5019
|
#topLevelElementsInSelection(selection) {
|
|
4527
5020
|
const elements = new Set();
|
|
4528
5021
|
for (const node of selection.getNodes()) {
|
|
@@ -4710,6 +5203,109 @@ function $isShadowRoot(node) {
|
|
|
4710
5203
|
return $isElementNode(node) && $isRootOrShadowRoot(node) && !$isRootNode(node)
|
|
4711
5204
|
}
|
|
4712
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
|
+
|
|
4713
5309
|
class Clipboard {
|
|
4714
5310
|
constructor(editorElement) {
|
|
4715
5311
|
this.editorElement = editorElement;
|
|
@@ -4889,9 +5485,31 @@ class Extensions {
|
|
|
4889
5485
|
}
|
|
4890
5486
|
|
|
4891
5487
|
initializeToolbars() {
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
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)
|
|
4895
5513
|
}
|
|
4896
5514
|
|
|
4897
5515
|
get #lexxyToolbar() {
|
|
@@ -4915,7 +5533,19 @@ class Extensions {
|
|
|
4915
5533
|
}
|
|
4916
5534
|
}
|
|
4917
5535
|
|
|
4918
|
-
|
|
5536
|
+
class BrowserAdapter {
|
|
5537
|
+
frozenLinkKey = null
|
|
5538
|
+
|
|
5539
|
+
dispatchAttributesChange(attributes, linkHref, highlight, headingTag) {}
|
|
5540
|
+
dispatchEditorInitialized(detail) {}
|
|
5541
|
+
freeze() {}
|
|
5542
|
+
thaw() {}
|
|
5543
|
+
unlinkFrozenNode() {
|
|
5544
|
+
return false
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5547
|
+
|
|
5548
|
+
// Custom TextNode exportDOM that avoids redundant wrapping.
|
|
4919
5549
|
//
|
|
4920
5550
|
// Lexical's built-in TextNode.exportDOM() calls createDOM() which produces semantic tags
|
|
4921
5551
|
// like <strong> for bold and <em> for italic, then unconditionally wraps the result
|
|
@@ -4925,6 +5555,9 @@ class Extensions {
|
|
|
4925
5555
|
// This custom export skips <b> when <strong> is already present and <i> when <em> is
|
|
4926
5556
|
// already present, while preserving <s> and <u> wrappers which have no semantic equivalents
|
|
4927
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.
|
|
4928
5561
|
|
|
4929
5562
|
function exportTextNodeDOM(editor, textNode) {
|
|
4930
5563
|
const element = textNode.createDOM(editor._config, editor);
|
|
@@ -4932,142 +5565,51 @@ function exportTextNodeDOM(editor, textNode) {
|
|
|
4932
5565
|
|
|
4933
5566
|
if (textNode.hasFormat("lowercase")) {
|
|
4934
5567
|
element.style.textTransform = "lowercase";
|
|
4935
|
-
} else if (textNode.hasFormat("uppercase")) {
|
|
4936
|
-
element.style.textTransform = "uppercase";
|
|
4937
|
-
} else if (textNode.hasFormat("capitalize")) {
|
|
4938
|
-
element.style.textTransform = "capitalize";
|
|
4939
|
-
}
|
|
4940
|
-
|
|
4941
|
-
let result = element;
|
|
4942
|
-
|
|
4943
|
-
if (textNode.hasFormat("bold") && !containsTag(element, "strong")) {
|
|
4944
|
-
result = wrapWith(result, "b");
|
|
4945
|
-
}
|
|
4946
|
-
if (textNode.hasFormat("italic") && !containsTag(element, "em")) {
|
|
4947
|
-
result = wrapWith(result, "i");
|
|
4948
|
-
}
|
|
4949
|
-
if (textNode.hasFormat("strikethrough")) {
|
|
4950
|
-
result = wrapWith(result, "s");
|
|
4951
|
-
}
|
|
4952
|
-
if (textNode.hasFormat("underline")) {
|
|
4953
|
-
result = wrapWith(result, "u");
|
|
4954
|
-
}
|
|
4955
|
-
|
|
4956
|
-
return { element: result }
|
|
4957
|
-
}
|
|
4958
|
-
|
|
4959
|
-
function containsTag(element, tagName) {
|
|
4960
|
-
const upperTag = tagName.toUpperCase();
|
|
4961
|
-
if (element.tagName === upperTag) return true
|
|
4962
|
-
|
|
4963
|
-
return element.querySelector(tagName) !== null
|
|
4964
|
-
}
|
|
4965
|
-
|
|
4966
|
-
function wrapWith(element, tag) {
|
|
4967
|
-
const wrapper = document.createElement(tag);
|
|
4968
|
-
wrapper.appendChild(element);
|
|
4969
|
-
return wrapper
|
|
4970
|
-
}
|
|
4971
|
-
|
|
4972
|
-
class ProvisionalParagraphNode extends ParagraphNode {
|
|
4973
|
-
$config() {
|
|
4974
|
-
return this.config("provisonal_paragraph", {
|
|
4975
|
-
extends: ParagraphNode,
|
|
4976
|
-
importDOM: () => null,
|
|
4977
|
-
$transform: (node) => {
|
|
4978
|
-
node.concretizeIfEdited(node);
|
|
4979
|
-
node.removeUnlessRequired(node);
|
|
4980
|
-
}
|
|
4981
|
-
})
|
|
4982
|
-
}
|
|
4983
|
-
|
|
4984
|
-
static neededBetween(nodeBefore, nodeAfter) {
|
|
4985
|
-
return !$isSelectableElement(nodeBefore, "next")
|
|
4986
|
-
&& !$isSelectableElement(nodeAfter, "previous")
|
|
4987
|
-
}
|
|
4988
|
-
|
|
4989
|
-
createDOM(editor) {
|
|
4990
|
-
const p = super.createDOM(editor);
|
|
4991
|
-
const selected = this.isSelected($getSelection());
|
|
4992
|
-
p.classList.add("provisional-paragraph");
|
|
4993
|
-
p.classList.toggle("hidden", !selected);
|
|
4994
|
-
return p
|
|
4995
|
-
}
|
|
4996
|
-
|
|
4997
|
-
updateDOM(_prevNode, dom) {
|
|
4998
|
-
const selected = this.isSelected($getSelection());
|
|
4999
|
-
dom.classList.toggle("hidden", !selected);
|
|
5000
|
-
return false
|
|
5001
|
-
}
|
|
5002
|
-
|
|
5003
|
-
getTextContent() {
|
|
5004
|
-
return ""
|
|
5005
|
-
}
|
|
5006
|
-
|
|
5007
|
-
exportDOM() {
|
|
5008
|
-
return {
|
|
5009
|
-
element: null
|
|
5010
|
-
}
|
|
5011
|
-
}
|
|
5012
|
-
|
|
5013
|
-
// override as Lexical has an interesting view of collapsed selection in ElementNodes
|
|
5014
|
-
// https://github.com/facebook/lexical/blob/f1e4f66014377b1f2595aec2b0ee17f5b7ef4dfc/packages/lexical/src/LexicalNode.ts#L646
|
|
5015
|
-
isSelected(selection = null) {
|
|
5016
|
-
const targetSelection = selection || $getSelection();
|
|
5017
|
-
if (!targetSelection) return false
|
|
5018
|
-
|
|
5019
|
-
if (targetSelection.getNodes().some(node => node.is(this) || this.isParentOf(node))) return true
|
|
5020
|
-
|
|
5021
|
-
// A collapsed range selection on the parent element at an offset adjacent to
|
|
5022
|
-
// this node means the caret is visually at this paragraph's position. Treat it
|
|
5023
|
-
// as selected so the paragraph is visible and the caret renders correctly.
|
|
5024
|
-
//
|
|
5025
|
-
// Both the offset matching our index (cursor just before us) and index + 1
|
|
5026
|
-
// (cursor just after us) count, because the provisional paragraph is an
|
|
5027
|
-
// invisible spacer: the browser resolves both offsets to the same visual spot.
|
|
5028
|
-
if ($isRangeSelection(targetSelection) && targetSelection.isCollapsed()) {
|
|
5029
|
-
const { anchor } = targetSelection;
|
|
5030
|
-
const parent = this.getParent();
|
|
5031
|
-
if (parent && anchor.getNode().is(parent) && anchor.type === "element") {
|
|
5032
|
-
const index = this.getIndexWithinParent();
|
|
5033
|
-
return anchor.offset === index || anchor.offset === index + 1
|
|
5034
|
-
}
|
|
5035
|
-
}
|
|
5036
|
-
|
|
5037
|
-
return false
|
|
5038
|
-
}
|
|
5039
|
-
|
|
5040
|
-
removeUnlessRequired(self = this.getLatest()) {
|
|
5041
|
-
if (!self.required) self.remove();
|
|
5042
|
-
}
|
|
5043
|
-
|
|
5044
|
-
concretizeIfEdited(self = this.getLatest()) {
|
|
5045
|
-
if (self.getTextContentSize() > 0) {
|
|
5046
|
-
self.replace($createParagraphNode(), true);
|
|
5047
|
-
}
|
|
5568
|
+
} else if (textNode.hasFormat("uppercase")) {
|
|
5569
|
+
element.style.textTransform = "uppercase";
|
|
5570
|
+
} else if (textNode.hasFormat("capitalize")) {
|
|
5571
|
+
element.style.textTransform = "capitalize";
|
|
5048
5572
|
}
|
|
5049
5573
|
|
|
5574
|
+
let result = element;
|
|
5050
5575
|
|
|
5051
|
-
|
|
5052
|
-
|
|
5576
|
+
if (textNode.hasFormat("bold") && !containsTag(element, "strong")) {
|
|
5577
|
+
result = wrapWith(result, "b");
|
|
5053
5578
|
}
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
const parent = this.getParent();
|
|
5057
|
-
return $isRootOrShadowRoot(parent)
|
|
5579
|
+
if (textNode.hasFormat("italic") && !containsTag(element, "em")) {
|
|
5580
|
+
result = wrapWith(result, "i");
|
|
5058
5581
|
}
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
return [ this.getPreviousSibling(), this.getNextSibling() ]
|
|
5582
|
+
if (textNode.hasFormat("strikethrough")) {
|
|
5583
|
+
result = wrapWith(result, "s");
|
|
5062
5584
|
}
|
|
5585
|
+
if (textNode.hasFormat("underline")) {
|
|
5586
|
+
result = wrapWith(result, "u");
|
|
5587
|
+
}
|
|
5588
|
+
|
|
5589
|
+
return { element: unwrapSpans(result) }
|
|
5063
5590
|
}
|
|
5064
5591
|
|
|
5065
|
-
function
|
|
5066
|
-
|
|
5592
|
+
function containsTag(element, tagName) {
|
|
5593
|
+
const upperTag = tagName.toUpperCase();
|
|
5594
|
+
if (element.tagName === upperTag) return true
|
|
5595
|
+
|
|
5596
|
+
return element.querySelector(tagName) !== null
|
|
5067
5597
|
}
|
|
5068
5598
|
|
|
5069
|
-
function
|
|
5070
|
-
|
|
5599
|
+
function wrapWith(element, tag) {
|
|
5600
|
+
const wrapper = document.createElement(tag);
|
|
5601
|
+
wrapper.appendChild(element);
|
|
5602
|
+
return wrapper
|
|
5603
|
+
}
|
|
5604
|
+
|
|
5605
|
+
function unwrapSpans(element) {
|
|
5606
|
+
if (element.tagName === "SPAN") return element.firstChild
|
|
5607
|
+
|
|
5608
|
+
for (const span of element.querySelectorAll("span")) {
|
|
5609
|
+
span.replaceWith(...span.childNodes);
|
|
5610
|
+
}
|
|
5611
|
+
|
|
5612
|
+
return element
|
|
5071
5613
|
}
|
|
5072
5614
|
|
|
5073
5615
|
class ProvisionalParagraphExtension extends LexxyExtension {
|
|
@@ -5220,6 +5762,10 @@ class TablesExtension extends LexxyExtension {
|
|
|
5220
5762
|
return this.editorElement.supportsRichText
|
|
5221
5763
|
}
|
|
5222
5764
|
|
|
5765
|
+
get allowedElements() {
|
|
5766
|
+
return [ "figure", "tbody" ]
|
|
5767
|
+
}
|
|
5768
|
+
|
|
5223
5769
|
get lexicalExtension() {
|
|
5224
5770
|
return defineExtension({
|
|
5225
5771
|
name: "lexxy/tables",
|
|
@@ -5323,21 +5869,21 @@ class AttachmentDragAndDrop {
|
|
|
5323
5869
|
#draggedNodeKey = null
|
|
5324
5870
|
#rafId = null
|
|
5325
5871
|
#draggingRafId = null
|
|
5326
|
-
#
|
|
5872
|
+
#listeners = new ListenerBin()
|
|
5327
5873
|
|
|
5328
5874
|
constructor(editor) {
|
|
5329
5875
|
this.#editor = editor;
|
|
5330
5876
|
|
|
5331
5877
|
// Register Lexical commands at HIGH priority to intercept before the
|
|
5332
5878
|
// base @lexical/rich-text handlers (which return true and consume the events).
|
|
5333
|
-
this.#
|
|
5879
|
+
this.#listeners.track(
|
|
5334
5880
|
editor.registerCommand(DRAGSTART_COMMAND, (event) => this.#handleDragStart(event), COMMAND_PRIORITY_HIGH),
|
|
5335
5881
|
editor.registerCommand(DROP_COMMAND, (event) => this.#handleDrop(event), COMMAND_PRIORITY_HIGH),
|
|
5336
5882
|
);
|
|
5337
5883
|
|
|
5338
5884
|
// Use a root listener to register DOM-level dragover/dragend handlers
|
|
5339
5885
|
// (these events need throttled rAF handling that works better as DOM listeners).
|
|
5340
|
-
|
|
5886
|
+
this.#listeners.track(editor.registerRootListener((root, prevRoot) => {
|
|
5341
5887
|
if (prevRoot) {
|
|
5342
5888
|
prevRoot.removeEventListener("dragover", this.#onDragOver);
|
|
5343
5889
|
prevRoot.removeEventListener("dragend", this.#onDragEnd);
|
|
@@ -5346,14 +5892,12 @@ class AttachmentDragAndDrop {
|
|
|
5346
5892
|
root.addEventListener("dragover", this.#onDragOver);
|
|
5347
5893
|
root.addEventListener("dragend", this.#onDragEnd);
|
|
5348
5894
|
}
|
|
5349
|
-
});
|
|
5350
|
-
this.#cleanupFns.push(unregister);
|
|
5895
|
+
}));
|
|
5351
5896
|
}
|
|
5352
5897
|
|
|
5353
5898
|
destroy() {
|
|
5354
5899
|
this.#cleanup();
|
|
5355
|
-
|
|
5356
|
-
this.#cleanupFns = [];
|
|
5900
|
+
this.#listeners.dispose();
|
|
5357
5901
|
}
|
|
5358
5902
|
|
|
5359
5903
|
// -- Event handlers --------------------------------------------------------
|
|
@@ -5675,11 +6219,18 @@ class AttachmentDragAndDrop {
|
|
|
5675
6219
|
}
|
|
5676
6220
|
}
|
|
5677
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
|
+
|
|
5678
6225
|
class AttachmentsExtension extends LexxyExtension {
|
|
5679
6226
|
get enabled() {
|
|
5680
6227
|
return this.editorElement.supportsAttachments
|
|
5681
6228
|
}
|
|
5682
6229
|
|
|
6230
|
+
get allowedElements() {
|
|
6231
|
+
return [ { tag: ActionTextAttachmentNode.TAG_NAME, attributes: ATTACHMENT_ATTRIBUTES } ]
|
|
6232
|
+
}
|
|
6233
|
+
|
|
5683
6234
|
get lexicalExtension() {
|
|
5684
6235
|
return defineExtension({
|
|
5685
6236
|
name: "lexxy/action-text-attachments",
|
|
@@ -5966,6 +6517,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
5966
6517
|
|
|
5967
6518
|
#initialValue = ""
|
|
5968
6519
|
#validationTextArea = document.createElement("textarea")
|
|
6520
|
+
#editorInitializedRafId = null
|
|
6521
|
+
#listeners = new ListenerBin()
|
|
5969
6522
|
#disposables = []
|
|
5970
6523
|
|
|
5971
6524
|
constructor() {
|
|
@@ -5975,12 +6528,13 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
5975
6528
|
}
|
|
5976
6529
|
|
|
5977
6530
|
connectedCallback() {
|
|
5978
|
-
this.id
|
|
6531
|
+
this.id ||= generateDomId("lexxy-editor");
|
|
5979
6532
|
this.config = new EditorConfiguration(this);
|
|
5980
6533
|
this.extensions = new Extensions(this);
|
|
5981
6534
|
|
|
5982
6535
|
this.editor = this.#createEditor();
|
|
5983
6536
|
this.#disposables.push(this.editor);
|
|
6537
|
+
this.#disposables.push(this.#listeners);
|
|
5984
6538
|
|
|
5985
6539
|
this.contents = new Contents(this);
|
|
5986
6540
|
this.#disposables.push(this.contents);
|
|
@@ -5989,13 +6543,14 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
5989
6543
|
this.#disposables.push(this.selection);
|
|
5990
6544
|
|
|
5991
6545
|
this.clipboard = new Clipboard(this);
|
|
6546
|
+
this.adapter = new BrowserAdapter();
|
|
5992
6547
|
|
|
5993
6548
|
const commandDispatcher = CommandDispatcher.configureFor(this);
|
|
5994
6549
|
this.#disposables.push(commandDispatcher);
|
|
5995
6550
|
|
|
5996
6551
|
this.#initialize();
|
|
5997
6552
|
|
|
5998
|
-
|
|
6553
|
+
this.#scheduleEditorInitializedDispatch();
|
|
5999
6554
|
this.toggleAttribute("connected", true);
|
|
6000
6555
|
|
|
6001
6556
|
this.#handleAutofocus();
|
|
@@ -6004,6 +6559,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6004
6559
|
}
|
|
6005
6560
|
|
|
6006
6561
|
disconnectedCallback() {
|
|
6562
|
+
this.#cancelEditorInitializedDispatch();
|
|
6007
6563
|
this.valueBeforeDisconnect = this.value;
|
|
6008
6564
|
this.#reset(); // Prevent hangs with Safari when morphing
|
|
6009
6565
|
}
|
|
@@ -6100,6 +6656,32 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6100
6656
|
return this.config.get("richText")
|
|
6101
6657
|
}
|
|
6102
6658
|
|
|
6659
|
+
registerAdapter(adapter) {
|
|
6660
|
+
this.adapter = adapter;
|
|
6661
|
+
|
|
6662
|
+
if (!this.editor) return
|
|
6663
|
+
|
|
6664
|
+
this.#cancelEditorInitializedDispatch();
|
|
6665
|
+
this.#dispatchEditorInitialized();
|
|
6666
|
+
this.#dispatchAttributesChange();
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6669
|
+
freezeSelection() {
|
|
6670
|
+
this.adapter.freeze();
|
|
6671
|
+
}
|
|
6672
|
+
|
|
6673
|
+
thawSelection() {
|
|
6674
|
+
this.adapter.thaw();
|
|
6675
|
+
}
|
|
6676
|
+
|
|
6677
|
+
dispatchAttributesChange() {
|
|
6678
|
+
this.#dispatchAttributesChange();
|
|
6679
|
+
}
|
|
6680
|
+
|
|
6681
|
+
dispatchEditorInitialized() {
|
|
6682
|
+
this.#dispatchEditorInitialized();
|
|
6683
|
+
}
|
|
6684
|
+
|
|
6103
6685
|
// TODO: Deprecate `single-line` attribute
|
|
6104
6686
|
get isSingleLineMode() {
|
|
6105
6687
|
return this.hasAttribute("single-line")
|
|
@@ -6116,7 +6698,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6116
6698
|
get value() {
|
|
6117
6699
|
if (!this.cachedValue) {
|
|
6118
6700
|
this.editor?.getEditorState().read(() => {
|
|
6119
|
-
this.cachedValue = sanitize($generateHtmlFromNodes(this.editor, null));
|
|
6701
|
+
this.cachedValue = sanitize($generateHtmlFromNodes(this.editor, null), this.#allowedElements);
|
|
6120
6702
|
});
|
|
6121
6703
|
}
|
|
6122
6704
|
|
|
@@ -6189,7 +6771,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6189
6771
|
theme: theme,
|
|
6190
6772
|
nodes: this.#lexicalNodes,
|
|
6191
6773
|
html: {
|
|
6192
|
-
export: new Map([ [ TextNode, exportTextNodeDOM ] ])
|
|
6774
|
+
export: new Map([ [ TextNode, exportTextNodeDOM ], [ CodeHighlightNode, exportTextNodeDOM ] ])
|
|
6193
6775
|
}
|
|
6194
6776
|
},
|
|
6195
6777
|
...this.extensions.lexicalExtensions
|
|
@@ -6224,6 +6806,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6224
6806
|
const editorContentElement = createElement("div", {
|
|
6225
6807
|
classList: "lexxy-editor__content",
|
|
6226
6808
|
contenteditable: true,
|
|
6809
|
+
autocapitalize: "none",
|
|
6227
6810
|
role: "textbox",
|
|
6228
6811
|
"aria-multiline": true,
|
|
6229
6812
|
"aria-label": this.#labelText,
|
|
@@ -6272,7 +6855,9 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6272
6855
|
}
|
|
6273
6856
|
|
|
6274
6857
|
#resetBeforeTurboCaches() {
|
|
6275
|
-
|
|
6858
|
+
this.#listeners.track(
|
|
6859
|
+
registerEventListener(document, "turbo:before-cache", this.#handleTurboBeforeCache)
|
|
6860
|
+
);
|
|
6276
6861
|
}
|
|
6277
6862
|
|
|
6278
6863
|
#handleTurboBeforeCache = (event) => {
|
|
@@ -6280,11 +6865,12 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6280
6865
|
}
|
|
6281
6866
|
|
|
6282
6867
|
#synchronizeWithChanges() {
|
|
6283
|
-
this.#
|
|
6868
|
+
this.#listeners.track(this.editor.registerUpdateListener(({ editorState }) => {
|
|
6284
6869
|
this.#clearCachedValues();
|
|
6285
6870
|
this.#internalFormValue = this.value;
|
|
6286
6871
|
this.#toggleEmptyStatus();
|
|
6287
6872
|
this.#setValidity();
|
|
6873
|
+
this.#dispatchAttributesChange();
|
|
6288
6874
|
}));
|
|
6289
6875
|
}
|
|
6290
6876
|
|
|
@@ -6293,18 +6879,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6293
6879
|
this.cachedStringValue = null;
|
|
6294
6880
|
}
|
|
6295
6881
|
|
|
6296
|
-
#addUnregisterHandler(handler) {
|
|
6297
|
-
this.unregisterHandlers = this.unregisterHandlers || [];
|
|
6298
|
-
this.unregisterHandlers.push(handler);
|
|
6299
|
-
}
|
|
6300
|
-
|
|
6301
|
-
#unregisterHandlers() {
|
|
6302
|
-
this.unregisterHandlers?.forEach((handler) => {
|
|
6303
|
-
handler();
|
|
6304
|
-
});
|
|
6305
|
-
this.unregisterHandlers = null;
|
|
6306
|
-
}
|
|
6307
|
-
|
|
6308
6882
|
#registerComponents() {
|
|
6309
6883
|
const registered = [];
|
|
6310
6884
|
|
|
@@ -6316,9 +6890,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6316
6890
|
this.#registerTableComponents();
|
|
6317
6891
|
this.#registerCodeHiglightingComponents();
|
|
6318
6892
|
if (this.supportsMarkdown) {
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6893
|
+
const transformers = [ ...TRANSFORMERS, HORIZONTAL_DIVIDER ];
|
|
6894
|
+
registered.push(
|
|
6895
|
+
registerMarkdownShortcuts(this.editor, transformers),
|
|
6896
|
+
registerMarkdownLeadingTagHandler(this.editor, transformers)
|
|
6322
6897
|
);
|
|
6323
6898
|
}
|
|
6324
6899
|
} else {
|
|
@@ -6327,7 +6902,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6327
6902
|
this.historyState = createEmptyHistoryState();
|
|
6328
6903
|
registered.push(registerHistory(this.editor, this.historyState, 20));
|
|
6329
6904
|
|
|
6330
|
-
this.#
|
|
6905
|
+
this.#listeners.track(...registered);
|
|
6331
6906
|
}
|
|
6332
6907
|
|
|
6333
6908
|
#registerTableComponents() {
|
|
@@ -6347,7 +6922,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6347
6922
|
|
|
6348
6923
|
#handleEnter() {
|
|
6349
6924
|
// We can't prevent these externally using regular keydown because Lexical handles it first.
|
|
6350
|
-
this.#
|
|
6925
|
+
this.#listeners.track(this.editor.registerCommand(
|
|
6351
6926
|
KEY_ENTER_COMMAND,
|
|
6352
6927
|
(event) => {
|
|
6353
6928
|
// Prevent CTRL+ENTER
|
|
@@ -6369,17 +6944,15 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6369
6944
|
}
|
|
6370
6945
|
|
|
6371
6946
|
#registerFocusEvents() {
|
|
6372
|
-
this.
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
this.removeEventListener("focusin", this.#handleFocusIn);
|
|
6377
|
-
this.removeEventListener("focusout", this.#handleFocusOut);
|
|
6378
|
-
});
|
|
6947
|
+
this.#listeners.track(
|
|
6948
|
+
registerEventListener(this, "focusin", this.#handleFocusIn),
|
|
6949
|
+
registerEventListener(this, "focusout", this.#handleFocusOut)
|
|
6950
|
+
);
|
|
6379
6951
|
}
|
|
6380
6952
|
|
|
6381
6953
|
#handleFocusIn(event) {
|
|
6382
6954
|
if (this.#elementInEditorOrToolbar(event.target) && !this.currentlyFocused) {
|
|
6955
|
+
this.#dispatchAttributesChange();
|
|
6383
6956
|
dispatch(this, "lexxy:focus");
|
|
6384
6957
|
this.currentlyFocused = true;
|
|
6385
6958
|
}
|
|
@@ -6460,7 +7033,121 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6460
7033
|
}
|
|
6461
7034
|
}
|
|
6462
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
|
+
|
|
7045
|
+
#dispatchAttributesChange() {
|
|
7046
|
+
let attributes = null;
|
|
7047
|
+
let linkHref = null;
|
|
7048
|
+
let highlight = null;
|
|
7049
|
+
let headingTag = null;
|
|
7050
|
+
|
|
7051
|
+
this.editor.getEditorState().read(() => {
|
|
7052
|
+
const selection = $getSelection();
|
|
7053
|
+
if (!$isRangeSelection(selection)) return
|
|
7054
|
+
|
|
7055
|
+
const format = this.selection.getFormat();
|
|
7056
|
+
if (Object.keys(format).length === 0) return
|
|
7057
|
+
|
|
7058
|
+
const anchorNode = selection.anchor.getNode();
|
|
7059
|
+
const linkNode = $getNearestNodeOfType(anchorNode, LinkNode);
|
|
7060
|
+
|
|
7061
|
+
attributes = {
|
|
7062
|
+
bold: { active: format.isBold, enabled: true },
|
|
7063
|
+
italic: { active: format.isItalic, enabled: true },
|
|
7064
|
+
strikethrough: { active: format.isStrikethrough, enabled: true },
|
|
7065
|
+
code: { active: format.isInCode, enabled: true },
|
|
7066
|
+
highlight: { active: format.isHighlight, enabled: true },
|
|
7067
|
+
link: { active: format.isInLink, enabled: true },
|
|
7068
|
+
quote: { active: format.isInQuote, enabled: true },
|
|
7069
|
+
heading: { active: format.isInHeading, enabled: true },
|
|
7070
|
+
"unordered-list": { active: format.isInList && format.listType === "bullet", enabled: true },
|
|
7071
|
+
"ordered-list": { active: format.isInList && format.listType === "number", enabled: true },
|
|
7072
|
+
undo: { active: false, enabled: this.historyState?.undoStack.length > 0 },
|
|
7073
|
+
redo: { active: false, enabled: this.historyState?.redoStack.length > 0 }
|
|
7074
|
+
};
|
|
7075
|
+
|
|
7076
|
+
linkHref = linkNode ? linkNode.getURL() : null;
|
|
7077
|
+
highlight = format.isHighlight ? getHighlightStyles(selection) : null;
|
|
7078
|
+
headingTag = format.headingTag ?? null;
|
|
7079
|
+
});
|
|
7080
|
+
|
|
7081
|
+
if (attributes) {
|
|
7082
|
+
this.adapter.dispatchAttributesChange(attributes, linkHref, highlight, headingTag);
|
|
7083
|
+
}
|
|
7084
|
+
}
|
|
7085
|
+
|
|
7086
|
+
#dispatchEditorInitialized() {
|
|
7087
|
+
if (!this.adapter) return
|
|
7088
|
+
|
|
7089
|
+
this.adapter.dispatchEditorInitialized({
|
|
7090
|
+
highlightColors: this.#resolvedHighlightColors,
|
|
7091
|
+
headingFormats: this.#supportedHeadingFormats
|
|
7092
|
+
});
|
|
7093
|
+
}
|
|
7094
|
+
|
|
7095
|
+
#scheduleEditorInitializedDispatch() {
|
|
7096
|
+
this.#cancelEditorInitializedDispatch();
|
|
7097
|
+
this.#editorInitializedRafId = requestAnimationFrame(() => {
|
|
7098
|
+
this.#editorInitializedRafId = null;
|
|
7099
|
+
if (!this.isConnected || !this.adapter) return
|
|
7100
|
+
|
|
7101
|
+
dispatch(this, "lexxy:initialize");
|
|
7102
|
+
this.#dispatchEditorInitialized();
|
|
7103
|
+
});
|
|
7104
|
+
}
|
|
7105
|
+
|
|
7106
|
+
#cancelEditorInitializedDispatch() {
|
|
7107
|
+
if (this.#editorInitializedRafId == null) return
|
|
7108
|
+
|
|
7109
|
+
cancelAnimationFrame(this.#editorInitializedRafId);
|
|
7110
|
+
this.#editorInitializedRafId = null;
|
|
7111
|
+
}
|
|
7112
|
+
|
|
7113
|
+
get #resolvedHighlightColors() {
|
|
7114
|
+
const buttons = this.config.get("highlight.buttons");
|
|
7115
|
+
if (!buttons) return null
|
|
7116
|
+
|
|
7117
|
+
const colors = this.#resolveColors("color", buttons.color || []);
|
|
7118
|
+
const backgroundColors = this.#resolveColors("background-color", buttons["background-color"] || []);
|
|
7119
|
+
return { colors, backgroundColors }
|
|
7120
|
+
}
|
|
7121
|
+
|
|
7122
|
+
get #supportedHeadingFormats() {
|
|
7123
|
+
if (!this.supportsRichText) return []
|
|
7124
|
+
|
|
7125
|
+
return [
|
|
7126
|
+
{ label: "Normal", command: "setFormatParagraph", tag: null },
|
|
7127
|
+
{ label: "Large heading", command: "setFormatHeadingLarge", tag: "h2" },
|
|
7128
|
+
{ label: "Medium heading", command: "setFormatHeadingMedium", tag: "h3" },
|
|
7129
|
+
{ label: "Small heading", command: "setFormatHeadingSmall", tag: "h4" },
|
|
7130
|
+
]
|
|
7131
|
+
}
|
|
7132
|
+
|
|
7133
|
+
#resolveColors(property, cssValues) {
|
|
7134
|
+
const resolver = document.createElement("span");
|
|
7135
|
+
resolver.style.display = "none";
|
|
7136
|
+
this.appendChild(resolver);
|
|
7137
|
+
|
|
7138
|
+
const resolved = cssValues.map(cssValue => {
|
|
7139
|
+
resolver.style.setProperty(property, cssValue);
|
|
7140
|
+
const value = window.getComputedStyle(resolver).getPropertyValue(property);
|
|
7141
|
+
resolver.style.removeProperty(property);
|
|
7142
|
+
return { name: cssValue, value }
|
|
7143
|
+
});
|
|
7144
|
+
|
|
7145
|
+
resolver.remove();
|
|
7146
|
+
return resolved
|
|
7147
|
+
}
|
|
7148
|
+
|
|
6463
7149
|
#reset() {
|
|
7150
|
+
this.#cancelEditorInitializedDispatch();
|
|
6464
7151
|
this.#dispose();
|
|
6465
7152
|
this.editorContentElement?.remove();
|
|
6466
7153
|
this.editorContentElement = null;
|
|
@@ -6471,9 +7158,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6471
7158
|
}
|
|
6472
7159
|
|
|
6473
7160
|
#dispose() {
|
|
6474
|
-
this.#unregisterHandlers();
|
|
6475
|
-
document.removeEventListener("turbo:before-cache", this.#handleTurboBeforeCache);
|
|
6476
|
-
|
|
6477
7161
|
while (this.#disposables.length) {
|
|
6478
7162
|
this.#disposables.pop().dispose();
|
|
6479
7163
|
}
|
|
@@ -6514,18 +7198,21 @@ function $getReadableTextContent(node) {
|
|
|
6514
7198
|
}
|
|
6515
7199
|
|
|
6516
7200
|
class ToolbarDropdown extends HTMLElement {
|
|
7201
|
+
#listeners = new ListenerBin()
|
|
7202
|
+
|
|
6517
7203
|
connectedCallback() {
|
|
6518
7204
|
this.container = this.closest("details");
|
|
6519
7205
|
|
|
6520
|
-
this.
|
|
6521
|
-
|
|
7206
|
+
this.#listeners.track(
|
|
7207
|
+
registerEventListener(this.container, "toggle", this.#handleToggle),
|
|
7208
|
+
registerEventListener(this.container, "keydown", this.#handleKeyDown)
|
|
7209
|
+
);
|
|
6522
7210
|
|
|
6523
7211
|
this.#onToolbarEditor(this.initialize.bind(this));
|
|
6524
7212
|
}
|
|
6525
7213
|
|
|
6526
7214
|
disconnectedCallback() {
|
|
6527
|
-
this.
|
|
6528
|
-
this.container?.removeEventListener("keydown", this.#handleKeyDown);
|
|
7215
|
+
this.#listeners.dispose();
|
|
6529
7216
|
}
|
|
6530
7217
|
|
|
6531
7218
|
get toolbar() {
|
|
@@ -6540,6 +7227,10 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
6540
7227
|
return this.toolbar.editor
|
|
6541
7228
|
}
|
|
6542
7229
|
|
|
7230
|
+
track(...listeners) {
|
|
7231
|
+
this.#listeners.track(...listeners);
|
|
7232
|
+
}
|
|
7233
|
+
|
|
6543
7234
|
initialize() {
|
|
6544
7235
|
// Any post-editor initialization
|
|
6545
7236
|
}
|
|
@@ -6591,18 +7282,23 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
6591
7282
|
class LinkDropdown extends ToolbarDropdown {
|
|
6592
7283
|
connectedCallback() {
|
|
6593
7284
|
super.connectedCallback();
|
|
7285
|
+
|
|
6594
7286
|
this.input = this.querySelector("input");
|
|
6595
7287
|
|
|
6596
|
-
this.
|
|
6597
|
-
|
|
6598
|
-
|
|
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
|
+
);
|
|
6599
7294
|
}
|
|
6600
7295
|
|
|
6601
|
-
|
|
6602
|
-
this.
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
7296
|
+
get linkButton() {
|
|
7297
|
+
return this.querySelector("[value='link']")
|
|
7298
|
+
}
|
|
7299
|
+
|
|
7300
|
+
get unlinkButton() {
|
|
7301
|
+
return this.querySelector("[value='unlink']")
|
|
6606
7302
|
}
|
|
6607
7303
|
|
|
6608
7304
|
#handleToggle = ({ newState }) => {
|
|
@@ -6610,9 +7306,21 @@ class LinkDropdown extends ToolbarDropdown {
|
|
|
6610
7306
|
this.input.required = newState === "open";
|
|
6611
7307
|
}
|
|
6612
7308
|
|
|
6613
|
-
#
|
|
6614
|
-
|
|
6615
|
-
|
|
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);
|
|
6616
7324
|
this.close();
|
|
6617
7325
|
}
|
|
6618
7326
|
|
|
@@ -6622,23 +7330,10 @@ class LinkDropdown extends ToolbarDropdown {
|
|
|
6622
7330
|
}
|
|
6623
7331
|
|
|
6624
7332
|
get #selectedLinkUrl() {
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
if (!$isRangeSelection(selection)) return
|
|
6630
|
-
|
|
6631
|
-
let node = selection.getNodes()[0];
|
|
6632
|
-
while (node && node.getParent()) {
|
|
6633
|
-
if ($isLinkNode(node)) {
|
|
6634
|
-
url = node.getURL();
|
|
6635
|
-
break
|
|
6636
|
-
}
|
|
6637
|
-
node = node.getParent();
|
|
6638
|
-
}
|
|
6639
|
-
});
|
|
6640
|
-
|
|
6641
|
-
return url
|
|
7333
|
+
return this.editor.getEditorState().read(() => {
|
|
7334
|
+
const linkNode = this.editorElement.selection.nearestNodeOfType(LinkNode);
|
|
7335
|
+
return linkNode?.getUrl() ?? null
|
|
7336
|
+
})
|
|
6642
7337
|
}
|
|
6643
7338
|
}
|
|
6644
7339
|
|
|
@@ -6658,23 +7353,14 @@ class HighlightDropdown extends ToolbarDropdown {
|
|
|
6658
7353
|
|
|
6659
7354
|
connectedCallback() {
|
|
6660
7355
|
super.connectedCallback();
|
|
6661
|
-
this.container
|
|
6662
|
-
}
|
|
6663
|
-
|
|
6664
|
-
disconnectedCallback() {
|
|
6665
|
-
this.container?.removeEventListener("toggle", this.#handleToggle);
|
|
6666
|
-
this.#removeButtonHandlers();
|
|
6667
|
-
super.disconnectedCallback();
|
|
7356
|
+
this.track(registerEventListener(this.container, "toggle", this.#handleToggle));
|
|
6668
7357
|
}
|
|
6669
7358
|
|
|
6670
7359
|
#registerButtonHandlers() {
|
|
6671
|
-
this.#colorButtons.forEach(button =>
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
#removeButtonHandlers() {
|
|
6676
|
-
this.#colorButtons.forEach(button => button.removeEventListener("click", this.#handleColorButtonClick));
|
|
6677
|
-
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));
|
|
6678
7364
|
}
|
|
6679
7365
|
|
|
6680
7366
|
#setUpButtons() {
|
|
@@ -6896,9 +7582,11 @@ class RemoteFilterSource extends BaseSource {
|
|
|
6896
7582
|
const NOTHING_FOUND_DEFAULT_MESSAGE = "Nothing found";
|
|
6897
7583
|
|
|
6898
7584
|
class LexicalPromptElement extends HTMLElement {
|
|
7585
|
+
#globalListeners = new ListenerBin()
|
|
7586
|
+
#popoverListeners = new ListenerBin()
|
|
7587
|
+
|
|
6899
7588
|
constructor() {
|
|
6900
7589
|
super();
|
|
6901
|
-
this.keyListeners = [];
|
|
6902
7590
|
this.showPopoverId = 0;
|
|
6903
7591
|
}
|
|
6904
7592
|
|
|
@@ -6912,6 +7600,8 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
6912
7600
|
}
|
|
6913
7601
|
|
|
6914
7602
|
disconnectedCallback() {
|
|
7603
|
+
this.#popoverListeners.dispose();
|
|
7604
|
+
this.#globalListeners.dispose();
|
|
6915
7605
|
this.source = null;
|
|
6916
7606
|
this.popoverElement = null;
|
|
6917
7607
|
}
|
|
@@ -6961,7 +7651,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
6961
7651
|
}
|
|
6962
7652
|
|
|
6963
7653
|
#addTriggerListener() {
|
|
6964
|
-
|
|
7654
|
+
this.#popoverListeners.track(this.#editor.registerUpdateListener(({ editorState }) => {
|
|
6965
7655
|
editorState.read(() => {
|
|
6966
7656
|
if (this.#selection.isInsideCodeBlock) return
|
|
6967
7657
|
|
|
@@ -6984,18 +7674,18 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
6984
7674
|
const isPrecededBySpaceOrNewline = charBeforeTrigger === " " || charBeforeTrigger === "\n";
|
|
6985
7675
|
|
|
6986
7676
|
if (isAtStart || isPrecededBySpaceOrNewline) {
|
|
6987
|
-
|
|
7677
|
+
this.#popoverListeners.dispose();
|
|
6988
7678
|
this.#showPopover();
|
|
6989
7679
|
}
|
|
6990
7680
|
}
|
|
6991
7681
|
}
|
|
6992
7682
|
}
|
|
6993
7683
|
});
|
|
6994
|
-
});
|
|
7684
|
+
}));
|
|
6995
7685
|
}
|
|
6996
7686
|
|
|
6997
7687
|
#addCursorPositionListener() {
|
|
6998
|
-
this.
|
|
7688
|
+
this.#popoverListeners.track(this.#editor.registerUpdateListener(({ editorState }) => {
|
|
6999
7689
|
if (this.closed) return
|
|
7000
7690
|
|
|
7001
7691
|
editorState.read(() => {
|
|
@@ -7022,14 +7712,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7022
7712
|
this.#hidePopover();
|
|
7023
7713
|
}
|
|
7024
7714
|
});
|
|
7025
|
-
});
|
|
7026
|
-
}
|
|
7027
|
-
|
|
7028
|
-
#removeCursorPositionListener() {
|
|
7029
|
-
if (this.cursorPositionListener) {
|
|
7030
|
-
this.cursorPositionListener();
|
|
7031
|
-
this.cursorPositionListener = null;
|
|
7032
|
-
}
|
|
7715
|
+
}));
|
|
7033
7716
|
}
|
|
7034
7717
|
|
|
7035
7718
|
get #editor() {
|
|
@@ -7056,8 +7739,10 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7056
7739
|
this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", true);
|
|
7057
7740
|
this.#selectFirstOption();
|
|
7058
7741
|
|
|
7059
|
-
this.#
|
|
7060
|
-
|
|
7742
|
+
this.#popoverListeners.track(
|
|
7743
|
+
registerEventListener(this.#editorElement, "keydown", this.#handleKeydownOnPopover),
|
|
7744
|
+
registerEventListener(this.#editorElement, "lexxy:change", this.#filterOptions)
|
|
7745
|
+
);
|
|
7061
7746
|
|
|
7062
7747
|
this.#registerKeyListeners();
|
|
7063
7748
|
this.#addCursorPositionListener();
|
|
@@ -7065,16 +7750,20 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7065
7750
|
|
|
7066
7751
|
#registerKeyListeners() {
|
|
7067
7752
|
// We can't use a regular keydown for Enter as Lexical handles it first
|
|
7068
|
-
this
|
|
7069
|
-
|
|
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
|
+
);
|
|
7070
7757
|
|
|
7071
7758
|
if (this.#doesSpaceSelect) {
|
|
7072
|
-
this.
|
|
7759
|
+
this.#popoverListeners.track(this.#editor.registerCommand(KEY_SPACE_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_CRITICAL));
|
|
7073
7760
|
}
|
|
7074
7761
|
|
|
7075
7762
|
// Register arrow keys with CRITICAL priority to prevent Lexical's selection handlers from running
|
|
7076
|
-
this
|
|
7077
|
-
|
|
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
|
+
);
|
|
7078
7767
|
}
|
|
7079
7768
|
|
|
7080
7769
|
#handleArrowUp(event) {
|
|
@@ -7166,21 +7855,12 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7166
7855
|
this.showPopoverId++;
|
|
7167
7856
|
this.#clearSelection();
|
|
7168
7857
|
this.popoverElement.classList.toggle("lexxy-prompt-menu--visible", false);
|
|
7169
|
-
this.#
|
|
7170
|
-
this.#editorElement.removeEventListener("keydown", this.#handleKeydownOnPopover);
|
|
7171
|
-
|
|
7172
|
-
this.#unregisterKeyListeners();
|
|
7173
|
-
this.#removeCursorPositionListener();
|
|
7858
|
+
this.#popoverListeners.dispose();
|
|
7174
7859
|
|
|
7175
7860
|
await nextFrame();
|
|
7176
7861
|
this.#addTriggerListener();
|
|
7177
7862
|
}
|
|
7178
7863
|
|
|
7179
|
-
#unregisterKeyListeners() {
|
|
7180
|
-
this.keyListeners.forEach((unregister) => unregister());
|
|
7181
|
-
this.keyListeners = [];
|
|
7182
|
-
}
|
|
7183
|
-
|
|
7184
7864
|
#filterOptions = async () => {
|
|
7185
7865
|
if (this.initialPrompt) {
|
|
7186
7866
|
this.initialPrompt = false;
|
|
@@ -7357,7 +8037,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7357
8037
|
popoverContainer.style.position = "absolute";
|
|
7358
8038
|
popoverContainer.setAttribute("nonce", getNonce());
|
|
7359
8039
|
popoverContainer.append(...await this.source.buildListItems());
|
|
7360
|
-
|
|
8040
|
+
this.#globalListeners.track(registerEventListener(popoverContainer, "click", this.#handlePopoverClick));
|
|
7361
8041
|
this.#editorElement.appendChild(popoverContainer);
|
|
7362
8042
|
return popoverContainer
|
|
7363
8043
|
}
|
|
@@ -7377,10 +8057,15 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
7377
8057
|
}
|
|
7378
8058
|
|
|
7379
8059
|
class CodeLanguagePicker extends HTMLElement {
|
|
8060
|
+
#abortController = null
|
|
8061
|
+
#listeners = new ListenerBin()
|
|
8062
|
+
|
|
7380
8063
|
connectedCallback() {
|
|
7381
8064
|
this.editorElement = this.closest("lexxy-editor");
|
|
7382
8065
|
this.editor = this.editorElement.editor;
|
|
7383
8066
|
this.classList.add("lexxy-floating-controls");
|
|
8067
|
+
this.#abortController = new AbortController();
|
|
8068
|
+
this.#listeners.track(() => this.#abortController?.abort());
|
|
7384
8069
|
|
|
7385
8070
|
this.#attachLanguagePicker();
|
|
7386
8071
|
this.#hide();
|
|
@@ -7392,13 +8077,24 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
7392
8077
|
}
|
|
7393
8078
|
|
|
7394
8079
|
dispose() {
|
|
7395
|
-
this.
|
|
7396
|
-
this.unregisterUpdateListener = null;
|
|
8080
|
+
this.#listeners.dispose();
|
|
7397
8081
|
}
|
|
7398
8082
|
|
|
7399
8083
|
#attachLanguagePicker() {
|
|
7400
8084
|
this.languagePickerElement = this.#findLanguagePicker() ?? this.#createLanguagePicker();
|
|
7401
|
-
|
|
8085
|
+
|
|
8086
|
+
const signal = this.#abortController.signal;
|
|
8087
|
+
|
|
8088
|
+
this.#listeners.track(registerEventListener(this.languagePickerElement, "change", () => {
|
|
8089
|
+
this.#updateCodeBlockLanguage(this.languagePickerElement.value);
|
|
8090
|
+
}, { signal }));
|
|
8091
|
+
|
|
8092
|
+
this.#listeners.track(registerEventListener(this.languagePickerElement, "mousedown", (event) => {
|
|
8093
|
+
this.#dispatchOpenEvent(event);
|
|
8094
|
+
}, { signal }));
|
|
8095
|
+
|
|
8096
|
+
this.languagePickerElement.setAttribute("nonce", getNonce());
|
|
8097
|
+
this.appendChild(this.languagePickerElement);
|
|
7402
8098
|
}
|
|
7403
8099
|
|
|
7404
8100
|
#findLanguagePicker() {
|
|
@@ -7415,32 +8111,39 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
7415
8111
|
selectElement.appendChild(option);
|
|
7416
8112
|
}
|
|
7417
8113
|
|
|
7418
|
-
selectElement.addEventListener("change", () => {
|
|
7419
|
-
this.#updateCodeBlockLanguage(this.languagePickerElement.value);
|
|
7420
|
-
});
|
|
7421
|
-
|
|
7422
|
-
selectElement.setAttribute("nonce", getNonce());
|
|
7423
|
-
|
|
7424
8114
|
return selectElement
|
|
7425
8115
|
}
|
|
7426
8116
|
|
|
7427
8117
|
get #languages() {
|
|
7428
8118
|
const languages = { ...CODE_LANGUAGE_FRIENDLY_NAME_MAP };
|
|
7429
8119
|
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
8120
|
+
languages.ruby ||= "Ruby";
|
|
8121
|
+
languages.php ||= "PHP";
|
|
8122
|
+
languages.go ||= "Go";
|
|
8123
|
+
languages.bash ||= "Bash";
|
|
8124
|
+
languages.json ||= "JSON";
|
|
8125
|
+
languages.diff ||= "Diff";
|
|
7436
8126
|
|
|
8127
|
+
// Place the "plain" entry first, then the rest of language sorted alphabetically
|
|
8128
|
+
delete languages.plain;
|
|
7437
8129
|
const sortedEntries = Object.entries(languages)
|
|
7438
|
-
.sort((
|
|
8130
|
+
.sort((a, b) => a[1].localeCompare(b[1]));
|
|
8131
|
+
return { plain: "Plain text", ...Object.fromEntries(sortedEntries) }
|
|
8132
|
+
}
|
|
7439
8133
|
|
|
7440
|
-
|
|
7441
|
-
const
|
|
7442
|
-
|
|
7443
|
-
|
|
8134
|
+
#dispatchOpenEvent(event) {
|
|
8135
|
+
const handled = !dispatch(this.editorElement, "lexxy:code-language-picker-open", {
|
|
8136
|
+
languages: this.#bridgeLanguages,
|
|
8137
|
+
currentLanguage: this.languagePickerElement.value
|
|
8138
|
+
}, true);
|
|
8139
|
+
|
|
8140
|
+
if (handled) {
|
|
8141
|
+
event.preventDefault();
|
|
8142
|
+
}
|
|
8143
|
+
}
|
|
8144
|
+
|
|
8145
|
+
get #bridgeLanguages() {
|
|
8146
|
+
return Object.entries(this.#languages).map(([ key, name ]) => ({ key, name }))
|
|
7444
8147
|
}
|
|
7445
8148
|
|
|
7446
8149
|
#updateCodeBlockLanguage(language) {
|
|
@@ -7454,8 +8157,8 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
7454
8157
|
}
|
|
7455
8158
|
|
|
7456
8159
|
#monitorForCodeBlockSelection() {
|
|
7457
|
-
this.
|
|
7458
|
-
|
|
8160
|
+
this.#listeners.track(this.editor.registerUpdateListener(({ editorState }) => {
|
|
8161
|
+
editorState.read(() => {
|
|
7459
8162
|
const codeNode = this.#getCurrentCodeNode();
|
|
7460
8163
|
|
|
7461
8164
|
if (codeNode) {
|
|
@@ -7464,26 +8167,11 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
7464
8167
|
this.#hide();
|
|
7465
8168
|
}
|
|
7466
8169
|
});
|
|
7467
|
-
});
|
|
8170
|
+
}));
|
|
7468
8171
|
}
|
|
7469
8172
|
|
|
7470
8173
|
#getCurrentCodeNode() {
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
if (!$isRangeSelection(selection)) {
|
|
7474
|
-
return null
|
|
7475
|
-
}
|
|
7476
|
-
|
|
7477
|
-
const anchorNode = selection.anchor.getNode();
|
|
7478
|
-
const parentNode = anchorNode.getParent();
|
|
7479
|
-
|
|
7480
|
-
if ($isCodeNode(anchorNode)) {
|
|
7481
|
-
return anchorNode
|
|
7482
|
-
} else if ($isCodeNode(parentNode)) {
|
|
7483
|
-
return parentNode
|
|
7484
|
-
}
|
|
7485
|
-
|
|
7486
|
-
return null
|
|
8174
|
+
return this.editorElement.selection.nearestNodeOfType(CodeNode)
|
|
7487
8175
|
}
|
|
7488
8176
|
|
|
7489
8177
|
#codeNodeWasSelected(codeNode) {
|
|
@@ -7570,6 +8258,8 @@ class NodeDeleteButton extends HTMLElement {
|
|
|
7570
8258
|
}
|
|
7571
8259
|
|
|
7572
8260
|
class TableController {
|
|
8261
|
+
#listeners = new ListenerBin()
|
|
8262
|
+
|
|
7573
8263
|
constructor(editorElement) {
|
|
7574
8264
|
this.editor = editorElement.editor;
|
|
7575
8265
|
this.contents = editorElement.contents;
|
|
@@ -7585,7 +8275,7 @@ class TableController {
|
|
|
7585
8275
|
this.currentTableNodeKey = null;
|
|
7586
8276
|
this.currentCellKey = null;
|
|
7587
8277
|
|
|
7588
|
-
this.#
|
|
8278
|
+
this.#listeners.dispose();
|
|
7589
8279
|
}
|
|
7590
8280
|
|
|
7591
8281
|
get currentCell() {
|
|
@@ -7867,16 +8557,10 @@ class TableController {
|
|
|
7867
8557
|
|
|
7868
8558
|
#registerKeyHandlers() {
|
|
7869
8559
|
// We can't prevent these externally using regular keydown because Lexical handles it first.
|
|
7870
|
-
this.
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
#unregisterKeyHandlers() {
|
|
7875
|
-
this.unregisterBackspaceKeyHandler?.();
|
|
7876
|
-
this.unregisterEnterKeyHandler?.();
|
|
7877
|
-
|
|
7878
|
-
this.unregisterBackspaceKeyHandler = null;
|
|
7879
|
-
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
|
+
);
|
|
7880
8564
|
}
|
|
7881
8565
|
|
|
7882
8566
|
#handleBackspaceKey(event) {
|
|
@@ -7970,6 +8654,8 @@ var TableIcons = {
|
|
|
7970
8654
|
};
|
|
7971
8655
|
|
|
7972
8656
|
class TableTools extends HTMLElement {
|
|
8657
|
+
#listeners = new ListenerBin()
|
|
8658
|
+
|
|
7973
8659
|
connectedCallback() {
|
|
7974
8660
|
this.tableController = new TableController(this.#editorElement);
|
|
7975
8661
|
this.classList.add("lexxy-floating-controls");
|
|
@@ -7985,12 +8671,7 @@ class TableTools extends HTMLElement {
|
|
|
7985
8671
|
}
|
|
7986
8672
|
|
|
7987
8673
|
dispose() {
|
|
7988
|
-
this.#
|
|
7989
|
-
|
|
7990
|
-
this.unregisterUpdateListener?.();
|
|
7991
|
-
this.unregisterUpdateListener = null;
|
|
7992
|
-
|
|
7993
|
-
this.removeEventListener("keydown", this.#handleToolsKeydown);
|
|
8674
|
+
this.#listeners.dispose();
|
|
7994
8675
|
|
|
7995
8676
|
this.tableController?.destroy();
|
|
7996
8677
|
this.tableController = null;
|
|
@@ -8015,7 +8696,7 @@ class TableTools extends HTMLElement {
|
|
|
8015
8696
|
this.appendChild(this.#createColumnButtonsContainer());
|
|
8016
8697
|
|
|
8017
8698
|
this.appendChild(this.#createDeleteTableButton());
|
|
8018
|
-
this.
|
|
8699
|
+
this.#listeners.track(registerEventListener(this, "keydown", this.#handleToolsKeydown));
|
|
8019
8700
|
}
|
|
8020
8701
|
|
|
8021
8702
|
#createButtonsContainer(childType, setCountProperty, moreMenu) {
|
|
@@ -8108,12 +8789,7 @@ class TableTools extends HTMLElement {
|
|
|
8108
8789
|
}
|
|
8109
8790
|
|
|
8110
8791
|
#registerKeyboardShortcuts() {
|
|
8111
|
-
this.
|
|
8112
|
-
}
|
|
8113
|
-
|
|
8114
|
-
#unregisterKeyboardShortcuts() {
|
|
8115
|
-
this.unregisterKeyboardShortcuts?.();
|
|
8116
|
-
this.unregisterKeyboardShortcuts = null;
|
|
8792
|
+
this.#listeners.track(this.#editor.registerCommand(KEY_DOWN_COMMAND, this.#handleAccessibilityShortcutKey, COMMAND_PRIORITY_HIGH));
|
|
8117
8793
|
}
|
|
8118
8794
|
|
|
8119
8795
|
#handleAccessibilityShortcutKey = (event) => {
|
|
@@ -8183,7 +8859,7 @@ class TableTools extends HTMLElement {
|
|
|
8183
8859
|
}
|
|
8184
8860
|
|
|
8185
8861
|
#monitorForTableSelection() {
|
|
8186
|
-
this.
|
|
8862
|
+
this.#listeners.track(this.#editor.registerUpdateListener(() => {
|
|
8187
8863
|
this.tableController.updateSelectedTable();
|
|
8188
8864
|
|
|
8189
8865
|
const tableNode = this.tableController.currentTableNode;
|
|
@@ -8192,7 +8868,7 @@ class TableTools extends HTMLElement {
|
|
|
8192
8868
|
} else {
|
|
8193
8869
|
this.#hide();
|
|
8194
8870
|
}
|
|
8195
|
-
});
|
|
8871
|
+
}));
|
|
8196
8872
|
}
|
|
8197
8873
|
|
|
8198
8874
|
#executeTableCommand(command) {
|
|
@@ -8304,9 +8980,107 @@ function defineElements() {
|
|
|
8304
8980
|
});
|
|
8305
8981
|
}
|
|
8306
8982
|
|
|
8983
|
+
class NativeAdapter {
|
|
8984
|
+
frozenLinkKey = null
|
|
8985
|
+
|
|
8986
|
+
constructor(editorElement) {
|
|
8987
|
+
this.editorElement = editorElement;
|
|
8988
|
+
this.editorContentElement = editorElement.editorContentElement;
|
|
8989
|
+
}
|
|
8990
|
+
|
|
8991
|
+
dispatchAttributesChange(attributes, linkHref, highlight, headingTag) {
|
|
8992
|
+
dispatch(this.editorElement, "lexxy:attributes-change", {
|
|
8993
|
+
attributes,
|
|
8994
|
+
link: linkHref ? { href: linkHref } : null,
|
|
8995
|
+
highlight,
|
|
8996
|
+
headingTag
|
|
8997
|
+
});
|
|
8998
|
+
}
|
|
8999
|
+
|
|
9000
|
+
dispatchEditorInitialized(detail) {
|
|
9001
|
+
dispatch(this.editorElement, "lexxy:editor-initialized", detail);
|
|
9002
|
+
}
|
|
9003
|
+
|
|
9004
|
+
freeze() {
|
|
9005
|
+
let frozenLinkKey = null;
|
|
9006
|
+
this.editorElement.editor?.getEditorState().read(() => {
|
|
9007
|
+
const selection = $getSelection();
|
|
9008
|
+
if (!$isRangeSelection(selection)) return
|
|
9009
|
+
|
|
9010
|
+
const linkNode = $getNearestNodeOfType(selection.anchor.getNode(), LinkNode);
|
|
9011
|
+
if (linkNode) {
|
|
9012
|
+
frozenLinkKey = linkNode.getKey();
|
|
9013
|
+
}
|
|
9014
|
+
});
|
|
9015
|
+
|
|
9016
|
+
this.frozenLinkKey = frozenLinkKey;
|
|
9017
|
+
this.editorContentElement.contentEditable = "false";
|
|
9018
|
+
}
|
|
9019
|
+
|
|
9020
|
+
thaw() {
|
|
9021
|
+
this.editorContentElement.contentEditable = "true";
|
|
9022
|
+
}
|
|
9023
|
+
|
|
9024
|
+
unlinkFrozenNode() {
|
|
9025
|
+
const key = this.frozenLinkKey;
|
|
9026
|
+
if (!key) return false
|
|
9027
|
+
|
|
9028
|
+
const linkNode = $getNodeByKey(key);
|
|
9029
|
+
if (!$isLinkNode(linkNode)) {
|
|
9030
|
+
this.frozenLinkKey = null;
|
|
9031
|
+
return false
|
|
9032
|
+
}
|
|
9033
|
+
|
|
9034
|
+
const children = linkNode.getChildren();
|
|
9035
|
+
for (const child of children) {
|
|
9036
|
+
linkNode.insertBefore(child);
|
|
9037
|
+
}
|
|
9038
|
+
linkNode.remove();
|
|
9039
|
+
|
|
9040
|
+
// Select the former link text so a follow-up createLink can re-wrap it.
|
|
9041
|
+
const firstText = this.#findFirstTextDescendant(children);
|
|
9042
|
+
const lastText = this.#findLastTextDescendant(children);
|
|
9043
|
+
if (firstText && lastText) {
|
|
9044
|
+
const selection = $getSelection();
|
|
9045
|
+
if ($isRangeSelection(selection)) {
|
|
9046
|
+
selection.anchor.set(firstText.getKey(), 0, "text");
|
|
9047
|
+
selection.focus.set(lastText.getKey(), lastText.getTextContent().length, "text");
|
|
9048
|
+
}
|
|
9049
|
+
}
|
|
9050
|
+
|
|
9051
|
+
this.frozenLinkKey = null;
|
|
9052
|
+
return true
|
|
9053
|
+
}
|
|
9054
|
+
|
|
9055
|
+
#findFirstTextDescendant(nodes) {
|
|
9056
|
+
for (const node of nodes) {
|
|
9057
|
+
if ($isTextNode(node)) return node
|
|
9058
|
+
if ($isElementNode(node)) {
|
|
9059
|
+
const nestedTextNode = this.#findFirstTextDescendant(node.getChildren());
|
|
9060
|
+
if (nestedTextNode) return nestedTextNode
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
9063
|
+
|
|
9064
|
+
return null
|
|
9065
|
+
}
|
|
9066
|
+
|
|
9067
|
+
#findLastTextDescendant(nodes) {
|
|
9068
|
+
for (let index = nodes.length - 1; index >= 0; index--) {
|
|
9069
|
+
const node = nodes[index];
|
|
9070
|
+
if ($isTextNode(node)) return node
|
|
9071
|
+
if ($isElementNode(node)) {
|
|
9072
|
+
const nestedTextNode = this.#findLastTextDescendant(node.getChildren());
|
|
9073
|
+
if (nestedTextNode) return nestedTextNode
|
|
9074
|
+
}
|
|
9075
|
+
}
|
|
9076
|
+
|
|
9077
|
+
return null
|
|
9078
|
+
}
|
|
9079
|
+
}
|
|
9080
|
+
|
|
8307
9081
|
const configure = Lexxy.configure;
|
|
8308
9082
|
|
|
8309
9083
|
// Pushing elements definition to after the current call stack to allow global configuration to take place first
|
|
8310
9084
|
setTimeout(defineElements, 0);
|
|
8311
9085
|
|
|
8312
|
-
export { $createActionTextAttachmentNode, $createActionTextAttachmentUploadNode, $isActionTextAttachmentNode, ActionTextAttachmentNode, ActionTextAttachmentUploadNode, CustomActionTextAttachmentNode, LexxyExtension as Extension, HorizontalDividerNode, configure };
|
|
9086
|
+
export { $createActionTextAttachmentNode, $createActionTextAttachmentUploadNode, $isActionTextAttachmentNode, ActionTextAttachmentNode, ActionTextAttachmentUploadNode, CustomActionTextAttachmentNode, LexxyExtension as Extension, HorizontalDividerNode, NativeAdapter, configure };
|