@37signals/lexxy 0.9.10-beta → 0.9.12-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 +1427 -1082
- package/dist/lexxy_helpers.esm.js +3 -63
- package/dist/stylesheets/lexxy-editor.css +124 -113
- package/package.json +24 -17
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { isActiveAndVisible, extractPlainTextFromHtml, createElement, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
|
|
2
1
|
export { highlightCode } from './lexxy_helpers.esm.js';
|
|
3
2
|
import DOMPurify from 'dompurify';
|
|
4
|
-
import { getStyleObjectFromCSS, getCSSFromStyleObject, $
|
|
5
|
-
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isTextNode, $isParagraphNode, $splitNode, $getSiblingCaret, LineBreakNode, $createParagraphNode, TextNode, createCommand, defineExtension, COMMAND_PRIORITY_EDITOR, $getEditor, $getNodeByKey, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $createRangeSelection, $setSelection, createState, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND,
|
|
3
|
+
import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $ensureForwardRangeSelection, $isAtNodeEnd, $patchStyleText, $setBlocksType, $forEachSelectedTextNode } from '@lexical/selection';
|
|
4
|
+
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isTextNode, $isParagraphNode, $splitNode, $getSiblingCaret, LineBreakNode, $createParagraphNode, $getCommonAncestor, $findMatchingParent, TextNode, createCommand, defineExtension, COMMAND_PRIORITY_EDITOR, $getEditor, $getNodeByKey, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $createRangeSelection, $setSelection, createState, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, ElementNode, $getChildCaretAtIndex, $createLineBreakNode, PASTE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, CLEAR_HISTORY_COMMAND, $onUpdate, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
5
|
+
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
|
|
6
6
|
import { buildEditorFromExtensions } from '@lexical/extension';
|
|
7
7
|
import { ListNode, ListItemNode, $getListDepth, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $isListItemNode, $isListNode, registerList } from '@lexical/list';
|
|
8
|
-
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
|
|
9
8
|
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, $descendantsMatching, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, IS_APPLE } from '@lexical/utils';
|
|
10
9
|
import { registerPlainText } from '@lexical/plain-text';
|
|
11
10
|
import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
@@ -126,6 +125,78 @@ class ListenerBin {
|
|
|
126
125
|
}
|
|
127
126
|
}
|
|
128
127
|
|
|
128
|
+
function createElement(name, properties, content = "") {
|
|
129
|
+
const element = document.createElement(name);
|
|
130
|
+
for (const [ key, value ] of Object.entries(properties || {})) {
|
|
131
|
+
if (key === "dataset") {
|
|
132
|
+
Object.entries(value).forEach(([ key, value ]) => (element.dataset[key] = value));
|
|
133
|
+
} else if (key in element) {
|
|
134
|
+
element[key] = value;
|
|
135
|
+
} else if (value !== null && value !== undefined) {
|
|
136
|
+
element.setAttribute(key, value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (content) {
|
|
140
|
+
element.innerHTML = content;
|
|
141
|
+
}
|
|
142
|
+
return element
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseHtml(html) {
|
|
146
|
+
const parser = new DOMParser();
|
|
147
|
+
return parser.parseFromString(html, "text/html")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function createAttachmentFigure(contentType, isPreviewable, fileName) {
|
|
151
|
+
const extension = fileName ? fileName.split(".").pop().toLowerCase() : "unknown";
|
|
152
|
+
return createElement("figure", {
|
|
153
|
+
className: `attachment attachment--${isPreviewable ? "preview" : "file"} attachment--${extension}`,
|
|
154
|
+
"data-content-type": contentType
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isPreviewableImage(contentType) {
|
|
159
|
+
return contentType.startsWith("image/") && !contentType.includes("svg")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function dispatch(element, eventName, detail = null, cancelable = false) {
|
|
163
|
+
return element.dispatchEvent(new CustomEvent(eventName, { bubbles: true, detail, cancelable }))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function addBlockSpacing(doc) {
|
|
167
|
+
const blocks = doc.querySelectorAll("body > :not(h1, h2, h3, h4, h5, h6) + *");
|
|
168
|
+
for (const block of blocks) {
|
|
169
|
+
const spacer = doc.createElement("p");
|
|
170
|
+
spacer.appendChild(doc.createElement("br"));
|
|
171
|
+
block.before(spacer);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function generateDomId(prefix) {
|
|
176
|
+
const randomPart = Math.random().toString(36).slice(2, 10);
|
|
177
|
+
return `${prefix}-${randomPart}`
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function extractPlainTextFromHtml(innerHtml = "") {
|
|
181
|
+
return parseHtml(innerHtml).body.textContent.trim()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isActiveAndVisible(element) {
|
|
185
|
+
return element && !element.disabled && checkVisibility(element)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// no `checkVisibility` in Safari < 17.4
|
|
189
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/checkVisibility#browser_compatibility
|
|
190
|
+
function checkVisibility(element, options) {
|
|
191
|
+
if (element.checkVisibility) {
|
|
192
|
+
return element.checkVisibility(options)
|
|
193
|
+
} else {
|
|
194
|
+
// Will not work for body or a fixed position element child of the body
|
|
195
|
+
// which is OK since that doesn't apply in the toolbar where this is used
|
|
196
|
+
return Boolean(element.offsetParent)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
129
200
|
function handleRollingTabIndex(elements, event) {
|
|
130
201
|
const previousActiveElement = document.activeElement;
|
|
131
202
|
|
|
@@ -336,6 +407,7 @@ var ToolbarIcons = {
|
|
|
336
407
|
class LexicalToolbarElement extends HTMLElement {
|
|
337
408
|
static observedAttributes = [ "connected" ]
|
|
338
409
|
#listeners = new ListenerBin()
|
|
410
|
+
#refreshToolbarAF = null
|
|
339
411
|
|
|
340
412
|
constructor() {
|
|
341
413
|
super();
|
|
@@ -346,7 +418,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
346
418
|
}
|
|
347
419
|
|
|
348
420
|
connectedCallback() {
|
|
349
|
-
|
|
421
|
+
this.requestOverflowRefresh();
|
|
350
422
|
this.setAttribute("role", "toolbar");
|
|
351
423
|
this.#installResizeObserver();
|
|
352
424
|
}
|
|
@@ -358,9 +430,12 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
358
430
|
dispose() {
|
|
359
431
|
this.#listeners.dispose();
|
|
360
432
|
|
|
433
|
+
cancelAnimationFrame(this.#refreshToolbarAF);
|
|
434
|
+
|
|
361
435
|
this.editorElement = null;
|
|
362
436
|
this.editor = null;
|
|
363
437
|
this.selection = null;
|
|
438
|
+
this.#refreshToolbarAF = null;
|
|
364
439
|
|
|
365
440
|
this.#createEditorPromise();
|
|
366
441
|
}
|
|
@@ -386,10 +461,9 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
386
461
|
this.#bindButtons();
|
|
387
462
|
this.#bindHotkeys();
|
|
388
463
|
this.#resetTabIndexValues();
|
|
389
|
-
this.#setItemPositionValues();
|
|
390
464
|
this.#monitorSelectionChanges();
|
|
391
465
|
this.#monitorHistoryChanges();
|
|
392
|
-
this
|
|
466
|
+
this.requestOverflowRefresh();
|
|
393
467
|
this.#bindFocusListeners();
|
|
394
468
|
|
|
395
469
|
this.resolveEditorPromise(editorElement);
|
|
@@ -401,6 +475,15 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
401
475
|
return this.editorElement || await this.editorPromise
|
|
402
476
|
}
|
|
403
477
|
|
|
478
|
+
requestOverflowRefresh() {
|
|
479
|
+
if (this.#refreshToolbarAF != null) return
|
|
480
|
+
|
|
481
|
+
this.#refreshToolbarAF = requestAnimationFrame(() => {
|
|
482
|
+
this.#refreshOverflow();
|
|
483
|
+
this.#refreshToolbarAF = null;
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
404
487
|
#reconnect() {
|
|
405
488
|
this.disconnectedCallback();
|
|
406
489
|
this.connectedCallback();
|
|
@@ -415,7 +498,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
415
498
|
}
|
|
416
499
|
|
|
417
500
|
#installResizeObserver() {
|
|
418
|
-
const resizeObserver = new ResizeObserver(() => this
|
|
501
|
+
const resizeObserver = new ResizeObserver(() => this.requestOverflowRefresh());
|
|
419
502
|
resizeObserver.observe(this);
|
|
420
503
|
this.#listeners.track(() => resizeObserver.disconnect());
|
|
421
504
|
}
|
|
@@ -482,30 +565,29 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
482
565
|
}
|
|
483
566
|
|
|
484
567
|
#handleEditorFocus = () => {
|
|
485
|
-
const firstVisible = this.#
|
|
568
|
+
const firstVisible = this.#buttons.find(isActiveAndVisible);
|
|
486
569
|
if (firstVisible) firstVisible.tabIndex = 0;
|
|
487
570
|
}
|
|
488
571
|
|
|
489
572
|
#handleEditorBlur = () => {
|
|
490
573
|
this.#resetTabIndexValues();
|
|
491
|
-
this.#closeDropdowns();
|
|
492
574
|
}
|
|
493
575
|
|
|
494
576
|
#handleKeydown = (event) => {
|
|
495
|
-
handleRollingTabIndex(this.#
|
|
577
|
+
handleRollingTabIndex(this.#buttons, event);
|
|
496
578
|
}
|
|
497
579
|
|
|
498
580
|
#resetTabIndexValues() {
|
|
499
|
-
this.#
|
|
581
|
+
this.#buttons.forEach((button) => {
|
|
500
582
|
button.tabIndex = -1;
|
|
501
583
|
});
|
|
502
584
|
}
|
|
503
585
|
|
|
504
586
|
#monitorSelectionChanges() {
|
|
505
|
-
this.#listeners.track(this.editor.registerUpdateListener(() => {
|
|
506
|
-
|
|
587
|
+
this.#listeners.track(this.editor.registerUpdateListener(({ editorState }) => {
|
|
588
|
+
editorState.read(() => {
|
|
507
589
|
this.#updateButtonStates();
|
|
508
|
-
this
|
|
590
|
+
this.closeDropdowns();
|
|
509
591
|
});
|
|
510
592
|
}));
|
|
511
593
|
}
|
|
@@ -553,41 +635,53 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
553
635
|
#setButtonPressed(name, isPressed) {
|
|
554
636
|
const button = this.querySelector(`[name="${name}"]`);
|
|
555
637
|
if (button) {
|
|
556
|
-
|
|
638
|
+
const next = isPressed.toString();
|
|
639
|
+
if (button.getAttribute("aria-pressed") !== next) {
|
|
640
|
+
button.setAttribute("aria-pressed", next);
|
|
641
|
+
}
|
|
557
642
|
}
|
|
558
643
|
}
|
|
559
644
|
|
|
560
645
|
#setButtonDisabled(name, isDisabled) {
|
|
561
646
|
const button = this.querySelector(`[name="${name}"]`);
|
|
562
647
|
if (button) {
|
|
563
|
-
button.disabled
|
|
564
|
-
|
|
648
|
+
if (button.disabled !== isDisabled) {
|
|
649
|
+
button.disabled = isDisabled;
|
|
650
|
+
}
|
|
651
|
+
const next = isDisabled.toString();
|
|
652
|
+
if (button.getAttribute("aria-disabled") !== next) {
|
|
653
|
+
button.setAttribute("aria-disabled", next);
|
|
654
|
+
}
|
|
565
655
|
}
|
|
566
656
|
}
|
|
567
657
|
|
|
568
|
-
#
|
|
658
|
+
#refreshOverflow() {
|
|
569
659
|
this.#resetToolbarOverflow();
|
|
660
|
+
this.#reindexToolbarItems();
|
|
570
661
|
this.#compactMenu();
|
|
571
662
|
|
|
572
|
-
this.#overflow.style.display = this.#overflowMenu.children.length ? "block" : "none";
|
|
573
|
-
this.#overflow.setAttribute("nonce", getNonce());
|
|
574
|
-
|
|
575
663
|
const isOverflowing = this.#overflowMenu.children.length > 0;
|
|
664
|
+
|
|
576
665
|
this.toggleAttribute("overflowing", isOverflowing);
|
|
666
|
+
|
|
667
|
+
this.#overflow.style.display = isOverflowing ? "block" : "none";
|
|
668
|
+
this.#overflow.setAttribute("nonce", getNonce());
|
|
669
|
+
|
|
577
670
|
this.#overflowMenu.toggleAttribute("disabled", !isOverflowing);
|
|
578
671
|
}
|
|
579
672
|
|
|
580
673
|
// Separates layout reads from DOM writes to avoid forced reflows during init.
|
|
581
674
|
// Measures every button's right edge in a single read pass, figures out which
|
|
582
675
|
// buttons overflow using math, and then moves them in a single write pass.
|
|
583
|
-
// The previous implementation interleaved `scrollWidth`/`clientWidth` reads with
|
|
584
|
-
// `prepend()` writes inside a loop, forcing one full browser reflow per button.
|
|
585
676
|
#compactMenu() {
|
|
586
|
-
const buttons = this.#
|
|
677
|
+
const buttons = this.#overflowButtons;
|
|
587
678
|
if (buttons.length === 0) return
|
|
588
679
|
|
|
589
|
-
const availableWidth = this.clientWidth
|
|
590
|
-
const buttonRightEdges = buttons.map(button =>
|
|
680
|
+
const availableWidth = this.clientWidth;
|
|
681
|
+
const buttonRightEdges = buttons.map(button => {
|
|
682
|
+
const style = window.getComputedStyle(button);
|
|
683
|
+
return button.offsetLeft + button.offsetWidth + parseFloat(style.marginRight)
|
|
684
|
+
});
|
|
591
685
|
|
|
592
686
|
let firstOverflowing = -1;
|
|
593
687
|
for (let i = 0; i < buttons.length; i++) {
|
|
@@ -600,12 +694,12 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
600
694
|
if (firstOverflowing === -1) return
|
|
601
695
|
|
|
602
696
|
// Move one extra button to reserve space for the overflow control, which is
|
|
603
|
-
// `display: none` until we show it
|
|
604
|
-
// "move one more after it stops overflowing" behaviour.
|
|
697
|
+
// `display: none` until we show it
|
|
605
698
|
const overflowIndex = Math.max(0, firstOverflowing - 1);
|
|
606
699
|
const overflowButtons = buttons.slice(overflowIndex).reverse();
|
|
607
700
|
for (const button of overflowButtons) {
|
|
608
701
|
this.#overflowMenu.prepend(button);
|
|
702
|
+
button.role = "menuitem";
|
|
609
703
|
}
|
|
610
704
|
}
|
|
611
705
|
|
|
@@ -615,6 +709,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
615
709
|
|
|
616
710
|
for (const item of items) {
|
|
617
711
|
const nextItem = this.querySelector(`[data-position="${this.#itemPosition(item) + 1}"]`) ?? this.#overflow;
|
|
712
|
+
item.removeAttribute("role");
|
|
618
713
|
this.insertBefore(item, nextItem);
|
|
619
714
|
}
|
|
620
715
|
}
|
|
@@ -623,22 +718,22 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
623
718
|
return parseInt(item.dataset.position ?? "999")
|
|
624
719
|
}
|
|
625
720
|
|
|
626
|
-
#
|
|
721
|
+
#reindexToolbarItems() {
|
|
627
722
|
this.#toolbarItems.forEach((item, index) => {
|
|
628
|
-
|
|
629
|
-
item.dataset.position = index;
|
|
630
|
-
}
|
|
723
|
+
item.dataset.position = index;
|
|
631
724
|
});
|
|
632
725
|
}
|
|
633
726
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
727
|
+
closeDropdowns({ except } = {}) {
|
|
728
|
+
this.#dropdowns.forEach((dropdown) => {
|
|
729
|
+
if (dropdown !== except) {
|
|
730
|
+
dropdown.close({ focusEditor: false });
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
639
734
|
|
|
640
735
|
get #dropdowns() {
|
|
641
|
-
return this.querySelectorAll("
|
|
736
|
+
return this.querySelectorAll(":scope .lexxy-editor__toolbar-dropdown")
|
|
642
737
|
}
|
|
643
738
|
|
|
644
739
|
get #overflow() {
|
|
@@ -646,15 +741,15 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
646
741
|
}
|
|
647
742
|
|
|
648
743
|
get #overflowMenu() {
|
|
649
|
-
return this
|
|
744
|
+
return this.#overflow?.querySelector(":scope > [data-dropdown-panel]")
|
|
650
745
|
}
|
|
651
746
|
|
|
652
|
-
get #
|
|
747
|
+
get #overflowButtons() {
|
|
653
748
|
return Array.from(this.querySelectorAll(":scope > button:not([data-prevent-overflow='true'])"))
|
|
654
749
|
}
|
|
655
750
|
|
|
656
|
-
get #
|
|
657
|
-
return Array.from(this.querySelectorAll(":scope button
|
|
751
|
+
get #buttons() {
|
|
752
|
+
return Array.from(this.querySelectorAll(":scope button"))
|
|
658
753
|
}
|
|
659
754
|
|
|
660
755
|
get #toolbarItems() {
|
|
@@ -662,6 +757,8 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
662
757
|
}
|
|
663
758
|
|
|
664
759
|
static get defaultTemplate() {
|
|
760
|
+
const linkInputId = generateDomId("lexxy-link-url");
|
|
761
|
+
|
|
665
762
|
return `
|
|
666
763
|
<button class="lexxy-editor__toolbar-button" type="button" name="image" data-command="uploadImage" data-prevent-overflow="true" title="Add images and video">
|
|
667
764
|
${ToolbarIcons.image}
|
|
@@ -679,59 +776,59 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
679
776
|
${ToolbarIcons.italic}
|
|
680
777
|
</button>
|
|
681
778
|
|
|
682
|
-
<
|
|
683
|
-
<
|
|
779
|
+
<lexxy-toolbar-dropdown class="lexxy-editor__toolbar-dropdown">
|
|
780
|
+
<button data-dropdown-trigger class="lexxy-editor__toolbar-button lexxy-editor__toolbar-button--chevron" type="button" name="format" title="Text formatting" aria-haspopup="menu" aria-expanded="false">
|
|
684
781
|
${ToolbarIcons.heading}
|
|
685
|
-
</
|
|
686
|
-
<div class="lexxy-editor__toolbar-dropdown-list">
|
|
687
|
-
<button type="button" name="paragraph" data-command="setFormatParagraph" title="Paragraph">
|
|
782
|
+
</button>
|
|
783
|
+
<div data-dropdown-panel role="menu" class="lexxy-editor__toolbar-dropdown-list" hidden>
|
|
784
|
+
<button type="button" name="paragraph" data-command="setFormatParagraph" title="Paragraph" role="menuitem">
|
|
688
785
|
${ToolbarIcons.paragraph} <span>Normal</span>
|
|
689
786
|
</button>
|
|
690
|
-
<button type="button" name="heading-large" data-command="setFormatHeadingLarge" title="Large heading">
|
|
787
|
+
<button type="button" name="heading-large" data-command="setFormatHeadingLarge" title="Large heading" role="menuitem">
|
|
691
788
|
${ToolbarIcons.h2} <span>Large Heading</span>
|
|
692
789
|
</button>
|
|
693
|
-
<button type="button" name="heading-medium" data-command="setFormatHeadingMedium" title="Medium heading">
|
|
790
|
+
<button type="button" name="heading-medium" data-command="setFormatHeadingMedium" title="Medium heading" role="menuitem">
|
|
694
791
|
${ToolbarIcons.h3} <span>Medium Heading</span>
|
|
695
792
|
</button>
|
|
696
|
-
<button class="lexxy-editor__toolbar-group-end" type="button" name="heading-small" data-command="setFormatHeadingSmall" title="Small heading">
|
|
793
|
+
<button class="lexxy-editor__toolbar-group-end" type="button" name="heading-small" data-command="setFormatHeadingSmall" title="Small heading" role="menuitem">
|
|
697
794
|
${ToolbarIcons.h4} <span>Small Heading</span>
|
|
698
795
|
</button>
|
|
699
796
|
<div class="lexxy-editor__toolbar-separator" role="separator"></div>
|
|
700
|
-
<button type="button" name="strikethrough" data-command="strikethrough" title="Strikethrough">
|
|
797
|
+
<button type="button" name="strikethrough" data-command="strikethrough" title="Strikethrough" role="menuitem">
|
|
701
798
|
${ToolbarIcons.strikethrough} <span>Strikethrough</span>
|
|
702
799
|
</button>
|
|
703
|
-
<button type="button" name="underline" data-command="underline" title="Underline">
|
|
800
|
+
<button type="button" name="underline" data-command="underline" title="Underline" role="menuitem">
|
|
704
801
|
${ToolbarIcons.underline} <span>Underline</span>
|
|
705
802
|
</button>
|
|
706
803
|
<div class="lexxy-editor__toolbar-separator" role="separator"></div>
|
|
707
|
-
<button type="button" name="clear-formatting" data-command="clearFormatting" title="Clear formatting">
|
|
804
|
+
<button type="button" name="clear-formatting" data-command="clearFormatting" title="Clear formatting" role="menuitem">
|
|
708
805
|
${ToolbarIcons.clearFormatting} <span>Clear formatting</span>
|
|
709
806
|
</button>
|
|
710
807
|
</div>
|
|
711
|
-
</
|
|
808
|
+
</lexxy-toolbar-dropdown>
|
|
712
809
|
|
|
713
|
-
<
|
|
714
|
-
<
|
|
810
|
+
<lexxy-highlight-dropdown class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-dropdown--highlight">
|
|
811
|
+
<button data-dropdown-trigger class="lexxy-editor__toolbar-button lexxy-editor__toolbar-button--chevron" type="button" name="highlight" title="Color highlight" aria-haspopup="menu" aria-expanded="false">
|
|
715
812
|
${ToolbarIcons.highlight}
|
|
716
|
-
</
|
|
717
|
-
<
|
|
813
|
+
</button>
|
|
814
|
+
<div data-dropdown-panel role="menu" hidden>
|
|
718
815
|
<div class="lexxy-highlight-colors"></div>
|
|
719
|
-
<button data-command="removeHighlight" class="lexxy-editor__toolbar-button lexxy-editor__toolbar-dropdown-reset">Remove all coloring</button>
|
|
720
|
-
</
|
|
721
|
-
</
|
|
816
|
+
<button data-command="removeHighlight" type="button" class="lexxy-editor__toolbar-button lexxy-editor__toolbar-dropdown-reset" role="menuitem">Remove all coloring</button>
|
|
817
|
+
</div>
|
|
818
|
+
</lexxy-highlight-dropdown>
|
|
722
819
|
|
|
723
|
-
<
|
|
724
|
-
<
|
|
820
|
+
<lexxy-link-dropdown class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-dropdown--link">
|
|
821
|
+
<button data-dropdown-trigger class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="link" title="Link" data-hotkey="cmd+k ctrl+k" aria-haspopup="dialog" aria-expanded="false">
|
|
725
822
|
${ToolbarIcons.link}
|
|
726
|
-
</
|
|
727
|
-
<
|
|
728
|
-
<input type="url" placeholder="Enter a URL…" class="input">
|
|
823
|
+
</button>
|
|
824
|
+
<div data-dropdown-panel role="dialog" aria-label="Link" hidden>
|
|
825
|
+
<input type="url" placeholder="Enter a URL…" class="input" id="${linkInputId}">
|
|
729
826
|
<div class="lexxy-editor__toolbar-dropdown-actions">
|
|
730
827
|
<button type="button" class="lexxy-editor__toolbar-button" value="link">Link</button>
|
|
731
828
|
<button type="button" class="lexxy-editor__toolbar-button" value="unlink">Unlink</button>
|
|
732
829
|
</div>
|
|
733
|
-
</
|
|
734
|
-
</
|
|
830
|
+
</div>
|
|
831
|
+
</lexxy-link-dropdown>
|
|
735
832
|
|
|
736
833
|
<button class="lexxy-editor__toolbar-button" type="button" name="quote" data-command="insertQuoteBlock" title="Quote">
|
|
737
834
|
${ToolbarIcons.quote}
|
|
@@ -756,9 +853,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
756
853
|
${ToolbarIcons.hr}
|
|
757
854
|
</button>
|
|
758
855
|
|
|
759
|
-
<
|
|
760
|
-
|
|
761
|
-
<button class="lexxy-editor__toolbar-button" type="button" name="undo" data-command="undo" title="Undo" disabled aria-disabled="true">
|
|
856
|
+
<button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-button--push-right" type="button" name="undo" data-command="undo" title="Undo" disabled aria-disabled="true">
|
|
762
857
|
${ToolbarIcons.undo}
|
|
763
858
|
</button>
|
|
764
859
|
|
|
@@ -766,399 +861,723 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
766
861
|
${ToolbarIcons.redo}
|
|
767
862
|
</button>
|
|
768
863
|
|
|
769
|
-
<
|
|
770
|
-
<
|
|
771
|
-
|
|
772
|
-
|
|
864
|
+
<lexxy-toolbar-dropdown class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-button--push-right lexxy-editor__toolbar-overflow">
|
|
865
|
+
<button data-dropdown-trigger class="lexxy-editor__toolbar-button" type="button" aria-haspopup="menu" aria-expanded="false" aria-label="Show more toolbar buttons">
|
|
866
|
+
${ToolbarIcons.overflow}
|
|
867
|
+
</button>
|
|
868
|
+
<div data-dropdown-panel role="menu" class="lexxy-editor__toolbar-overflow-menu" aria-label="More toolbar buttons" hidden></div>
|
|
869
|
+
</lexxy-toolbar-dropdown>
|
|
773
870
|
`
|
|
774
871
|
}
|
|
775
872
|
}
|
|
776
873
|
|
|
777
|
-
function
|
|
778
|
-
|
|
779
|
-
for (const [ key, value ] of Object.entries(source)) {
|
|
780
|
-
if (arePlainHashes(target[key], value)) {
|
|
781
|
-
result[key] = deepMerge(target[key], value);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
return result
|
|
786
|
-
}
|
|
874
|
+
function debounce(fn, wait) {
|
|
875
|
+
let timeout;
|
|
787
876
|
|
|
788
|
-
|
|
789
|
-
|
|
877
|
+
return (...args) => {
|
|
878
|
+
clearTimeout(timeout);
|
|
879
|
+
timeout = setTimeout(() => fn(...args), wait);
|
|
880
|
+
}
|
|
790
881
|
}
|
|
791
882
|
|
|
792
|
-
|
|
793
|
-
|
|
883
|
+
function debounceAsync(fn, wait) {
|
|
884
|
+
let timeout;
|
|
794
885
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
}
|
|
886
|
+
return (...args) => {
|
|
887
|
+
clearTimeout(timeout);
|
|
798
888
|
|
|
799
|
-
|
|
800
|
-
|
|
889
|
+
return new Promise((resolve, reject) => {
|
|
890
|
+
timeout = setTimeout(async () => {
|
|
891
|
+
try {
|
|
892
|
+
const result = await fn(...args);
|
|
893
|
+
resolve(result);
|
|
894
|
+
} catch (err) {
|
|
895
|
+
reject(err);
|
|
896
|
+
}
|
|
897
|
+
}, wait);
|
|
898
|
+
})
|
|
801
899
|
}
|
|
900
|
+
}
|
|
802
901
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return keys.reduce((node, key) => node[key], this.#tree)
|
|
806
|
-
}
|
|
902
|
+
function delay(ms) {
|
|
903
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
807
904
|
}
|
|
808
905
|
|
|
809
|
-
function
|
|
810
|
-
return
|
|
906
|
+
function nextFrame() {
|
|
907
|
+
return new Promise(requestAnimationFrame)
|
|
811
908
|
}
|
|
812
909
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
attachmentContentTypeNamespace: "actiontext",
|
|
816
|
-
authenticatedUploads: false,
|
|
817
|
-
extensions: []
|
|
818
|
-
});
|
|
910
|
+
class ToolbarDropdown extends HTMLElement {
|
|
911
|
+
#listeners = new ListenerBin()
|
|
819
912
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
permittedAttachmentTypes: null,
|
|
826
|
-
richText: true,
|
|
827
|
-
toolbar: {
|
|
828
|
-
upload: "both"
|
|
829
|
-
},
|
|
830
|
-
highlight: {
|
|
831
|
-
buttons: {
|
|
832
|
-
color: range(1, 9).map(n => `var(--highlight-${n})`),
|
|
833
|
-
"background-color": range(1, 9).map(n => `var(--highlight-bg-${n})`),
|
|
834
|
-
},
|
|
835
|
-
permit: {
|
|
836
|
-
color: [],
|
|
837
|
-
"background-color": []
|
|
838
|
-
}
|
|
839
|
-
}
|
|
913
|
+
connectedCallback() {
|
|
914
|
+
this.#onToolbarEditor(() => {
|
|
915
|
+
this.#registerListeners();
|
|
916
|
+
this.editorReady();
|
|
917
|
+
});
|
|
840
918
|
}
|
|
841
|
-
});
|
|
842
919
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
presets,
|
|
846
|
-
configure({ global: newGlobal, ...newPresets }) {
|
|
847
|
-
if (newGlobal) {
|
|
848
|
-
global.merge(newGlobal);
|
|
849
|
-
}
|
|
850
|
-
presets.merge(newPresets);
|
|
920
|
+
disconnectedCallback() {
|
|
921
|
+
this.#listeners.dispose();
|
|
851
922
|
}
|
|
852
|
-
};
|
|
853
923
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
}
|
|
924
|
+
editorReady() {}
|
|
925
|
+
onOpen() {}
|
|
926
|
+
onClose() {}
|
|
858
927
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
928
|
+
get trigger() {
|
|
929
|
+
return this.querySelector(":scope > [data-dropdown-trigger]")
|
|
930
|
+
}
|
|
862
931
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
867
|
-
const value = bytes / Math.pow(1024, i);
|
|
868
|
-
return `${ value.toFixed(2) } ${ sizes[i] }`
|
|
869
|
-
}
|
|
932
|
+
get panel() {
|
|
933
|
+
return this.querySelector(":scope > [data-dropdown-panel]")
|
|
934
|
+
}
|
|
870
935
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
}
|
|
936
|
+
get toolbar() {
|
|
937
|
+
return this.closest("lexxy-toolbar")
|
|
938
|
+
}
|
|
874
939
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
function parseAttachmentContent(content) {
|
|
878
|
-
try {
|
|
879
|
-
return JSON.parse(content)
|
|
880
|
-
} catch {
|
|
881
|
-
return content
|
|
940
|
+
get editorElement() {
|
|
941
|
+
return this.toolbar?.editorElement
|
|
882
942
|
}
|
|
883
|
-
}
|
|
884
943
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
return "custom_action_text_attachment"
|
|
944
|
+
get editor() {
|
|
945
|
+
return this.toolbar?.editor
|
|
888
946
|
}
|
|
889
947
|
|
|
890
|
-
|
|
891
|
-
return
|
|
948
|
+
get isOpen() {
|
|
949
|
+
return this.panel.hidden === false
|
|
892
950
|
}
|
|
893
951
|
|
|
894
|
-
|
|
895
|
-
return
|
|
952
|
+
get isClosed() {
|
|
953
|
+
return !this.isOpen
|
|
896
954
|
}
|
|
897
955
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
if (!element.getAttribute("content")) {
|
|
902
|
-
return null
|
|
903
|
-
}
|
|
956
|
+
track(...listeners) {
|
|
957
|
+
this.#listeners.track(...listeners);
|
|
958
|
+
}
|
|
904
959
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
}
|
|
960
|
+
open() {
|
|
961
|
+
if (this.isOpen) return
|
|
962
|
+
this.trigger.setAttribute("aria-expanded", "true");
|
|
963
|
+
this.panel.hidden = false;
|
|
964
|
+
this.onOpen();
|
|
965
|
+
this.#focusFirstInteractive();
|
|
966
|
+
}
|
|
913
967
|
|
|
914
|
-
|
|
968
|
+
close({ focusEditor = true } = {}) {
|
|
969
|
+
if (focusEditor) this.editor?.focus();
|
|
915
970
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}));
|
|
971
|
+
if (this.isClosed) return
|
|
972
|
+
this.trigger.setAttribute("aria-expanded", "false");
|
|
973
|
+
this.panel.hidden = true;
|
|
974
|
+
this.onClose();
|
|
975
|
+
}
|
|
922
976
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
977
|
+
#registerListeners() {
|
|
978
|
+
this.#listeners.track(
|
|
979
|
+
registerEventListener(this, "keydown", this.#handleKeyDown),
|
|
980
|
+
registerEventListener(this.trigger, "click", this.#handleTriggerClick)
|
|
981
|
+
);
|
|
982
|
+
}
|
|
927
983
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
984
|
+
#handleTriggerClick = () => {
|
|
985
|
+
if (this.isOpen) {
|
|
986
|
+
this.close({ focusEditor: false });
|
|
987
|
+
} else {
|
|
988
|
+
this.toolbar?.closeDropdowns({ except: this });
|
|
989
|
+
this.open();
|
|
933
990
|
}
|
|
934
991
|
}
|
|
935
992
|
|
|
936
|
-
|
|
937
|
-
|
|
993
|
+
async #onToolbarEditor(callback) {
|
|
994
|
+
if (!this.toolbar) return
|
|
995
|
+
|
|
996
|
+
await this.toolbar.getEditorElement();
|
|
997
|
+
if (this.isConnected && this.toolbar) callback();
|
|
938
998
|
}
|
|
939
999
|
|
|
940
|
-
|
|
941
|
-
|
|
1000
|
+
#handleKeyDown = (event) => {
|
|
1001
|
+
if (event.key === "Escape") {
|
|
1002
|
+
event.stopPropagation();
|
|
1003
|
+
this.close();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
942
1006
|
|
|
943
|
-
|
|
1007
|
+
async #focusFirstInteractive() {
|
|
1008
|
+
this.#interactiveElements[0]?.focus();
|
|
1009
|
+
await this.#resetTabIndexValues();
|
|
1010
|
+
}
|
|
944
1011
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
this.
|
|
948
|
-
|
|
949
|
-
|
|
1012
|
+
async #resetTabIndexValues() {
|
|
1013
|
+
await nextFrame();
|
|
1014
|
+
this.#buttons.forEach((element, index) => {
|
|
1015
|
+
element.setAttribute("tabindex", index === 0 ? 0 : "-1");
|
|
1016
|
+
});
|
|
950
1017
|
}
|
|
951
1018
|
|
|
952
|
-
|
|
953
|
-
|
|
1019
|
+
get #interactiveElements() {
|
|
1020
|
+
return Array.from(this.panel.querySelectorAll("button, input"))
|
|
1021
|
+
}
|
|
954
1022
|
|
|
955
|
-
|
|
1023
|
+
get #buttons() {
|
|
1024
|
+
return Array.from(this.panel.querySelectorAll("button"))
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
956
1027
|
|
|
957
|
-
|
|
958
|
-
|
|
1028
|
+
const APPLY_HIGHLIGHT_SELECTOR = "button.lexxy-highlight-button";
|
|
1029
|
+
const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
|
|
959
1030
|
|
|
960
|
-
|
|
961
|
-
|
|
1031
|
+
// Use Symbol instead of null since $getSelectionStyleValueForProperty
|
|
1032
|
+
// responds differently for backward selections if null is the default
|
|
1033
|
+
// see https://github.com/facebook/lexical/issues/8013
|
|
1034
|
+
const NO_STYLE = Symbol("no_style");
|
|
962
1035
|
|
|
963
|
-
|
|
964
|
-
|
|
1036
|
+
class HighlightDropdown extends ToolbarDropdown {
|
|
1037
|
+
editorReady() {
|
|
1038
|
+
this.#setUpButtons();
|
|
1039
|
+
this.#registerButtonHandlers();
|
|
965
1040
|
}
|
|
966
1041
|
|
|
967
|
-
|
|
968
|
-
|
|
1042
|
+
onOpen() {
|
|
1043
|
+
this.editor.getEditorState().read(() => {
|
|
1044
|
+
this.#updateColorButtonStates($getSelection());
|
|
1045
|
+
});
|
|
969
1046
|
}
|
|
970
1047
|
|
|
971
|
-
|
|
972
|
-
|
|
1048
|
+
#registerButtonHandlers() {
|
|
1049
|
+
this.#colorButtons.forEach(button => {
|
|
1050
|
+
this.track(registerEventListener(button, "click", this.#handleColorButtonClick));
|
|
1051
|
+
});
|
|
973
1052
|
}
|
|
974
1053
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
}
|
|
1054
|
+
#setUpButtons() {
|
|
1055
|
+
this.#buttonContainer.innerHTML = "";
|
|
978
1056
|
|
|
979
|
-
|
|
980
|
-
const attachment = createElement(this.tagName, {
|
|
981
|
-
sgid: this.sgid,
|
|
982
|
-
content: this.innerHtml,
|
|
983
|
-
"content-type": this.contentType
|
|
984
|
-
});
|
|
1057
|
+
const colorGroups = this.editorElement.config.get("highlight.buttons");
|
|
985
1058
|
|
|
986
|
-
|
|
987
|
-
|
|
1059
|
+
this.#populateButtonGroup("color", colorGroups.color);
|
|
1060
|
+
this.#populateButtonGroup("background-color", colorGroups["background-color"]);
|
|
988
1061
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
type: "custom_action_text_attachment",
|
|
992
|
-
version: 1,
|
|
993
|
-
tagName: this.tagName,
|
|
994
|
-
sgid: this.sgid,
|
|
995
|
-
contentType: this.contentType,
|
|
996
|
-
innerHtml: this.innerHtml,
|
|
997
|
-
plainText: this.plainText
|
|
998
|
-
}
|
|
1062
|
+
const maxNumberOfColors = Math.max(colorGroups.color.length, colorGroups["background-color"].length);
|
|
1063
|
+
this.panel.style.setProperty("--max-colors", maxNumberOfColors);
|
|
999
1064
|
}
|
|
1000
1065
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1066
|
+
#populateButtonGroup(attribute, values) {
|
|
1067
|
+
values.forEach((value, index) => {
|
|
1068
|
+
this.#buttonContainer.appendChild(this.#createButton(attribute, value, index));
|
|
1069
|
+
});
|
|
1003
1070
|
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
function dasherize(value) {
|
|
1007
|
-
return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
|
|
1008
|
-
}
|
|
1009
1071
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1072
|
+
#createButton(attribute, value, index) {
|
|
1073
|
+
return createElement("button", {
|
|
1074
|
+
type: "button",
|
|
1075
|
+
dataset: { value, style: attribute },
|
|
1076
|
+
style: `${attribute}: ${value}`,
|
|
1077
|
+
class: "lexxy-editor__toolbar-button lexxy-highlight-button",
|
|
1078
|
+
name: `${attribute}-${index}`,
|
|
1079
|
+
role: "menuitem"
|
|
1080
|
+
})
|
|
1016
1081
|
}
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
function normalizeFilteredText(string) {
|
|
1020
|
-
return string
|
|
1021
|
-
.toLowerCase()
|
|
1022
|
-
.normalize("NFD").replace(/[\u0300-\u036f]/g, "") // Remove diacritics
|
|
1023
|
-
}
|
|
1024
1082
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const normalizedMatch = normalizeFilteredText(potentialMatch);
|
|
1083
|
+
#handleColorButtonClick = (event) => {
|
|
1084
|
+
event.preventDefault();
|
|
1028
1085
|
|
|
1029
|
-
|
|
1086
|
+
const button = event.target.closest(APPLY_HIGHLIGHT_SELECTOR);
|
|
1087
|
+
if (!button) return
|
|
1030
1088
|
|
|
1031
|
-
|
|
1032
|
-
return match ? match.index : -1
|
|
1033
|
-
}
|
|
1089
|
+
const { style, value } = button.dataset;
|
|
1034
1090
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
}
|
|
1091
|
+
this.editor.dispatchCommand("toggleHighlight", { [style]: value });
|
|
1092
|
+
this.close();
|
|
1093
|
+
}
|
|
1038
1094
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
}
|
|
1095
|
+
#updateColorButtonStates(selection) {
|
|
1096
|
+
if (!$isRangeSelection(selection)) { return }
|
|
1042
1097
|
|
|
1043
|
-
//
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
if (typeof value === "string") return value === "true"
|
|
1047
|
-
return Boolean(value)
|
|
1048
|
-
}
|
|
1098
|
+
// Use non-"" default, so "" indicates mixed highlighting
|
|
1099
|
+
const textColor = $getSelectionStyleValueForProperty(selection, "color", NO_STYLE);
|
|
1100
|
+
const backgroundColor = $getSelectionStyleValueForProperty(selection, "background-color", NO_STYLE);
|
|
1049
1101
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1102
|
+
this.#colorButtons.forEach(button => {
|
|
1103
|
+
const matchesSelection = button.dataset.value === textColor || button.dataset.value === backgroundColor;
|
|
1104
|
+
const next = matchesSelection.toString();
|
|
1105
|
+
if (button.getAttribute("aria-pressed") !== next) {
|
|
1106
|
+
button.setAttribute("aria-pressed", next);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1052
1109
|
|
|
1053
|
-
|
|
1054
|
-
this
|
|
1110
|
+
const hasHighlight = textColor !== NO_STYLE || backgroundColor !== NO_STYLE;
|
|
1111
|
+
this.panel.querySelector(REMOVE_HIGHLIGHT_SELECTOR).disabled = !hasHighlight;
|
|
1055
1112
|
}
|
|
1056
1113
|
|
|
1057
|
-
get
|
|
1058
|
-
return this
|
|
1114
|
+
get #buttonContainer() {
|
|
1115
|
+
return this.panel.querySelector(".lexxy-highlight-colors")
|
|
1059
1116
|
}
|
|
1060
1117
|
|
|
1061
|
-
get
|
|
1062
|
-
return this
|
|
1118
|
+
get #colorButtons() {
|
|
1119
|
+
return Array.from(this.panel.querySelectorAll(APPLY_HIGHLIGHT_SELECTOR))
|
|
1063
1120
|
}
|
|
1121
|
+
}
|
|
1064
1122
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1123
|
+
class LinkDropdown extends ToolbarDropdown {
|
|
1124
|
+
editorReady() {
|
|
1125
|
+
this.input = this.panel.querySelector("input");
|
|
1126
|
+
|
|
1127
|
+
this.track(
|
|
1128
|
+
registerEventListener(this.input, "keydown", this.#handleEnter),
|
|
1129
|
+
registerEventListener(this.linkButton, "click", this.#handleLink),
|
|
1130
|
+
registerEventListener(this.unlinkButton, "click", this.#handleUnlink)
|
|
1131
|
+
);
|
|
1068
1132
|
}
|
|
1069
1133
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1134
|
+
onOpen() {
|
|
1135
|
+
this.input.value = this.#selectedLinkUrl;
|
|
1136
|
+
this.input.required = true;
|
|
1072
1137
|
}
|
|
1073
1138
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1139
|
+
onClose() {
|
|
1140
|
+
this.input.required = false;
|
|
1076
1141
|
}
|
|
1077
1142
|
|
|
1078
|
-
|
|
1143
|
+
get linkButton() {
|
|
1144
|
+
return this.panel.querySelector("[value='link']")
|
|
1145
|
+
}
|
|
1079
1146
|
|
|
1147
|
+
get unlinkButton() {
|
|
1148
|
+
return this.panel.querySelector("[value='unlink']")
|
|
1080
1149
|
}
|
|
1081
|
-
}
|
|
1082
1150
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1151
|
+
#handleEnter = (event) => {
|
|
1152
|
+
if (event.key === "Enter") {
|
|
1153
|
+
event.preventDefault();
|
|
1154
|
+
event.stopPropagation();
|
|
1155
|
+
this.#handleLink(event);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1088
1158
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1159
|
+
#handleLink = () => {
|
|
1160
|
+
if (!this.input.checkValidity()) {
|
|
1161
|
+
this.input.reportValidity();
|
|
1162
|
+
return
|
|
1163
|
+
}
|
|
1092
1164
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
return $wrapNodeInElement(node, $createParagraphNode)
|
|
1096
|
-
} else if (node.isParentRequired()) {
|
|
1097
|
-
const parent = node.createRequiredParent();
|
|
1098
|
-
return $wrapNodeInElement(node, parent)
|
|
1099
|
-
} else {
|
|
1100
|
-
return node
|
|
1165
|
+
this.editor.dispatchCommand("link", this.input.value);
|
|
1166
|
+
this.close();
|
|
1101
1167
|
}
|
|
1102
|
-
}
|
|
1103
1168
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1169
|
+
#handleUnlink = () => {
|
|
1170
|
+
this.editor.dispatchCommand("unlink");
|
|
1171
|
+
this.close();
|
|
1172
|
+
}
|
|
1108
1173
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1174
|
+
get #selectedLinkUrl() {
|
|
1175
|
+
return this.editor.getEditorState().read(() => {
|
|
1176
|
+
const linkNode = this.editorElement.selection.nearestNodeOfType(LinkNode);
|
|
1177
|
+
return linkNode?.getURL() ?? ""
|
|
1178
|
+
})
|
|
1179
|
+
}
|
|
1112
1180
|
}
|
|
1113
1181
|
|
|
1114
|
-
function
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1182
|
+
function deepMerge(target, source) {
|
|
1183
|
+
const result = { ...target, ...source };
|
|
1184
|
+
for (const [ key, value ] of Object.entries(source)) {
|
|
1185
|
+
if (arePlainHashes(target[key], value)) {
|
|
1186
|
+
result[key] = deepMerge(target[key], value);
|
|
1187
|
+
}
|
|
1119
1188
|
}
|
|
1120
|
-
}
|
|
1121
1189
|
|
|
1122
|
-
|
|
1123
|
-
return point.offset === 0
|
|
1190
|
+
return result
|
|
1124
1191
|
}
|
|
1125
1192
|
|
|
1126
|
-
function
|
|
1127
|
-
return
|
|
1128
|
-
...conversionOutput,
|
|
1129
|
-
forChild: (lexicalNode, parentNode) => {
|
|
1130
|
-
const originalForChild = conversionOutput?.forChild ?? (x => x);
|
|
1131
|
-
let childNode = originalForChild(lexicalNode, parentNode);
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
if ($isTextNode(childNode)) {
|
|
1135
|
-
childNode = callbacks.reduce(
|
|
1136
|
-
(childNode, callback) => callback(childNode, element) ?? childNode,
|
|
1137
|
-
childNode
|
|
1138
|
-
);
|
|
1139
|
-
return childNode
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
}))
|
|
1193
|
+
function arePlainHashes(...values) {
|
|
1194
|
+
return values.every(value => value && value.constructor == Object)
|
|
1143
1195
|
}
|
|
1144
1196
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
const converter = nodeKlass.importDOM()?.[conversionName]?.(element);
|
|
1148
|
-
if (!converter) return null
|
|
1197
|
+
class Configuration {
|
|
1198
|
+
#tree = {}
|
|
1149
1199
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1200
|
+
constructor(...configs) {
|
|
1201
|
+
this.merge(...configs);
|
|
1202
|
+
}
|
|
1152
1203
|
|
|
1153
|
-
|
|
1204
|
+
merge(...configs) {
|
|
1205
|
+
return this.#tree = configs.reduce(deepMerge, this.#tree)
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
get(path) {
|
|
1209
|
+
const keys = path.split(".");
|
|
1210
|
+
return keys.reduce((node, key) => node[key], this.#tree)
|
|
1154
1211
|
}
|
|
1155
1212
|
}
|
|
1156
1213
|
|
|
1157
|
-
function
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1214
|
+
function range(from, to) {
|
|
1215
|
+
return [ ...Array(1 + to - from).keys() ].map(i => i + from)
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const global = new Configuration({
|
|
1219
|
+
attachmentTagName: "action-text-attachment",
|
|
1220
|
+
attachmentContentTypeNamespace: "actiontext",
|
|
1221
|
+
authenticatedUploads: false,
|
|
1222
|
+
extensions: []
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
const presets = new Configuration({
|
|
1226
|
+
default: {
|
|
1227
|
+
attachments: true,
|
|
1228
|
+
markdown: true,
|
|
1229
|
+
multiLine: true,
|
|
1230
|
+
permittedAttachmentTypes: null,
|
|
1231
|
+
richText: true,
|
|
1232
|
+
toolbar: {
|
|
1233
|
+
upload: "both"
|
|
1234
|
+
},
|
|
1235
|
+
highlight: {
|
|
1236
|
+
buttons: {
|
|
1237
|
+
color: range(1, 9).map(n => `var(--highlight-${n})`),
|
|
1238
|
+
"background-color": range(1, 9).map(n => `var(--highlight-bg-${n})`),
|
|
1239
|
+
},
|
|
1240
|
+
permit: {
|
|
1241
|
+
color: [],
|
|
1242
|
+
"background-color": []
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
var Lexxy = {
|
|
1249
|
+
global,
|
|
1250
|
+
presets,
|
|
1251
|
+
configure({ global: newGlobal, ...newPresets }) {
|
|
1252
|
+
if (newGlobal) {
|
|
1253
|
+
global.merge(newGlobal);
|
|
1254
|
+
}
|
|
1255
|
+
presets.merge(newPresets);
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
function setSanitizerConfig(allowedTags) {
|
|
1260
|
+
DOMPurify.clearConfig();
|
|
1261
|
+
DOMPurify.setConfig(buildConfig(allowedTags));
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function sanitize(html) {
|
|
1265
|
+
return DOMPurify.sanitize(html)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function bytesToHumanSize(bytes) {
|
|
1269
|
+
if (bytes === 0) return "0 B"
|
|
1270
|
+
const sizes = [ "B", "KB", "MB", "GB", "TB", "PB" ];
|
|
1271
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
1272
|
+
const value = bytes / Math.pow(1024, i);
|
|
1273
|
+
return `${ value.toFixed(2) } ${ sizes[i] }`
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function extractFileName(string) {
|
|
1277
|
+
return string.split("/").pop()
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// The content attribute is raw HTML (matching Trix/ActionText). Older Lexxy
|
|
1281
|
+
// versions JSON-encoded it, so try JSON.parse first for backward compatibility.
|
|
1282
|
+
function parseAttachmentContent(content) {
|
|
1283
|
+
try {
|
|
1284
|
+
return JSON.parse(content)
|
|
1285
|
+
} catch {
|
|
1286
|
+
return content
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function mimeTypeToExtension(mimeType) {
|
|
1291
|
+
if (!mimeType) return null
|
|
1292
|
+
|
|
1293
|
+
const extension = mimeType.split("/")[1];
|
|
1294
|
+
return extension
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
1298
|
+
static getType() {
|
|
1299
|
+
return "custom_action_text_attachment"
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
static clone(node) {
|
|
1303
|
+
return new CustomActionTextAttachmentNode({ ...node }, node.__key)
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
static importJSON(serializedNode) {
|
|
1307
|
+
return new CustomActionTextAttachmentNode({ ...serializedNode })
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
static importDOM() {
|
|
1311
|
+
return {
|
|
1312
|
+
[this.TAG_NAME]: (element) => {
|
|
1313
|
+
if (!element.getAttribute("content")) {
|
|
1314
|
+
return null
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
return {
|
|
1318
|
+
conversion: (attachment) => {
|
|
1319
|
+
// Preserve initial space if present since Lexical removes it
|
|
1320
|
+
const nodes = [];
|
|
1321
|
+
const previousSibling = attachment.previousSibling;
|
|
1322
|
+
if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
|
|
1323
|
+
nodes.push($createTextNode(" "));
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
const innerHtml = parseAttachmentContent(attachment.getAttribute("content"));
|
|
1327
|
+
|
|
1328
|
+
nodes.push(new CustomActionTextAttachmentNode({
|
|
1329
|
+
sgid: attachment.getAttribute("sgid"),
|
|
1330
|
+
innerHtml,
|
|
1331
|
+
plainText: attachment.textContent.trim() || extractPlainTextFromHtml(innerHtml),
|
|
1332
|
+
contentType: attachment.getAttribute("content-type")
|
|
1333
|
+
}));
|
|
1334
|
+
|
|
1335
|
+
const nextSibling = attachment.nextSibling;
|
|
1336
|
+
if (nextSibling && nextSibling.nodeType === Node.TEXT_NODE && /^\s/.test(nextSibling.textContent)) {
|
|
1337
|
+
nodes.push($createTextNode(" "));
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
return { node: nodes }
|
|
1341
|
+
},
|
|
1342
|
+
priority: 2
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
static get TAG_NAME() {
|
|
1349
|
+
return Lexxy.global.get("attachmentTagName")
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
constructor({ tagName, sgid, contentType, innerHtml, plainText }, key) {
|
|
1353
|
+
super(key);
|
|
1354
|
+
|
|
1355
|
+
const contentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace");
|
|
1356
|
+
|
|
1357
|
+
this.tagName = tagName || CustomActionTextAttachmentNode.TAG_NAME;
|
|
1358
|
+
this.sgid = sgid;
|
|
1359
|
+
this.contentType = contentType || `application/vnd.${contentTypeNamespace}.unknown`;
|
|
1360
|
+
this.innerHtml = innerHtml;
|
|
1361
|
+
this.plainText = plainText ?? extractPlainTextFromHtml(innerHtml);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
createDOM() {
|
|
1365
|
+
const figure = createElement(this.tagName, { "content-type": this.contentType, "data-lexxy-decorator": true });
|
|
1366
|
+
|
|
1367
|
+
figure.insertAdjacentHTML("beforeend", sanitize(this.innerHtml));
|
|
1368
|
+
|
|
1369
|
+
const deleteButton = createElement("lexxy-node-delete-button");
|
|
1370
|
+
figure.appendChild(deleteButton);
|
|
1371
|
+
|
|
1372
|
+
return figure
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
updateDOM() {
|
|
1376
|
+
return false
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
getTextContent() {
|
|
1380
|
+
return "\ufeff"
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
getReadableTextContent() {
|
|
1384
|
+
return this.plainText || `[${this.contentType}]`
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
isInline() {
|
|
1388
|
+
return true
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
exportDOM() {
|
|
1392
|
+
const attachment = createElement(this.tagName, {
|
|
1393
|
+
sgid: this.sgid,
|
|
1394
|
+
content: this.innerHtml,
|
|
1395
|
+
"content-type": this.contentType
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
return { element: attachment }
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
exportJSON() {
|
|
1402
|
+
return {
|
|
1403
|
+
type: "custom_action_text_attachment",
|
|
1404
|
+
version: 1,
|
|
1405
|
+
tagName: this.tagName,
|
|
1406
|
+
sgid: this.sgid,
|
|
1407
|
+
contentType: this.contentType,
|
|
1408
|
+
innerHtml: this.innerHtml,
|
|
1409
|
+
plainText: this.plainText
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
decorate() {
|
|
1414
|
+
return null
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function dasherize(value) {
|
|
1419
|
+
return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function isAutolinkableURL(string) {
|
|
1423
|
+
return /^(?:[a-z0-9]+:\/\/|www\.)[^\s]+$/i.test(string)
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function normalizeFilteredText(string) {
|
|
1427
|
+
return string
|
|
1428
|
+
.toLowerCase()
|
|
1429
|
+
.normalize("NFD").replace(/[\u0300-\u036f]/g, "") // Remove diacritics
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function filterMatchPosition(text, potentialMatch) {
|
|
1433
|
+
const normalizedText = normalizeFilteredText(text);
|
|
1434
|
+
const normalizedMatch = normalizeFilteredText(potentialMatch);
|
|
1435
|
+
|
|
1436
|
+
if (!normalizedMatch) return 0
|
|
1437
|
+
|
|
1438
|
+
const match = normalizedText.match(new RegExp(`(?:^|\\b)${escapeForRegExp(normalizedMatch)}`));
|
|
1439
|
+
return match ? match.index : -1
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function upcaseFirst(string) {
|
|
1443
|
+
return string.charAt(0).toUpperCase() + string.slice(1)
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function escapeForRegExp(string) {
|
|
1447
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// Parses a value that may arrive as a boolean or as a string (e.g. from DOM
|
|
1451
|
+
// getAttribute) into a proper boolean. Ensures "false" doesn't evaluate as truthy.
|
|
1452
|
+
function parseBoolean(value) {
|
|
1453
|
+
if (typeof value === "string") return value === "true"
|
|
1454
|
+
return Boolean(value)
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
class LexxyExtension {
|
|
1458
|
+
#editorElement
|
|
1459
|
+
|
|
1460
|
+
constructor(editorElement) {
|
|
1461
|
+
this.#editorElement = editorElement;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
get editorElement() {
|
|
1465
|
+
return this.#editorElement
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
get editorConfig() {
|
|
1469
|
+
return this.#editorElement.config
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// optional: defaults to true
|
|
1473
|
+
get enabled() {
|
|
1474
|
+
return true
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
get lexicalExtension() {
|
|
1478
|
+
return null
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
get allowedElements() {
|
|
1482
|
+
return []
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
initializeToolbar(_lexxyToolbar) {
|
|
1486
|
+
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
dispose() {
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function $containsRangeSelection(node, selection = $getSelection()) {
|
|
1494
|
+
if ($isRangeSelection(selection)) {
|
|
1495
|
+
const { commonAncestor } = $getCommonAncestor(selection.focus.getNode(), selection.anchor.getNode());
|
|
1496
|
+
return $findMatchingParent(commonAncestor, parent => parent.is(node))
|
|
1497
|
+
} else {
|
|
1498
|
+
return false
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function $createNodeSelectionWith(...nodes) {
|
|
1503
|
+
const selection = $createNodeSelection();
|
|
1504
|
+
nodes.forEach(node => selection.add(node.getKey()));
|
|
1505
|
+
return selection
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function $isShadowRoot(node) {
|
|
1509
|
+
return $isElementNode(node) && $isRootOrShadowRoot(node) && !$isRootNode(node)
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function $makeSafeForRoot(node) {
|
|
1513
|
+
if ($isTextNode(node)) {
|
|
1514
|
+
return $wrapNodeInElement(node, $createParagraphNode)
|
|
1515
|
+
} else if (node.isParentRequired()) {
|
|
1516
|
+
const parent = node.createRequiredParent();
|
|
1517
|
+
return $wrapNodeInElement(node, parent)
|
|
1518
|
+
} else {
|
|
1519
|
+
return node
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function getListType(node) {
|
|
1524
|
+
const list = $getNearestNodeOfType(node, ListNode);
|
|
1525
|
+
return list?.getListType() ?? null
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function isEditorFocused(editor) {
|
|
1529
|
+
const rootElement = editor.getRootElement();
|
|
1530
|
+
return rootElement !== null && rootElement.contains(document.activeElement)
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function $isAtNodeEdge(point, atStart = null) {
|
|
1534
|
+
if (atStart === null) {
|
|
1535
|
+
return $isAtNodeEdge(point, true) || $isAtNodeEdge(point, false)
|
|
1536
|
+
} else {
|
|
1537
|
+
return atStart ? $isAtNodeStart(point) : $isAtNodeEnd(point)
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function $isAtNodeStart(point) {
|
|
1542
|
+
return point.offset === 0
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function extendTextNodeConversion(conversionName, ...callbacks) {
|
|
1546
|
+
return extendConversion(TextNode, conversionName, (conversionOutput, element) => ({
|
|
1547
|
+
...conversionOutput,
|
|
1548
|
+
forChild: (lexicalNode, parentNode) => {
|
|
1549
|
+
const originalForChild = conversionOutput?.forChild ?? (x => x);
|
|
1550
|
+
let childNode = originalForChild(lexicalNode, parentNode);
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
if ($isTextNode(childNode)) {
|
|
1554
|
+
childNode = callbacks.reduce(
|
|
1555
|
+
(childNode, callback) => callback(childNode, element) ?? childNode,
|
|
1556
|
+
childNode
|
|
1557
|
+
);
|
|
1558
|
+
return childNode
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}))
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function extendConversion(nodeKlass, conversionName, callback = (output => output)) {
|
|
1565
|
+
return (element) => {
|
|
1566
|
+
const converter = nodeKlass.importDOM()?.[conversionName]?.(element);
|
|
1567
|
+
if (!converter) return null
|
|
1568
|
+
|
|
1569
|
+
const conversionOutput = converter.conversion(element);
|
|
1570
|
+
if (!conversionOutput) return conversionOutput
|
|
1571
|
+
|
|
1572
|
+
return callback(conversionOutput, element) ?? conversionOutput
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function $isCursorOnLastLine(selection) {
|
|
1577
|
+
const anchorNode = selection.anchor.getNode();
|
|
1578
|
+
const elementNode = $isElementNode(anchorNode) ? anchorNode : anchorNode.getParentOrThrow();
|
|
1579
|
+
const children = elementNode.getChildren();
|
|
1580
|
+
if (children.length === 0) return true
|
|
1162
1581
|
|
|
1163
1582
|
const lastChild = children[children.length - 1];
|
|
1164
1583
|
|
|
@@ -1796,11 +2215,6 @@ function $isActionTextAttachmentNode(node) {
|
|
|
1796
2215
|
return node instanceof ActionTextAttachmentNode
|
|
1797
2216
|
}
|
|
1798
2217
|
|
|
1799
|
-
function $generateFilteredNodesFromDOM(editorElement, doc) {
|
|
1800
|
-
const nodes = $generateNodesFromDOM(editorElement.editor, doc);
|
|
1801
|
-
return filterDisallowedAttachmentNodes(nodes, editorElement)
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
2218
|
function filterDisallowedAttachmentNodes(nodes, editorElement) {
|
|
1805
2219
|
return nodes
|
|
1806
2220
|
.filter(node => !isDisallowedAttachment(node, editorElement))
|
|
@@ -1819,6 +2233,67 @@ function isDisallowedAttachment(node, editorElement) {
|
|
|
1819
2233
|
!editorElement.permitsAttachmentContentType(node.contentType)
|
|
1820
2234
|
}
|
|
1821
2235
|
|
|
2236
|
+
// Replaces inline `data:image/...` attachments with upload nodes that flow through the normal
|
|
2237
|
+
// file upload pipeline.
|
|
2238
|
+
//
|
|
2239
|
+
// Without this step, pasted-from-Google-Docs-style content lands in the editor with the entire
|
|
2240
|
+
// base64 image embedded in the attachment's `src`, which then persists into the saved HTML and
|
|
2241
|
+
// bloats the stored document.
|
|
2242
|
+
//
|
|
2243
|
+
// Each conversion dispatches the cancelable `lexxy:file-accept` event so the host's allowlist (and
|
|
2244
|
+
// any other listener) can refuse the synthesized File before it's accepted into the upload
|
|
2245
|
+
// pipeline; on refusal, the node is dropped silently — matching how `Contents#uploadFiles` handles
|
|
2246
|
+
// file-picker rejections.
|
|
2247
|
+
function $convertInlineImageDataURIs(nodes, editorElement) {
|
|
2248
|
+
const topLevel = nodes
|
|
2249
|
+
.map(node => isInlineImageDataURIAttachment(node) ? $tryCreateUploadFromDataURI(node, editorElement) : node)
|
|
2250
|
+
.filter(node => node !== null);
|
|
2251
|
+
|
|
2252
|
+
for (const node of topLevel) {
|
|
2253
|
+
for (const desc of $descendantsMatching([ node ], isInlineImageDataURIAttachment)) {
|
|
2254
|
+
const upload = $tryCreateUploadFromDataURI(desc, editorElement);
|
|
2255
|
+
if (upload) {
|
|
2256
|
+
desc.replace(upload);
|
|
2257
|
+
} else {
|
|
2258
|
+
desc.remove();
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
return topLevel
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
function isInlineImageDataURIAttachment(node) {
|
|
2267
|
+
return node instanceof ActionTextAttachmentNode &&
|
|
2268
|
+
/^data:image\/[^,]*;base64,/i.test(node.src ?? "")
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
function $tryCreateUploadFromDataURI(node, editorElement) {
|
|
2272
|
+
const file = dataURIToFile(node.src);
|
|
2273
|
+
if (file && editorElement.acceptsFile(file)) {
|
|
2274
|
+
return editorElement.contents.$createUploadNode(file)
|
|
2275
|
+
}
|
|
2276
|
+
return null
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
function dataURIToFile(dataURI) {
|
|
2280
|
+
try {
|
|
2281
|
+
const [ header, data ] = dataURI.split(",");
|
|
2282
|
+
|
|
2283
|
+
// https://datatracker.ietf.org/doc/html/rfc6838#section-4.2
|
|
2284
|
+
const mimeType = header.match(/^data:(image\/[A-Za-z0-9][A-Za-z0-9!#$&\-^_.+]*)/)?.[1];
|
|
2285
|
+
if (mimeType) {
|
|
2286
|
+
const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0));
|
|
2287
|
+
const extension = mimeTypeToExtension(mimeType) ?? "png";
|
|
2288
|
+
return new File([ bytes ], `pasted-image-${Date.now()}.${extension}`, { type: mimeType })
|
|
2289
|
+
} else {
|
|
2290
|
+
return null
|
|
2291
|
+
}
|
|
2292
|
+
} catch {
|
|
2293
|
+
return null
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
|
|
1822
2297
|
class HorizontalDividerNode extends DecoratorNode {
|
|
1823
2298
|
static getType() {
|
|
1824
2299
|
return "horizontal_divider"
|
|
@@ -2813,17 +3288,12 @@ class CommandDispatcher {
|
|
|
2813
3288
|
this.editor = editorElement.editor;
|
|
2814
3289
|
this.selection = editorElement.selection;
|
|
2815
3290
|
this.contents = editorElement.contents;
|
|
2816
|
-
this.clipboard = editorElement.clipboard;
|
|
2817
3291
|
|
|
2818
3292
|
this.#registerCommands();
|
|
2819
3293
|
this.#registerKeyboardCommands();
|
|
2820
3294
|
this.#registerDragAndDropHandlers();
|
|
2821
3295
|
}
|
|
2822
3296
|
|
|
2823
|
-
dispatchPaste(event) {
|
|
2824
|
-
return this.clipboard.paste(event)
|
|
2825
|
-
}
|
|
2826
|
-
|
|
2827
3297
|
dispatchBold() {
|
|
2828
3298
|
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
|
|
2829
3299
|
}
|
|
@@ -3050,8 +3520,6 @@ class CommandDispatcher {
|
|
|
3050
3520
|
const methodName = `dispatch${capitalize(command)}`;
|
|
3051
3521
|
this.#registerCommandHandler(command, 0, this[methodName].bind(this));
|
|
3052
3522
|
}
|
|
3053
|
-
|
|
3054
|
-
this.#registerCommandHandler(PASTE_COMMAND, COMMAND_PRIORITY_LOW, this.dispatchPaste.bind(this));
|
|
3055
3523
|
}
|
|
3056
3524
|
|
|
3057
3525
|
#registerCommandHandler(command, priority, handler) {
|
|
@@ -3184,42 +3652,6 @@ function capitalize(str) {
|
|
|
3184
3652
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
3185
3653
|
}
|
|
3186
3654
|
|
|
3187
|
-
function debounce(fn, wait) {
|
|
3188
|
-
let timeout;
|
|
3189
|
-
|
|
3190
|
-
return (...args) => {
|
|
3191
|
-
clearTimeout(timeout);
|
|
3192
|
-
timeout = setTimeout(() => fn(...args), wait);
|
|
3193
|
-
}
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3196
|
-
function debounceAsync(fn, wait) {
|
|
3197
|
-
let timeout;
|
|
3198
|
-
|
|
3199
|
-
return (...args) => {
|
|
3200
|
-
clearTimeout(timeout);
|
|
3201
|
-
|
|
3202
|
-
return new Promise((resolve, reject) => {
|
|
3203
|
-
timeout = setTimeout(async () => {
|
|
3204
|
-
try {
|
|
3205
|
-
const result = await fn(...args);
|
|
3206
|
-
resolve(result);
|
|
3207
|
-
} catch (err) {
|
|
3208
|
-
reject(err);
|
|
3209
|
-
}
|
|
3210
|
-
}, wait);
|
|
3211
|
-
})
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
|
|
3215
|
-
function delay(ms) {
|
|
3216
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
3217
|
-
}
|
|
3218
|
-
|
|
3219
|
-
function nextFrame() {
|
|
3220
|
-
return new Promise(requestAnimationFrame)
|
|
3221
|
-
}
|
|
3222
|
-
|
|
3223
3655
|
class Selection {
|
|
3224
3656
|
#listeners = new ListenerBin()
|
|
3225
3657
|
|
|
@@ -3412,6 +3844,11 @@ class Selection {
|
|
|
3412
3844
|
return $isActionTextAttachmentNode(firstNode) && firstNode.isPreviewableImage
|
|
3413
3845
|
}
|
|
3414
3846
|
|
|
3847
|
+
get isAtNodeStart() {
|
|
3848
|
+
const { anchorNode, offset } = this.#getCollapsedSelectionData();
|
|
3849
|
+
return anchorNode && offset === 0
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3415
3852
|
get nodeAfterCursor() {
|
|
3416
3853
|
const { anchorNode, offset } = this.#getCollapsedSelectionData();
|
|
3417
3854
|
if (!anchorNode) return null
|
|
@@ -4127,577 +4564,574 @@ class EditorConfiguration {
|
|
|
4127
4564
|
}
|
|
4128
4565
|
}
|
|
4129
4566
|
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
});
|
|
4567
|
+
class ImageGalleryNode extends ElementNode {
|
|
4568
|
+
$config() {
|
|
4569
|
+
return this.config("image_gallery", {
|
|
4570
|
+
extends: ElementNode,
|
|
4571
|
+
})
|
|
4572
|
+
}
|
|
4137
4573
|
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4574
|
+
static transform() {
|
|
4575
|
+
return (gallery) => {
|
|
4576
|
+
gallery.unwrapEmptyNode()
|
|
4577
|
+
|| gallery.replaceWithSingularChild()
|
|
4578
|
+
|| gallery.splitAroundInvalidChild();
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4141
4581
|
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4582
|
+
static importDOM() {
|
|
4583
|
+
return {
|
|
4584
|
+
div: (element) => {
|
|
4585
|
+
if (!this.#isGalleryElement(element)) return null
|
|
4145
4586
|
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4587
|
+
return {
|
|
4588
|
+
conversion: () => {
|
|
4589
|
+
return {
|
|
4590
|
+
node: $createImageGalleryNode()
|
|
4591
|
+
}
|
|
4592
|
+
},
|
|
4593
|
+
priority: 2
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4149
4597
|
}
|
|
4150
4598
|
|
|
4151
|
-
static
|
|
4152
|
-
return
|
|
4599
|
+
static canCollapseWith(node) {
|
|
4600
|
+
return $isImageGalleryNode(node) || this.isValidChild(node)
|
|
4153
4601
|
}
|
|
4154
4602
|
|
|
4155
|
-
static
|
|
4156
|
-
return
|
|
4603
|
+
static isValidChild(node) {
|
|
4604
|
+
return $isActionTextAttachmentNode(node) && node.isPreviewableImage
|
|
4157
4605
|
}
|
|
4158
4606
|
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
return
|
|
4607
|
+
static #isGalleryElement(element) {
|
|
4608
|
+
const attachmentChildren = element.querySelectorAll(`:scope > :is(${this.#attachmentTags.join()})`);
|
|
4609
|
+
return element.textContent.trim() === ""
|
|
4610
|
+
&& attachmentChildren.length > 0
|
|
4611
|
+
&& element.children.length === attachmentChildren.length
|
|
4162
4612
|
}
|
|
4163
4613
|
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
super({ ...node, contentType: file?.type ?? contentType }, key);
|
|
4167
|
-
this.file = file ?? null;
|
|
4168
|
-
this.fileName = file?.name ?? fileName;
|
|
4169
|
-
this.uploadUrl = uploadUrl;
|
|
4170
|
-
this.blobUrlTemplate = blobUrlTemplate;
|
|
4171
|
-
this.progress = progress ?? null;
|
|
4172
|
-
this.width = width;
|
|
4173
|
-
this.height = height;
|
|
4174
|
-
this.uploadError = uploadError;
|
|
4614
|
+
static get #attachmentTags() {
|
|
4615
|
+
return Object.keys(ActionTextAttachmentNode.importDOM())
|
|
4175
4616
|
}
|
|
4176
4617
|
|
|
4177
4618
|
createDOM() {
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
// node is reloaded from saved state such as from history.
|
|
4183
|
-
this.#startUploadIfNeeded();
|
|
4184
|
-
|
|
4185
|
-
// Bridge-managed uploads (uploadUrl is null) don't have file data to show
|
|
4186
|
-
// an image preview, so always show the file icon during upload.
|
|
4187
|
-
const canPreviewFile = this.isPreviewableAttachment && this.uploadUrl != null;
|
|
4188
|
-
const figure = this.createAttachmentFigure(canPreviewFile);
|
|
4189
|
-
|
|
4190
|
-
if (canPreviewFile) {
|
|
4191
|
-
const img = figure.appendChild(this.#createDOMForImage());
|
|
4619
|
+
const div = document.createElement("div");
|
|
4620
|
+
div.className = this.#galleryClassNames;
|
|
4621
|
+
return div
|
|
4622
|
+
}
|
|
4192
4623
|
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
}
|
|
4624
|
+
updateDOM(_prevNode, dom) {
|
|
4625
|
+
dom.className = this.#galleryClassNames;
|
|
4626
|
+
return false
|
|
4627
|
+
}
|
|
4198
4628
|
|
|
4199
|
-
|
|
4200
|
-
|
|
4629
|
+
canBeEmpty() {
|
|
4630
|
+
// Return `true` to conform to `$isBlock(node)`
|
|
4631
|
+
// We clean-up empty galleries with a transform
|
|
4632
|
+
return true
|
|
4633
|
+
}
|
|
4201
4634
|
|
|
4202
|
-
|
|
4635
|
+
collapseAtStart(_selection) {
|
|
4636
|
+
return true
|
|
4203
4637
|
}
|
|
4204
4638
|
|
|
4205
|
-
|
|
4206
|
-
|
|
4639
|
+
insertNewAfter(selection, restoreSelection) {
|
|
4640
|
+
const selectionBeforeLastChild = selection.anchor.getNode().is(this) && selection.anchor.offset == this.getChildrenSize() - 1;
|
|
4641
|
+
if (selectionBeforeLastChild) {
|
|
4642
|
+
const paragraph = $createParagraphNode();
|
|
4643
|
+
this.insertAfter(paragraph, false);
|
|
4644
|
+
paragraph.insertAfter(this.getLastChild(), false);
|
|
4645
|
+
paragraph.selectEnd();
|
|
4207
4646
|
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
progress.value = this.progress ?? 0;
|
|
4647
|
+
// return null as selection has been managed
|
|
4648
|
+
return null
|
|
4211
4649
|
}
|
|
4212
4650
|
|
|
4213
|
-
|
|
4651
|
+
const newNode = $createImageGalleryNode();
|
|
4652
|
+
this.insertAfter(newNode, restoreSelection);
|
|
4653
|
+
return newNode
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
getImageAttachments() {
|
|
4657
|
+
const children = this.getChildren();
|
|
4658
|
+
return children.filter($isActionTextAttachmentNode)
|
|
4214
4659
|
}
|
|
4215
4660
|
|
|
4216
4661
|
exportDOM() {
|
|
4217
|
-
|
|
4662
|
+
const div = document.createElement("div");
|
|
4663
|
+
div.className = this.#galleryClassNames;
|
|
4664
|
+
return { element: div }
|
|
4218
4665
|
}
|
|
4219
4666
|
|
|
4220
|
-
|
|
4221
|
-
return
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
uploadUrl: this.uploadUrl,
|
|
4228
|
-
blobUrlTemplate: this.blobUrlTemplate,
|
|
4229
|
-
progress: this.progress,
|
|
4230
|
-
width: this.width,
|
|
4231
|
-
height: this.height,
|
|
4232
|
-
uploadError: this.uploadError
|
|
4667
|
+
collapseWith(node, backwards) {
|
|
4668
|
+
if (!ImageGalleryNode.canCollapseWith(node)) return false
|
|
4669
|
+
|
|
4670
|
+
if (backwards) {
|
|
4671
|
+
$insertFirst(this, node);
|
|
4672
|
+
} else {
|
|
4673
|
+
this.append(node);
|
|
4233
4674
|
}
|
|
4234
|
-
}
|
|
4235
4675
|
|
|
4236
|
-
|
|
4237
|
-
return this.progress !== null
|
|
4238
|
-
}
|
|
4676
|
+
$unwrapAndFilterDescendants(this, ImageGalleryNode.isValidChild);
|
|
4239
4677
|
|
|
4240
|
-
|
|
4241
|
-
return createElement("img")
|
|
4678
|
+
return true
|
|
4242
4679
|
}
|
|
4243
4680
|
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4681
|
+
unwrapEmptyNode() {
|
|
4682
|
+
if (this.isEmpty()) {
|
|
4683
|
+
const paragraph = $createParagraphNode();
|
|
4684
|
+
return this.replace(paragraph)
|
|
4685
|
+
}
|
|
4248
4686
|
}
|
|
4249
4687
|
|
|
4250
|
-
|
|
4251
|
-
|
|
4688
|
+
replaceWithSingularChild() {
|
|
4689
|
+
if (this.#hasSingularChild) {
|
|
4690
|
+
const child = this.getFirstChild();
|
|
4691
|
+
return this.replace(child)
|
|
4692
|
+
}
|
|
4252
4693
|
}
|
|
4253
4694
|
|
|
4254
|
-
|
|
4255
|
-
const
|
|
4695
|
+
splitAroundInvalidChild() {
|
|
4696
|
+
for (const child of $firstToLastIterator(this)) {
|
|
4697
|
+
if (ImageGalleryNode.isValidChild(child)) continue
|
|
4256
4698
|
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4699
|
+
const poppedNode = $makeSafeForRoot(child);
|
|
4700
|
+
const [ topGallery, secondGallery ] = this.splitAtIndex(poppedNode.getIndexWithinParent());
|
|
4701
|
+
topGallery.insertAfter(poppedNode);
|
|
4702
|
+
poppedNode.selectEnd();
|
|
4261
4703
|
|
|
4262
|
-
|
|
4263
|
-
|
|
4704
|
+
// remove an empty gallery rather than let it unwrap to a paragraph
|
|
4705
|
+
if (secondGallery.isEmpty()) secondGallery.remove();
|
|
4264
4706
|
|
|
4265
|
-
|
|
4266
|
-
|
|
4707
|
+
break
|
|
4708
|
+
}
|
|
4267
4709
|
}
|
|
4268
4710
|
|
|
4269
|
-
|
|
4270
|
-
|
|
4711
|
+
splitAtIndex(index) {
|
|
4712
|
+
return $splitNode(this, index)
|
|
4713
|
+
}
|
|
4271
4714
|
|
|
4272
|
-
|
|
4715
|
+
get #hasSingularChild() {
|
|
4716
|
+
return this.getChildrenSize() === 1
|
|
4273
4717
|
}
|
|
4274
4718
|
|
|
4275
|
-
get #
|
|
4276
|
-
return
|
|
4719
|
+
get #galleryClassNames() {
|
|
4720
|
+
return `attachment-gallery attachment-gallery--${this.getChildrenSize()}`
|
|
4277
4721
|
}
|
|
4722
|
+
}
|
|
4278
4723
|
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4724
|
+
function $createImageGalleryNode() {
|
|
4725
|
+
return new ImageGalleryNode()
|
|
4726
|
+
}
|
|
4282
4727
|
|
|
4283
|
-
|
|
4728
|
+
function $isImageGalleryNode(node) {
|
|
4729
|
+
return node instanceof ImageGalleryNode
|
|
4730
|
+
}
|
|
4284
4731
|
|
|
4285
|
-
|
|
4732
|
+
function $findOrCreateGalleryForImage(node) {
|
|
4733
|
+
if (!ImageGalleryNode.canCollapseWith(node)) return null
|
|
4286
4734
|
|
|
4287
|
-
|
|
4288
|
-
|
|
4735
|
+
const existingGallery = $getNearestNodeOfType(node, ImageGalleryNode);
|
|
4736
|
+
return existingGallery ?? $wrapNodeInElement(node, $createImageGalleryNode)
|
|
4737
|
+
}
|
|
4289
4738
|
|
|
4290
|
-
|
|
4739
|
+
class Uploader {
|
|
4740
|
+
#files
|
|
4291
4741
|
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
this.#handleUploadError(error);
|
|
4296
|
-
} else {
|
|
4297
|
-
this.#dispatchEvent("lexxy:upload-end", { file: this.file, error: null });
|
|
4298
|
-
this.editor.update(() => {
|
|
4299
|
-
this.$showUploadedAttachment(blob);
|
|
4300
|
-
});
|
|
4301
|
-
}
|
|
4302
|
-
});
|
|
4742
|
+
static for(editorElement, files) {
|
|
4743
|
+
const UploaderKlass = GalleryUploader.handle(editorElement, files) ? GalleryUploader : Uploader;
|
|
4744
|
+
return new UploaderKlass(editorElement, files)
|
|
4303
4745
|
}
|
|
4304
4746
|
|
|
4305
|
-
|
|
4306
|
-
|
|
4747
|
+
constructor(editorElement, files, options = {}) {
|
|
4748
|
+
this.#files = files;
|
|
4749
|
+
this.options = options;
|
|
4307
4750
|
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
directUploadWillStoreFileWithXHR: (request) => {
|
|
4313
|
-
if (shouldAuthenticateUploads) request.withCredentials = true;
|
|
4751
|
+
this.editorElement = editorElement;
|
|
4752
|
+
this.contents = editorElement.contents;
|
|
4753
|
+
this.selection = editorElement.selection;
|
|
4754
|
+
}
|
|
4314
4755
|
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
}
|
|
4318
|
-
}
|
|
4756
|
+
get files() {
|
|
4757
|
+
return Array.from(this.#files)
|
|
4319
4758
|
}
|
|
4320
4759
|
|
|
4321
|
-
|
|
4322
|
-
this
|
|
4760
|
+
$uploadFiles() {
|
|
4761
|
+
this.$createUploadNodes();
|
|
4762
|
+
this.$insertUploadNodes();
|
|
4323
4763
|
}
|
|
4324
4764
|
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4765
|
+
$createUploadNodes() {
|
|
4766
|
+
this.nodes = this.files.map(file => this.contents.$createUploadNode(file));
|
|
4767
|
+
}
|
|
4768
|
+
|
|
4769
|
+
$insertUploadNodes() {
|
|
4770
|
+
this.contents.insertAtCursor(...this.nodes);
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
class GalleryUploader extends Uploader {
|
|
4775
|
+
#gallery
|
|
4776
|
+
|
|
4777
|
+
static handle(editorElement, files) {
|
|
4778
|
+
return this.isMultipleImageUpload(files) || this.gallerySelection(editorElement.selection)
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
static isMultipleImageUpload(files) {
|
|
4782
|
+
let imageFileCount = 0;
|
|
4783
|
+
for (const file of files) {
|
|
4784
|
+
if (isPreviewableImage(file.type)) imageFileCount++;
|
|
4785
|
+
if (imageFileCount > 1) return true
|
|
4332
4786
|
}
|
|
4787
|
+
return false
|
|
4333
4788
|
}
|
|
4334
4789
|
|
|
4335
|
-
|
|
4336
|
-
this.
|
|
4790
|
+
static gallerySelection(selection) {
|
|
4791
|
+
return selection.isOnPreviewableImage || this.selectionIsAfterGalleryEdge(selection)
|
|
4337
4792
|
}
|
|
4338
4793
|
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
this.patchAndRewriteHistory({ uploadError: true });
|
|
4794
|
+
static selectionIsAfterGalleryEdge(selection) {
|
|
4795
|
+
return selection.isAtNodeStart && ImageGalleryNode.canCollapseWith(selection.nodeBeforeCursor)
|
|
4343
4796
|
}
|
|
4344
4797
|
|
|
4345
|
-
$
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4798
|
+
$insertUploadNodes() {
|
|
4799
|
+
this.#findOrCreateGallery();
|
|
4800
|
+
this.#insertImagesInGallery();
|
|
4801
|
+
this.#insertNonImagesAfterGallery();
|
|
4802
|
+
}
|
|
4350
4803
|
|
|
4351
|
-
|
|
4804
|
+
#findOrCreateGallery() {
|
|
4805
|
+
if (this.selection.isOnPreviewableImage) {
|
|
4806
|
+
this.#gallery = $findOrCreateGalleryForImage(this.#selectedNode);
|
|
4807
|
+
} else if (this.#selectionIsAfterGalleryEdge) {
|
|
4808
|
+
this.#gallery = $findOrCreateGalleryForImage(this.selection.nodeBeforeCursor);
|
|
4809
|
+
} else {
|
|
4810
|
+
this.#gallery = $createImageGalleryNode();
|
|
4811
|
+
this.contents.insertAtCursor(this.#gallery);
|
|
4812
|
+
}
|
|
4352
4813
|
}
|
|
4353
4814
|
|
|
4354
|
-
#
|
|
4355
|
-
|
|
4356
|
-
return conversion.toAttachmentNode()
|
|
4815
|
+
get #selectionIsAfterGalleryEdge() {
|
|
4816
|
+
return this.constructor.selectionIsAfterGalleryEdge(this.selection)
|
|
4357
4817
|
}
|
|
4358
4818
|
|
|
4359
|
-
#
|
|
4360
|
-
const
|
|
4361
|
-
|
|
4819
|
+
get #selectedNode() {
|
|
4820
|
+
const { node } = this.selection.selectedNodeWithOffset();
|
|
4821
|
+
return node
|
|
4362
4822
|
}
|
|
4363
|
-
}
|
|
4364
4823
|
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4824
|
+
get #galleryInsertPosition() {
|
|
4825
|
+
if (this.#selectionIsAfterGalleryEdge) return this.#gallery.getChildrenSize()
|
|
4826
|
+
|
|
4827
|
+
const anchor = $getSelection()?.anchor;
|
|
4828
|
+
const galleryHasElementSelection = anchor?.getNode().is(this.#gallery);
|
|
4829
|
+
if (galleryHasElementSelection) return anchor.offset
|
|
4830
|
+
|
|
4831
|
+
const selectedNode = this.#selectedNode;
|
|
4832
|
+
const childIndex = this.#gallery.isParentOf(selectedNode) && selectedNode.getIndexWithinParent();
|
|
4833
|
+
return childIndex !== false ? (childIndex + 1) : 0
|
|
4370
4834
|
}
|
|
4371
4835
|
|
|
4372
|
-
|
|
4373
|
-
return
|
|
4374
|
-
...this.uploadNode,
|
|
4375
|
-
...this.#propertiesFromBlob,
|
|
4376
|
-
src: this.#src,
|
|
4377
|
-
previewSrc: this.previewSrc,
|
|
4378
|
-
pendingPreview: this.blob.previewable && !this.uploadNode.isPreviewableImage
|
|
4379
|
-
})
|
|
4836
|
+
get #imageNodes() {
|
|
4837
|
+
return this.nodes.filter(node => ImageGalleryNode.isValidChild(node))
|
|
4380
4838
|
}
|
|
4381
4839
|
|
|
4382
|
-
get #
|
|
4383
|
-
|
|
4384
|
-
return {
|
|
4385
|
-
sgid: blob.attachable_sgid,
|
|
4386
|
-
altText: blob.filename,
|
|
4387
|
-
contentType: blob.content_type,
|
|
4388
|
-
fileName: blob.filename,
|
|
4389
|
-
fileSize: blob.byte_size,
|
|
4390
|
-
previewable: blob.previewable,
|
|
4391
|
-
}
|
|
4840
|
+
get #nonImageNodes() {
|
|
4841
|
+
return this.nodes.filter(node => !ImageGalleryNode.isValidChild(node))
|
|
4392
4842
|
}
|
|
4393
4843
|
|
|
4394
|
-
|
|
4395
|
-
|
|
4844
|
+
#insertImagesInGallery() {
|
|
4845
|
+
this.#gallery.splice(this.#galleryInsertPosition, 0, this.#imageNodes);
|
|
4396
4846
|
}
|
|
4397
4847
|
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4848
|
+
#insertNonImagesAfterGallery() {
|
|
4849
|
+
let beforeNode = this.#gallery;
|
|
4850
|
+
|
|
4851
|
+
for (const node of this.#nonImageNodes) {
|
|
4852
|
+
beforeNode.insertAfter(node);
|
|
4853
|
+
beforeNode = node;
|
|
4854
|
+
}
|
|
4402
4855
|
}
|
|
4403
4856
|
}
|
|
4404
4857
|
|
|
4405
|
-
function
|
|
4406
|
-
return new
|
|
4407
|
-
|
|
4858
|
+
async function loadFileIntoImage(file, image) {
|
|
4859
|
+
return new Promise((resolve) => {
|
|
4860
|
+
const reader = new FileReader();
|
|
4408
4861
|
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
extends: ElementNode,
|
|
4413
|
-
})
|
|
4414
|
-
}
|
|
4862
|
+
image.addEventListener("load", () => {
|
|
4863
|
+
resolve(image);
|
|
4864
|
+
});
|
|
4415
4865
|
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|| gallery.replaceWithSingularChild()
|
|
4420
|
-
|| gallery.splitAroundInvalidChild();
|
|
4421
|
-
}
|
|
4422
|
-
}
|
|
4866
|
+
reader.onload = (event) => {
|
|
4867
|
+
image.src = event.target.result || null;
|
|
4868
|
+
};
|
|
4423
4869
|
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
if (!this.#isGalleryElement(element)) return null
|
|
4870
|
+
reader.readAsDataURL(file);
|
|
4871
|
+
})
|
|
4872
|
+
}
|
|
4428
4873
|
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
node: $createImageGalleryNode()
|
|
4433
|
-
}
|
|
4434
|
-
},
|
|
4435
|
-
priority: 2
|
|
4436
|
-
}
|
|
4437
|
-
}
|
|
4438
|
-
}
|
|
4874
|
+
class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
4875
|
+
static getType() {
|
|
4876
|
+
return "action_text_attachment_upload"
|
|
4439
4877
|
}
|
|
4440
4878
|
|
|
4441
|
-
static
|
|
4442
|
-
return
|
|
4879
|
+
static clone(node) {
|
|
4880
|
+
return new ActionTextAttachmentUploadNode({ ...node }, node.__key)
|
|
4443
4881
|
}
|
|
4444
4882
|
|
|
4445
|
-
static
|
|
4446
|
-
return
|
|
4883
|
+
static importJSON(serializedNode) {
|
|
4884
|
+
return new ActionTextAttachmentUploadNode({ ...serializedNode })
|
|
4447
4885
|
}
|
|
4448
4886
|
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
return
|
|
4452
|
-
&& attachmentChildren.length > 0
|
|
4453
|
-
&& element.children.length === attachmentChildren.length
|
|
4887
|
+
// Should never run since this is a transient node. Defined to remove console warning.
|
|
4888
|
+
static importDOM() {
|
|
4889
|
+
return null
|
|
4454
4890
|
}
|
|
4455
4891
|
|
|
4456
|
-
|
|
4457
|
-
|
|
4892
|
+
constructor(node, key) {
|
|
4893
|
+
const { file, uploadUrl, blobUrlTemplate, progress, width, height, uploadError, fileName, contentType } = node;
|
|
4894
|
+
super({ ...node, contentType: file?.type ?? contentType }, key);
|
|
4895
|
+
this.file = file ?? null;
|
|
4896
|
+
this.fileName = file?.name ?? fileName;
|
|
4897
|
+
this.uploadUrl = uploadUrl;
|
|
4898
|
+
this.blobUrlTemplate = blobUrlTemplate;
|
|
4899
|
+
this.progress = progress ?? null;
|
|
4900
|
+
this.width = width;
|
|
4901
|
+
this.height = height;
|
|
4902
|
+
this.uploadError = uploadError;
|
|
4458
4903
|
}
|
|
4459
4904
|
|
|
4460
4905
|
createDOM() {
|
|
4461
|
-
|
|
4462
|
-
div.className = this.#galleryClassNames;
|
|
4463
|
-
return div
|
|
4464
|
-
}
|
|
4465
|
-
|
|
4466
|
-
updateDOM(_prevNode, dom) {
|
|
4467
|
-
dom.className = this.#galleryClassNames;
|
|
4468
|
-
return false
|
|
4469
|
-
}
|
|
4906
|
+
if (this.uploadError) return this.createDOMForError()
|
|
4470
4907
|
|
|
4471
|
-
|
|
4472
|
-
//
|
|
4473
|
-
//
|
|
4474
|
-
|
|
4475
|
-
}
|
|
4908
|
+
// This side-effect is trigged on DOM load to fire only once and avoid multiple
|
|
4909
|
+
// uploads through cloning. The upload is guarded from restarting in case the
|
|
4910
|
+
// node is reloaded from saved state such as from history.
|
|
4911
|
+
this.#startUploadIfNeeded();
|
|
4476
4912
|
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4913
|
+
// Bridge-managed uploads (uploadUrl is null) don't have file data to show
|
|
4914
|
+
// an image preview, so always show the file icon during upload.
|
|
4915
|
+
const canPreviewFile = this.isPreviewableAttachment && this.uploadUrl != null;
|
|
4916
|
+
const figure = this.createAttachmentFigure(canPreviewFile);
|
|
4480
4917
|
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
if (selectionBeforeLastChild) {
|
|
4484
|
-
const paragraph = $createParagraphNode();
|
|
4485
|
-
this.insertAfter(paragraph, false);
|
|
4486
|
-
paragraph.insertAfter(this.getLastChild(), false);
|
|
4487
|
-
paragraph.selectEnd();
|
|
4918
|
+
if (canPreviewFile) {
|
|
4919
|
+
const img = figure.appendChild(this.#createDOMForImage());
|
|
4488
4920
|
|
|
4489
|
-
//
|
|
4490
|
-
|
|
4921
|
+
// load file locally to set dimensions and prevent vertical shifting
|
|
4922
|
+
loadFileIntoImage(this.file, img).then(img => this.#setDimensionsFromImage(img));
|
|
4923
|
+
} else {
|
|
4924
|
+
figure.appendChild(this.#createDOMForFile());
|
|
4491
4925
|
}
|
|
4492
4926
|
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
return newNode
|
|
4496
|
-
}
|
|
4497
|
-
|
|
4498
|
-
getImageAttachments() {
|
|
4499
|
-
const children = this.getChildren();
|
|
4500
|
-
return children.filter($isActionTextAttachmentNode)
|
|
4501
|
-
}
|
|
4927
|
+
figure.appendChild(this.#createCaption());
|
|
4928
|
+
figure.appendChild(this.#createProgressBar());
|
|
4502
4929
|
|
|
4503
|
-
|
|
4504
|
-
const div = document.createElement("div");
|
|
4505
|
-
div.className = this.#galleryClassNames;
|
|
4506
|
-
return { element: div }
|
|
4930
|
+
return figure
|
|
4507
4931
|
}
|
|
4508
4932
|
|
|
4509
|
-
|
|
4510
|
-
if (
|
|
4933
|
+
updateDOM(prevNode, dom) {
|
|
4934
|
+
if (this.uploadError !== prevNode.uploadError) return true
|
|
4511
4935
|
|
|
4512
|
-
if (
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
this.append(node);
|
|
4936
|
+
if (prevNode.progress !== this.progress) {
|
|
4937
|
+
const progress = dom.querySelector("progress");
|
|
4938
|
+
progress.value = this.progress ?? 0;
|
|
4516
4939
|
}
|
|
4517
4940
|
|
|
4518
|
-
|
|
4941
|
+
return false
|
|
4942
|
+
}
|
|
4519
4943
|
|
|
4520
|
-
|
|
4944
|
+
exportDOM() {
|
|
4945
|
+
return { element: null }
|
|
4521
4946
|
}
|
|
4522
4947
|
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4948
|
+
exportJSON() {
|
|
4949
|
+
return {
|
|
4950
|
+
...super.exportJSON(),
|
|
4951
|
+
type: "action_text_attachment_upload",
|
|
4952
|
+
version: 1,
|
|
4953
|
+
fileName: this.fileName,
|
|
4954
|
+
contentType: this.contentType,
|
|
4955
|
+
uploadUrl: this.uploadUrl,
|
|
4956
|
+
blobUrlTemplate: this.blobUrlTemplate,
|
|
4957
|
+
progress: this.progress,
|
|
4958
|
+
width: this.width,
|
|
4959
|
+
height: this.height,
|
|
4960
|
+
uploadError: this.uploadError
|
|
4527
4961
|
}
|
|
4528
4962
|
}
|
|
4529
4963
|
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4964
|
+
get #uploadStarted() {
|
|
4965
|
+
return this.progress !== null
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
#createDOMForImage() {
|
|
4969
|
+
return createElement("img")
|
|
4535
4970
|
}
|
|
4536
4971
|
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4972
|
+
#createDOMForFile() {
|
|
4973
|
+
const extension = this.#getFileExtension();
|
|
4974
|
+
const span = createElement("span", { className: "attachment__icon", textContent: extension });
|
|
4975
|
+
return span
|
|
4976
|
+
}
|
|
4540
4977
|
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
poppedNode.selectEnd();
|
|
4978
|
+
#getFileExtension() {
|
|
4979
|
+
return (this.fileName || "").split(".").pop().toLowerCase()
|
|
4980
|
+
}
|
|
4545
4981
|
|
|
4546
|
-
|
|
4547
|
-
|
|
4982
|
+
#createCaption() {
|
|
4983
|
+
const figcaption = createElement("figcaption", { className: "attachment__caption" });
|
|
4548
4984
|
|
|
4549
|
-
|
|
4550
|
-
}
|
|
4985
|
+
const nameSpan = createElement("span", { className: "attachment__name", textContent: this.caption || this.fileName || "" });
|
|
4986
|
+
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.file?.size) });
|
|
4987
|
+
figcaption.appendChild(nameSpan);
|
|
4988
|
+
figcaption.appendChild(sizeSpan);
|
|
4989
|
+
|
|
4990
|
+
return figcaption
|
|
4551
4991
|
}
|
|
4552
4992
|
|
|
4553
|
-
|
|
4554
|
-
return
|
|
4993
|
+
#createProgressBar() {
|
|
4994
|
+
return createElement("progress", { value: this.progress ?? 0, max: 100 })
|
|
4555
4995
|
}
|
|
4556
4996
|
|
|
4557
|
-
|
|
4558
|
-
|
|
4997
|
+
#setDimensionsFromImage({ width, height }) {
|
|
4998
|
+
if (this.#hasDimensions) return
|
|
4999
|
+
|
|
5000
|
+
this.patchAndRewriteHistory({ width, height });
|
|
4559
5001
|
}
|
|
4560
5002
|
|
|
4561
|
-
get #
|
|
4562
|
-
return
|
|
5003
|
+
get #hasDimensions() {
|
|
5004
|
+
return Boolean(this.width && this.height)
|
|
4563
5005
|
}
|
|
4564
|
-
}
|
|
4565
5006
|
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
5007
|
+
async #startUploadIfNeeded() {
|
|
5008
|
+
if (this.#uploadStarted) return
|
|
5009
|
+
if (!this.uploadUrl) return // Bridge-managed upload — skip DirectUpload
|
|
4569
5010
|
|
|
4570
|
-
|
|
4571
|
-
return node instanceof ImageGalleryNode
|
|
4572
|
-
}
|
|
5011
|
+
this.#setUploadStarted();
|
|
4573
5012
|
|
|
4574
|
-
|
|
4575
|
-
if (!ImageGalleryNode.canCollapseWith(node)) return null
|
|
5013
|
+
const { DirectUpload } = await import('@rails/activestorage');
|
|
4576
5014
|
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
}
|
|
5015
|
+
const upload = new DirectUpload(this.file, this.uploadUrl, this);
|
|
5016
|
+
upload.delegate = this.#createUploadDelegate();
|
|
4580
5017
|
|
|
4581
|
-
|
|
4582
|
-
#files
|
|
5018
|
+
this.#dispatchEvent("lexxy:upload-start", { file: this.file });
|
|
4583
5019
|
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
5020
|
+
upload.create((error, blob) => {
|
|
5021
|
+
if (error) {
|
|
5022
|
+
this.#dispatchEvent("lexxy:upload-end", { file: this.file, error });
|
|
5023
|
+
this.#handleUploadError(error);
|
|
5024
|
+
} else {
|
|
5025
|
+
this.#dispatchEvent("lexxy:upload-end", { file: this.file, error: null });
|
|
5026
|
+
this.editor.update(() => {
|
|
5027
|
+
this.$showUploadedAttachment(blob);
|
|
5028
|
+
});
|
|
5029
|
+
}
|
|
5030
|
+
});
|
|
4587
5031
|
}
|
|
4588
5032
|
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
this.editorElement = editorElement;
|
|
4593
|
-
this.contents = editorElement.contents;
|
|
4594
|
-
this.selection = editorElement.selection;
|
|
4595
|
-
}
|
|
5033
|
+
#createUploadDelegate() {
|
|
5034
|
+
const shouldAuthenticateUploads = Lexxy.global.get("authenticatedUploads");
|
|
4596
5035
|
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
5036
|
+
return {
|
|
5037
|
+
directUploadWillCreateBlobWithXHR: (request) => {
|
|
5038
|
+
if (shouldAuthenticateUploads) request.withCredentials = true;
|
|
5039
|
+
},
|
|
5040
|
+
directUploadWillStoreFileWithXHR: (request) => {
|
|
5041
|
+
if (shouldAuthenticateUploads) request.withCredentials = true;
|
|
4600
5042
|
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
5043
|
+
const uploadProgressHandler = (event) => this.#handleUploadProgress(event, request);
|
|
5044
|
+
request.upload.addEventListener("progress", uploadProgressHandler);
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
4604
5047
|
}
|
|
4605
5048
|
|
|
4606
|
-
|
|
4607
|
-
this
|
|
4608
|
-
$createActionTextAttachmentUploadNode({
|
|
4609
|
-
...this.#nodeUrlProperties,
|
|
4610
|
-
file: file,
|
|
4611
|
-
contentType: file.type
|
|
4612
|
-
})
|
|
4613
|
-
);
|
|
5049
|
+
#setUploadStarted() {
|
|
5050
|
+
this.#setProgress(1);
|
|
4614
5051
|
}
|
|
4615
5052
|
|
|
4616
|
-
|
|
4617
|
-
|
|
5053
|
+
#handleUploadProgress(event, request) {
|
|
5054
|
+
const progress = Math.round(event.loaded / event.total * 100);
|
|
5055
|
+
try {
|
|
5056
|
+
this.#setProgress(progress);
|
|
5057
|
+
this.#dispatchEvent("lexxy:upload-progress", { file: this.file, progress });
|
|
5058
|
+
} catch {
|
|
5059
|
+
request.abort();
|
|
5060
|
+
}
|
|
4618
5061
|
}
|
|
4619
5062
|
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
uploadUrl: this.editorElement.directUploadUrl,
|
|
4623
|
-
blobUrlTemplate: this.editorElement.blobUrlTemplate
|
|
4624
|
-
}
|
|
5063
|
+
#setProgress(progress) {
|
|
5064
|
+
this.patchAndRewriteHistory({ progress });
|
|
4625
5065
|
}
|
|
4626
|
-
}
|
|
4627
5066
|
|
|
4628
|
-
|
|
4629
|
-
|
|
5067
|
+
#handleUploadError(error) {
|
|
5068
|
+
console.warn(`Upload error for ${this.file?.name ?? "file"}: ${error}`);
|
|
4630
5069
|
|
|
4631
|
-
|
|
4632
|
-
return this.#isMultipleImageUpload(files) || this.#gallerySelection(editorElement.selection)
|
|
5070
|
+
this.patchAndRewriteHistory({ uploadError: true });
|
|
4633
5071
|
}
|
|
4634
5072
|
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
for (const file of files) {
|
|
4638
|
-
if (isPreviewableImage(file.type)) imageFileCount++;
|
|
4639
|
-
if (imageFileCount > 1) return true
|
|
4640
|
-
}
|
|
4641
|
-
return false
|
|
4642
|
-
}
|
|
5073
|
+
$showUploadedAttachment(blob) {
|
|
5074
|
+
const previewSrc = this.isPreviewableImage && this.file ? URL.createObjectURL(this.file) : null;
|
|
4643
5075
|
|
|
4644
|
-
|
|
4645
|
-
|
|
5076
|
+
const replacementNode = this.#toActionTextAttachmentNodeWith(blob, previewSrc);
|
|
5077
|
+
this.replaceAndRewriteHistory(replacementNode);
|
|
4646
5078
|
|
|
4647
|
-
|
|
4648
|
-
return $getNearestNodeOfType(selectedNode, ImageGalleryNode) !== null
|
|
5079
|
+
return replacementNode.getKey()
|
|
4649
5080
|
}
|
|
4650
5081
|
|
|
4651
|
-
|
|
4652
|
-
this
|
|
4653
|
-
|
|
4654
|
-
this.#insertNonImagesAfterGallery();
|
|
5082
|
+
#toActionTextAttachmentNodeWith(blob, previewSrc) {
|
|
5083
|
+
const conversion = new AttachmentNodeConversion(this, blob, previewSrc);
|
|
5084
|
+
return conversion.toAttachmentNode()
|
|
4655
5085
|
}
|
|
4656
5086
|
|
|
4657
|
-
#
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
} else {
|
|
4661
|
-
this.#gallery = $createImageGalleryNode();
|
|
4662
|
-
this.contents.insertAtCursor(this.#gallery);
|
|
4663
|
-
}
|
|
5087
|
+
#dispatchEvent(name, detail) {
|
|
5088
|
+
const figure = this.editor.getElementByKey(this.getKey());
|
|
5089
|
+
if (figure) dispatch(figure, name, detail);
|
|
4664
5090
|
}
|
|
5091
|
+
}
|
|
4665
5092
|
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
5093
|
+
class AttachmentNodeConversion {
|
|
5094
|
+
constructor(uploadNode, blob, previewSrc) {
|
|
5095
|
+
this.uploadNode = uploadNode;
|
|
5096
|
+
this.blob = blob;
|
|
5097
|
+
this.previewSrc = previewSrc;
|
|
4669
5098
|
}
|
|
4670
5099
|
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
5100
|
+
toAttachmentNode() {
|
|
5101
|
+
return new ActionTextAttachmentNode({
|
|
5102
|
+
...this.uploadNode,
|
|
5103
|
+
...this.#propertiesFromBlob,
|
|
5104
|
+
src: this.#src,
|
|
5105
|
+
previewSrc: this.previewSrc,
|
|
5106
|
+
pendingPreview: this.blob.previewable && !this.uploadNode.isPreviewableImage
|
|
5107
|
+
})
|
|
4679
5108
|
}
|
|
4680
5109
|
|
|
4681
|
-
get #
|
|
4682
|
-
|
|
5110
|
+
get #propertiesFromBlob() {
|
|
5111
|
+
const { blob } = this;
|
|
5112
|
+
return {
|
|
5113
|
+
sgid: blob.attachable_sgid,
|
|
5114
|
+
altText: blob.filename,
|
|
5115
|
+
contentType: blob.content_type,
|
|
5116
|
+
fileName: blob.filename,
|
|
5117
|
+
fileSize: blob.byte_size,
|
|
5118
|
+
previewable: blob.previewable,
|
|
5119
|
+
}
|
|
4683
5120
|
}
|
|
4684
5121
|
|
|
4685
|
-
get #
|
|
4686
|
-
return this.
|
|
5122
|
+
get #src() {
|
|
5123
|
+
return this.blob.previewable ? this.blob.url : this.#blobSrc
|
|
4687
5124
|
}
|
|
4688
5125
|
|
|
4689
|
-
#
|
|
4690
|
-
this
|
|
5126
|
+
get #blobSrc() {
|
|
5127
|
+
return this.uploadNode.blobUrlTemplate
|
|
5128
|
+
.replace(":signed_id", this.blob.signed_id)
|
|
5129
|
+
.replace(":filename", encodeURIComponent(this.blob.filename))
|
|
4691
5130
|
}
|
|
5131
|
+
}
|
|
4692
5132
|
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
for (const node of this.#nonImageNodes) {
|
|
4697
|
-
beforeNode.insertAfter(node);
|
|
4698
|
-
beforeNode = node;
|
|
4699
|
-
}
|
|
4700
|
-
}
|
|
5133
|
+
function $createActionTextAttachmentUploadNode(...args) {
|
|
5134
|
+
return new ActionTextAttachmentUploadNode(...args)
|
|
4701
5135
|
}
|
|
4702
5136
|
|
|
4703
5137
|
class NodeInserter {
|
|
@@ -4806,7 +5240,7 @@ class Contents {
|
|
|
4806
5240
|
this.editor.update(() => {
|
|
4807
5241
|
if ($hasUpdateTag(PASTE_TAG)) this.#stripTableCellColorStyles(doc);
|
|
4808
5242
|
|
|
4809
|
-
const nodes =
|
|
5243
|
+
const nodes = this.editorElement.$generateNodesFromDOM(doc);
|
|
4810
5244
|
if (!this.#insertUploadNodes(nodes)) {
|
|
4811
5245
|
this.insertAtCursor(...nodes);
|
|
4812
5246
|
}
|
|
@@ -5015,7 +5449,7 @@ class Contents {
|
|
|
5015
5449
|
console.warn("This editor does not supports attachments (it's configured with [attachments=false])");
|
|
5016
5450
|
return
|
|
5017
5451
|
}
|
|
5018
|
-
const validFiles = Array.from(files).filter(this
|
|
5452
|
+
const validFiles = Array.from(files).filter(file => this.editorElement.acceptsFile(file));
|
|
5019
5453
|
|
|
5020
5454
|
this.editor.update(() => {
|
|
5021
5455
|
const uploader = Uploader.for(this.editorElement, validFiles);
|
|
@@ -5029,6 +5463,15 @@ class Contents {
|
|
|
5029
5463
|
});
|
|
5030
5464
|
}
|
|
5031
5465
|
|
|
5466
|
+
$createUploadNode(file) {
|
|
5467
|
+
return $createActionTextAttachmentUploadNode({
|
|
5468
|
+
file,
|
|
5469
|
+
uploadUrl: this.editorElement.directUploadUrl,
|
|
5470
|
+
blobUrlTemplate: this.editorElement.blobUrlTemplate,
|
|
5471
|
+
contentType: file.type,
|
|
5472
|
+
})
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5032
5475
|
insertPendingAttachment(file) {
|
|
5033
5476
|
if (!this.editorElement.supportsAttachments) return null
|
|
5034
5477
|
|
|
@@ -5367,14 +5810,10 @@ class Contents {
|
|
|
5367
5810
|
}
|
|
5368
5811
|
|
|
5369
5812
|
#createHtmlNodeWith(html) {
|
|
5370
|
-
const htmlNodes =
|
|
5813
|
+
const htmlNodes = this.editorElement.$generateNodesFromDOM(parseHtml(html));
|
|
5371
5814
|
return htmlNodes[0] || $createParagraphNode()
|
|
5372
5815
|
}
|
|
5373
5816
|
|
|
5374
|
-
#shouldUploadFile(file) {
|
|
5375
|
-
return dispatch(this.editorElement, "lexxy:file-accept", { file }, true)
|
|
5376
|
-
}
|
|
5377
|
-
|
|
5378
5817
|
// When the selection anchor is on a shadow root (e.g. a table cell), Lexical's
|
|
5379
5818
|
// insertNodes can't find a block parent and fails silently. Normalize the
|
|
5380
5819
|
// selection to point inside the shadow root's content instead.
|
|
@@ -5396,10 +5835,22 @@ class Contents {
|
|
|
5396
5835
|
}
|
|
5397
5836
|
|
|
5398
5837
|
class Clipboard {
|
|
5838
|
+
#listeners = new ListenerBin()
|
|
5839
|
+
|
|
5399
5840
|
constructor(editorElement) {
|
|
5400
5841
|
this.editorElement = editorElement;
|
|
5401
5842
|
this.editor = editorElement.editor;
|
|
5402
5843
|
this.contents = editorElement.contents;
|
|
5844
|
+
|
|
5845
|
+
this.#registerPasteCommands();
|
|
5846
|
+
}
|
|
5847
|
+
|
|
5848
|
+
dispose() {
|
|
5849
|
+
this.editorElement = null;
|
|
5850
|
+
this.editor = null;
|
|
5851
|
+
this.contents = null;
|
|
5852
|
+
|
|
5853
|
+
this.#listeners.dispose();
|
|
5403
5854
|
}
|
|
5404
5855
|
|
|
5405
5856
|
paste(event) {
|
|
@@ -5424,6 +5875,25 @@ class Clipboard {
|
|
|
5424
5875
|
return handled
|
|
5425
5876
|
}
|
|
5426
5877
|
|
|
5878
|
+
#registerPasteCommands() {
|
|
5879
|
+
this.#listeners.track(
|
|
5880
|
+
this.editor.registerCommand(PASTE_COMMAND, this.paste.bind(this), COMMAND_PRIORITY_NORMAL),
|
|
5881
|
+
this.editor.registerCommand(
|
|
5882
|
+
SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,
|
|
5883
|
+
(payload) => this.#handleParsedClipboardNodes(payload),
|
|
5884
|
+
COMMAND_PRIORITY_NORMAL
|
|
5885
|
+
)
|
|
5886
|
+
);
|
|
5887
|
+
}
|
|
5888
|
+
|
|
5889
|
+
#handleParsedClipboardNodes({ nodes, selection }) {
|
|
5890
|
+
const url = $bareUrlFromSingleLink(nodes);
|
|
5891
|
+
if (!url) return false
|
|
5892
|
+
|
|
5893
|
+
this.#insertSingleLinkAt(selection, url);
|
|
5894
|
+
return true
|
|
5895
|
+
}
|
|
5896
|
+
|
|
5427
5897
|
#isPlainTextOrURLPasted(clipboardData) {
|
|
5428
5898
|
return this.#isOnlyPlainTextPasted(clipboardData) || this.#isOnlyURLPasted(clipboardData)
|
|
5429
5899
|
}
|
|
@@ -5473,9 +5943,9 @@ class Clipboard {
|
|
|
5473
5943
|
#pastePlainText(clipboardData) {
|
|
5474
5944
|
const item = clipboardData.items[0];
|
|
5475
5945
|
item.getAsString((text) => {
|
|
5476
|
-
if (
|
|
5946
|
+
if (isAutolinkableURL(text) && this.contents.hasSelectedText()) {
|
|
5477
5947
|
this.contents.createLinkWithSelectedText(text);
|
|
5478
|
-
} else if (
|
|
5948
|
+
} else if (isAutolinkableURL(text)) {
|
|
5479
5949
|
const nodeKey = this.contents.createLink(text);
|
|
5480
5950
|
this.#dispatchLinkInsertEvent(nodeKey, { url: text });
|
|
5481
5951
|
} else if (this.editorElement.supportsMarkdown) {
|
|
@@ -5486,6 +5956,24 @@ class Clipboard {
|
|
|
5486
5956
|
});
|
|
5487
5957
|
}
|
|
5488
5958
|
|
|
5959
|
+
#insertSingleLinkAt(selection, url) {
|
|
5960
|
+
if (!$isRangeSelection(selection)) return
|
|
5961
|
+
|
|
5962
|
+
if (!selection.isCollapsed()) {
|
|
5963
|
+
$toggleLink(null);
|
|
5964
|
+
$toggleLink(url);
|
|
5965
|
+
return
|
|
5966
|
+
}
|
|
5967
|
+
|
|
5968
|
+
const linkNode = $createLinkNode(url).append($createTextNode(url));
|
|
5969
|
+
selection.insertNodes([ linkNode ]);
|
|
5970
|
+
|
|
5971
|
+
// Defer the lexxy:insert-link event until after the active update commits;
|
|
5972
|
+
// listeners may run editor mutations of their own.
|
|
5973
|
+
const nodeKey = linkNode.getKey();
|
|
5974
|
+
Promise.resolve().then(() => this.#dispatchLinkInsertEvent(nodeKey, { url }));
|
|
5975
|
+
}
|
|
5976
|
+
|
|
5489
5977
|
#dispatchLinkInsertEvent(nodeKey, payload) {
|
|
5490
5978
|
const linkManipulationMethods = {
|
|
5491
5979
|
replaceLinkWith: (html, options) => this.contents.replaceNodeWithHTML(nodeKey, html, options),
|
|
@@ -5577,6 +6065,28 @@ class Clipboard {
|
|
|
5577
6065
|
}
|
|
5578
6066
|
}
|
|
5579
6067
|
|
|
6068
|
+
function $bareUrlFromSingleLink(nodes) {
|
|
6069
|
+
if (nodes.length !== 1) return null
|
|
6070
|
+
|
|
6071
|
+
const node = nodes[0];
|
|
6072
|
+
if ($isLinkNode(node)) return $bareUrlFromLink(node)
|
|
6073
|
+
|
|
6074
|
+
if ($isParagraphNode(node)) {
|
|
6075
|
+
const children = node.getChildren();
|
|
6076
|
+
if (children.length === 1 && $isLinkNode(children[0])) {
|
|
6077
|
+
return $bareUrlFromLink(children[0])
|
|
6078
|
+
}
|
|
6079
|
+
}
|
|
6080
|
+
|
|
6081
|
+
return null
|
|
6082
|
+
}
|
|
6083
|
+
|
|
6084
|
+
function $bareUrlFromLink(linkNode) {
|
|
6085
|
+
const url = linkNode.getURL();
|
|
6086
|
+
if (!url) return null
|
|
6087
|
+
return linkNode.getTextContent() === url ? url : null
|
|
6088
|
+
}
|
|
6089
|
+
|
|
5580
6090
|
class Extensions {
|
|
5581
6091
|
|
|
5582
6092
|
constructor(lexxyElement) {
|
|
@@ -5595,6 +6105,13 @@ class Extensions {
|
|
|
5595
6105
|
|
|
5596
6106
|
this.#clearPreviousExtensionToolbarButtons(toolbar);
|
|
5597
6107
|
this.#addExtensionToolbarButtons(toolbar);
|
|
6108
|
+
toolbar.requestOverflowRefresh();
|
|
6109
|
+
}
|
|
6110
|
+
|
|
6111
|
+
dispose() {
|
|
6112
|
+
while (this.enabledExtensions.length) {
|
|
6113
|
+
this.enabledExtensions.pop().dispose();
|
|
6114
|
+
}
|
|
5598
6115
|
}
|
|
5599
6116
|
|
|
5600
6117
|
#clearPreviousExtensionToolbarButtons(toolbar) {
|
|
@@ -6002,9 +6519,15 @@ class TablesExtension extends LexxyExtension {
|
|
|
6002
6519
|
setScrollableTablesActive(editor, true);
|
|
6003
6520
|
|
|
6004
6521
|
return mergeRegister(
|
|
6005
|
-
// Register Lexical table plugins
|
|
6006
6522
|
registerTablePlugin(editor),
|
|
6007
|
-
|
|
6523
|
+
|
|
6524
|
+
// Lexxy registers extensions before setRootElement(), but table
|
|
6525
|
+
// drag-selection needs a root before wiring its pointer handlers.
|
|
6526
|
+
editor.registerRootListener((rootElement) => {
|
|
6527
|
+
if (rootElement) {
|
|
6528
|
+
return registerTableSelectionObserver(editor, true)
|
|
6529
|
+
}
|
|
6530
|
+
}),
|
|
6008
6531
|
|
|
6009
6532
|
// Bug fix: Prevent hardcoded background color (Lexical #8089)
|
|
6010
6533
|
editor.registerNodeTransform(TableCellNode, (node) => {
|
|
@@ -6712,7 +7235,8 @@ class FormatEscapeExtension extends LexxyExtension {
|
|
|
6712
7235
|
KEY_ARROW_DOWN_COMMAND,
|
|
6713
7236
|
(event) => $handleArrowDownInCodeBlock(event),
|
|
6714
7237
|
COMMAND_PRIORITY_NORMAL
|
|
6715
|
-
)
|
|
7238
|
+
),
|
|
7239
|
+
editor.registerNodeTransform(QuoteNode, $ensureQuoteHasParagraphChild)
|
|
6716
7240
|
)
|
|
6717
7241
|
}
|
|
6718
7242
|
})
|
|
@@ -6765,6 +7289,13 @@ function $handleArrowDownInCodeBlock(event) {
|
|
|
6765
7289
|
return false
|
|
6766
7290
|
}
|
|
6767
7291
|
|
|
7292
|
+
function $ensureQuoteHasParagraphChild(quoteNode) {
|
|
7293
|
+
if (!quoteNode.isEmpty()) return
|
|
7294
|
+
|
|
7295
|
+
quoteNode.append($createParagraphNode());
|
|
7296
|
+
if ($containsRangeSelection(quoteNode)) quoteNode.getFirstChild().select();
|
|
7297
|
+
}
|
|
7298
|
+
|
|
6768
7299
|
class LinkOpenerExtension extends LexxyExtension {
|
|
6769
7300
|
get enabled() {
|
|
6770
7301
|
return this.editorElement.supportsRichText
|
|
@@ -6828,6 +7359,38 @@ function $openLink(target) {
|
|
|
6828
7359
|
}
|
|
6829
7360
|
}
|
|
6830
7361
|
|
|
7362
|
+
class PreventLexicalTripleClickExtension extends LexxyExtension {
|
|
7363
|
+
get lexicalExtension() {
|
|
7364
|
+
return defineExtension({
|
|
7365
|
+
name: "lexxy/prevent-lexical-triple-click",
|
|
7366
|
+
register: (editor) => editor.registerRootListener((rootElement) => {
|
|
7367
|
+
if (rootElement) {
|
|
7368
|
+
return registerEventListener(
|
|
7369
|
+
rootElement,
|
|
7370
|
+
"click",
|
|
7371
|
+
this.#handleTripleClick.bind(this),
|
|
7372
|
+
{ capture: true }
|
|
7373
|
+
)
|
|
7374
|
+
}
|
|
7375
|
+
})
|
|
7376
|
+
})
|
|
7377
|
+
}
|
|
7378
|
+
|
|
7379
|
+
// Stop propagation of the triple-click to prevent Lexical's handler from running.
|
|
7380
|
+
//
|
|
7381
|
+
// Lexical's onClick handler implements a triple-click handler that is trivial/anemic/naïve. The
|
|
7382
|
+
// intention of the change, made in facebook/lexical#4512, seems to be to deal with browsers'
|
|
7383
|
+
// "overselection" behavior, where a triple-click selection might end at offset 0 of the following
|
|
7384
|
+
// block, which can cause issues when transforming the selection. But the implementation breaks
|
|
7385
|
+
// many common real-world use cases and Lexxy does not demonstrate the behavior it's intended to
|
|
7386
|
+
// work around (in headers or tables).
|
|
7387
|
+
#handleTripleClick(event) {
|
|
7388
|
+
if (event.detail === 3) {
|
|
7389
|
+
event.stopPropagation();
|
|
7390
|
+
}
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
|
|
6831
7394
|
class LexicalEditorElement extends HTMLElement {
|
|
6832
7395
|
static formAssociated = true
|
|
6833
7396
|
static debug = false
|
|
@@ -6852,6 +7415,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6852
7415
|
this.id ||= generateDomId("lexxy-editor");
|
|
6853
7416
|
this.config = new EditorConfiguration(this);
|
|
6854
7417
|
this.extensions = new Extensions(this);
|
|
7418
|
+
this.#disposables.push(this.extensions);
|
|
6855
7419
|
|
|
6856
7420
|
this.editor = this.#createEditor();
|
|
6857
7421
|
this.#disposables.push(this.editor);
|
|
@@ -6864,6 +7428,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6864
7428
|
this.#disposables.push(this.selection);
|
|
6865
7429
|
|
|
6866
7430
|
this.clipboard = new Clipboard(this);
|
|
7431
|
+
this.#disposables.push(this.clipboard);
|
|
7432
|
+
|
|
6867
7433
|
this.adapter = new BrowserAdapter();
|
|
6868
7434
|
|
|
6869
7435
|
const commandDispatcher = CommandDispatcher.configureFor(this);
|
|
@@ -6935,7 +7501,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6935
7501
|
RewritableHistoryExtension,
|
|
6936
7502
|
AttachmentsExtension,
|
|
6937
7503
|
FormatEscapeExtension,
|
|
6938
|
-
LinkOpenerExtension
|
|
7504
|
+
LinkOpenerExtension,
|
|
7505
|
+
PreventLexicalTripleClickExtension
|
|
6939
7506
|
]
|
|
6940
7507
|
}
|
|
6941
7508
|
|
|
@@ -6966,6 +7533,16 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6966
7533
|
}
|
|
6967
7534
|
}
|
|
6968
7535
|
|
|
7536
|
+
acceptsFile(file) {
|
|
7537
|
+
return dispatch(this, "lexxy:file-accept", { file }, true)
|
|
7538
|
+
}
|
|
7539
|
+
|
|
7540
|
+
$generateNodesFromDOM(doc) {
|
|
7541
|
+
let nodes = $generateNodesFromDOM(this.editor, doc);
|
|
7542
|
+
if ($hasUpdateTag(PASTE_TAG)) nodes = $convertInlineImageDataURIs(nodes, this);
|
|
7543
|
+
return filterDisallowedAttachmentNodes(nodes, this)
|
|
7544
|
+
}
|
|
7545
|
+
|
|
6969
7546
|
get isEmpty() {
|
|
6970
7547
|
return [ "<p><br></p>", "<p></p>", "" ].includes(this.value.trim())
|
|
6971
7548
|
}
|
|
@@ -7088,7 +7665,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7088
7665
|
|
|
7089
7666
|
#parseHtmlIntoLexicalNodes(html) {
|
|
7090
7667
|
if (!html) html = "<p></p>";
|
|
7091
|
-
const nodes =
|
|
7668
|
+
const nodes = this.$generateNodesFromDOM(parseHtml(`${html}`));
|
|
7092
7669
|
|
|
7093
7670
|
return nodes
|
|
7094
7671
|
.filter(this.#isNotWhitespaceOnlyNode)
|
|
@@ -7597,253 +8174,6 @@ function $getReadableTextContent(node) {
|
|
|
7597
8174
|
return node.getTextContent()
|
|
7598
8175
|
}
|
|
7599
8176
|
|
|
7600
|
-
class ToolbarDropdown extends HTMLElement {
|
|
7601
|
-
#listeners = new ListenerBin()
|
|
7602
|
-
|
|
7603
|
-
connectedCallback() {
|
|
7604
|
-
this.container = this.closest("details");
|
|
7605
|
-
|
|
7606
|
-
this.#listeners.track(
|
|
7607
|
-
registerEventListener(this.container, "toggle", this.#handleToggle),
|
|
7608
|
-
registerEventListener(this.container, "keydown", this.#handleKeyDown)
|
|
7609
|
-
);
|
|
7610
|
-
|
|
7611
|
-
this.#onToolbarEditor(this.initialize.bind(this));
|
|
7612
|
-
}
|
|
7613
|
-
|
|
7614
|
-
disconnectedCallback() {
|
|
7615
|
-
this.#listeners.dispose();
|
|
7616
|
-
}
|
|
7617
|
-
|
|
7618
|
-
get toolbar() {
|
|
7619
|
-
return this.closest("lexxy-toolbar")
|
|
7620
|
-
}
|
|
7621
|
-
|
|
7622
|
-
get editorElement() {
|
|
7623
|
-
return this.toolbar.editorElement
|
|
7624
|
-
}
|
|
7625
|
-
|
|
7626
|
-
get editor() {
|
|
7627
|
-
return this.toolbar.editor
|
|
7628
|
-
}
|
|
7629
|
-
|
|
7630
|
-
track(...listeners) {
|
|
7631
|
-
this.#listeners.track(...listeners);
|
|
7632
|
-
}
|
|
7633
|
-
|
|
7634
|
-
initialize() {
|
|
7635
|
-
// Any post-editor initialization
|
|
7636
|
-
}
|
|
7637
|
-
|
|
7638
|
-
close() {
|
|
7639
|
-
this.editor.focus();
|
|
7640
|
-
this.container.open = false;
|
|
7641
|
-
}
|
|
7642
|
-
|
|
7643
|
-
async #onToolbarEditor(callback) {
|
|
7644
|
-
await this.toolbar.editorElement;
|
|
7645
|
-
callback();
|
|
7646
|
-
}
|
|
7647
|
-
|
|
7648
|
-
#handleToggle = () => {
|
|
7649
|
-
if (this.container.open) {
|
|
7650
|
-
this.#handleOpen();
|
|
7651
|
-
}
|
|
7652
|
-
}
|
|
7653
|
-
|
|
7654
|
-
async #handleOpen() {
|
|
7655
|
-
this.#interactiveElements[0].focus();
|
|
7656
|
-
this.#resetTabIndexValues();
|
|
7657
|
-
}
|
|
7658
|
-
|
|
7659
|
-
#handleKeyDown = (event) => {
|
|
7660
|
-
if (event.key === "Escape") {
|
|
7661
|
-
event.stopPropagation();
|
|
7662
|
-
this.close();
|
|
7663
|
-
}
|
|
7664
|
-
}
|
|
7665
|
-
|
|
7666
|
-
async #resetTabIndexValues() {
|
|
7667
|
-
await nextFrame();
|
|
7668
|
-
this.#buttons.forEach((element, index) => {
|
|
7669
|
-
element.setAttribute("tabindex", index === 0 ? 0 : "-1");
|
|
7670
|
-
});
|
|
7671
|
-
}
|
|
7672
|
-
|
|
7673
|
-
get #interactiveElements() {
|
|
7674
|
-
return Array.from(this.querySelectorAll("button, input"))
|
|
7675
|
-
}
|
|
7676
|
-
|
|
7677
|
-
get #buttons() {
|
|
7678
|
-
return Array.from(this.querySelectorAll("button"))
|
|
7679
|
-
}
|
|
7680
|
-
}
|
|
7681
|
-
|
|
7682
|
-
class LinkDropdown extends ToolbarDropdown {
|
|
7683
|
-
connectedCallback() {
|
|
7684
|
-
super.connectedCallback();
|
|
7685
|
-
|
|
7686
|
-
this.input = this.querySelector("input");
|
|
7687
|
-
|
|
7688
|
-
this.track(
|
|
7689
|
-
registerEventListener(this.container, "toggle", this.#handleToggle),
|
|
7690
|
-
registerEventListener(this.input, "keydown", this.#handleEnter),
|
|
7691
|
-
registerEventListener(this.linkButton, "click", this.#handleLink),
|
|
7692
|
-
registerEventListener(this.unlinkButton, "click", this.#handleUnlink)
|
|
7693
|
-
);
|
|
7694
|
-
}
|
|
7695
|
-
|
|
7696
|
-
get linkButton() {
|
|
7697
|
-
return this.querySelector("[value='link']")
|
|
7698
|
-
}
|
|
7699
|
-
|
|
7700
|
-
get unlinkButton() {
|
|
7701
|
-
return this.querySelector("[value='unlink']")
|
|
7702
|
-
}
|
|
7703
|
-
|
|
7704
|
-
#handleToggle = ({ newState }) => {
|
|
7705
|
-
this.input.value = this.#selectedLinkUrl;
|
|
7706
|
-
this.input.required = newState === "open";
|
|
7707
|
-
}
|
|
7708
|
-
|
|
7709
|
-
#handleEnter = (event) => {
|
|
7710
|
-
if (event.key === "Enter") {
|
|
7711
|
-
event.preventDefault();
|
|
7712
|
-
event.stopPropagation();
|
|
7713
|
-
this.#handleLink(event);
|
|
7714
|
-
}
|
|
7715
|
-
}
|
|
7716
|
-
|
|
7717
|
-
#handleLink = () => {
|
|
7718
|
-
if (!this.input.checkValidity()) {
|
|
7719
|
-
this.input.reportValidity();
|
|
7720
|
-
return
|
|
7721
|
-
}
|
|
7722
|
-
|
|
7723
|
-
this.editor.dispatchCommand("link", this.input.value);
|
|
7724
|
-
this.close();
|
|
7725
|
-
}
|
|
7726
|
-
|
|
7727
|
-
#handleUnlink = () => {
|
|
7728
|
-
this.editor.dispatchCommand("unlink");
|
|
7729
|
-
this.close();
|
|
7730
|
-
}
|
|
7731
|
-
|
|
7732
|
-
get #selectedLinkUrl() {
|
|
7733
|
-
return this.editor.getEditorState().read(() => {
|
|
7734
|
-
const linkNode = this.editorElement.selection.nearestNodeOfType(LinkNode);
|
|
7735
|
-
return linkNode?.getURL() ?? ""
|
|
7736
|
-
})
|
|
7737
|
-
}
|
|
7738
|
-
}
|
|
7739
|
-
|
|
7740
|
-
const APPLY_HIGHLIGHT_SELECTOR = "button.lexxy-highlight-button";
|
|
7741
|
-
const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
|
|
7742
|
-
|
|
7743
|
-
// Use Symbol instead of null since $getSelectionStyleValueForProperty
|
|
7744
|
-
// responds differently for backward selections if null is the default
|
|
7745
|
-
// see https://github.com/facebook/lexical/issues/8013
|
|
7746
|
-
const NO_STYLE = Symbol("no_style");
|
|
7747
|
-
|
|
7748
|
-
class HighlightDropdown extends ToolbarDropdown {
|
|
7749
|
-
initialize() {
|
|
7750
|
-
this.#setUpButtons();
|
|
7751
|
-
this.#registerButtonHandlers();
|
|
7752
|
-
}
|
|
7753
|
-
|
|
7754
|
-
connectedCallback() {
|
|
7755
|
-
super.connectedCallback();
|
|
7756
|
-
this.track(registerEventListener(this.container, "toggle", this.#handleToggle));
|
|
7757
|
-
}
|
|
7758
|
-
|
|
7759
|
-
#registerButtonHandlers() {
|
|
7760
|
-
this.#colorButtons.forEach(button => {
|
|
7761
|
-
this.track(registerEventListener(button, "click", this.#handleColorButtonClick));
|
|
7762
|
-
});
|
|
7763
|
-
this.track(registerEventListener(this.querySelector(REMOVE_HIGHLIGHT_SELECTOR), "click", this.#handleRemoveHighlightClick));
|
|
7764
|
-
}
|
|
7765
|
-
|
|
7766
|
-
#setUpButtons() {
|
|
7767
|
-
this.#buttonContainer.innerHTML = "";
|
|
7768
|
-
|
|
7769
|
-
const colorGroups = this.editorElement.config.get("highlight.buttons");
|
|
7770
|
-
|
|
7771
|
-
this.#populateButtonGroup("color", colorGroups.color);
|
|
7772
|
-
this.#populateButtonGroup("background-color", colorGroups["background-color"]);
|
|
7773
|
-
|
|
7774
|
-
const maxNumberOfColors = Math.max(colorGroups.color.length, colorGroups["background-color"].length);
|
|
7775
|
-
this.style.setProperty("--max-colors", maxNumberOfColors);
|
|
7776
|
-
}
|
|
7777
|
-
|
|
7778
|
-
#populateButtonGroup(attribute, values) {
|
|
7779
|
-
values.forEach((value, index) => {
|
|
7780
|
-
this.#buttonContainer.appendChild(this.#createButton(attribute, value, index));
|
|
7781
|
-
});
|
|
7782
|
-
}
|
|
7783
|
-
|
|
7784
|
-
#createButton(attribute, value, index) {
|
|
7785
|
-
const button = document.createElement("button");
|
|
7786
|
-
button.dataset.style = attribute;
|
|
7787
|
-
button.style.setProperty(attribute, value);
|
|
7788
|
-
button.dataset.value = value;
|
|
7789
|
-
button.classList.add("lexxy-editor__toolbar-button", "lexxy-highlight-button");
|
|
7790
|
-
button.name = attribute + "-" + index;
|
|
7791
|
-
return button
|
|
7792
|
-
}
|
|
7793
|
-
|
|
7794
|
-
#handleToggle = ({ newState }) => {
|
|
7795
|
-
if (newState === "open") {
|
|
7796
|
-
this.editor.getEditorState().read(() => {
|
|
7797
|
-
this.#updateColorButtonStates($getSelection());
|
|
7798
|
-
});
|
|
7799
|
-
}
|
|
7800
|
-
}
|
|
7801
|
-
|
|
7802
|
-
#handleColorButtonClick = (event) => {
|
|
7803
|
-
event.preventDefault();
|
|
7804
|
-
|
|
7805
|
-
const button = event.target.closest(APPLY_HIGHLIGHT_SELECTOR);
|
|
7806
|
-
if (!button) return
|
|
7807
|
-
|
|
7808
|
-
const attribute = button.dataset.style;
|
|
7809
|
-
const value = button.dataset.value;
|
|
7810
|
-
|
|
7811
|
-
this.editor.dispatchCommand("toggleHighlight", { [attribute]: value });
|
|
7812
|
-
this.close();
|
|
7813
|
-
}
|
|
7814
|
-
|
|
7815
|
-
#handleRemoveHighlightClick = (event) => {
|
|
7816
|
-
event.preventDefault();
|
|
7817
|
-
|
|
7818
|
-
this.editor.dispatchCommand("removeHighlight");
|
|
7819
|
-
this.close();
|
|
7820
|
-
}
|
|
7821
|
-
|
|
7822
|
-
#updateColorButtonStates(selection) {
|
|
7823
|
-
if (!$isRangeSelection(selection)) { return }
|
|
7824
|
-
|
|
7825
|
-
// Use non-"" default, so "" indicates mixed highlighting
|
|
7826
|
-
const textColor = $getSelectionStyleValueForProperty(selection, "color", NO_STYLE);
|
|
7827
|
-
const backgroundColor = $getSelectionStyleValueForProperty(selection, "background-color", NO_STYLE);
|
|
7828
|
-
|
|
7829
|
-
this.#colorButtons.forEach(button => {
|
|
7830
|
-
const matchesSelection = button.dataset.value === textColor || button.dataset.value === backgroundColor;
|
|
7831
|
-
button.setAttribute("aria-pressed", matchesSelection);
|
|
7832
|
-
});
|
|
7833
|
-
|
|
7834
|
-
const hasHighlight = textColor !== NO_STYLE || backgroundColor !== NO_STYLE;
|
|
7835
|
-
this.querySelector(REMOVE_HIGHLIGHT_SELECTOR).disabled = !hasHighlight;
|
|
7836
|
-
}
|
|
7837
|
-
|
|
7838
|
-
get #buttonContainer() {
|
|
7839
|
-
return this.querySelector(".lexxy-highlight-colors")
|
|
7840
|
-
}
|
|
7841
|
-
|
|
7842
|
-
get #colorButtons() {
|
|
7843
|
-
return Array.from(this.querySelectorAll(APPLY_HIGHLIGHT_SELECTOR))
|
|
7844
|
-
}
|
|
7845
|
-
}
|
|
7846
|
-
|
|
7847
8177
|
class BaseSource {
|
|
7848
8178
|
// Template method to override
|
|
7849
8179
|
async buildListItems(filter = "") {
|
|
@@ -8236,7 +8566,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8236
8566
|
}
|
|
8237
8567
|
|
|
8238
8568
|
#selectOption(listItem) {
|
|
8239
|
-
this.#
|
|
8569
|
+
this.#clearListItemSelection();
|
|
8240
8570
|
listItem.toggleAttribute("aria-selected", true);
|
|
8241
8571
|
listItem.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
8242
8572
|
listItem.focus();
|
|
@@ -8246,18 +8576,28 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8246
8576
|
this.#editorElement.focus();
|
|
8247
8577
|
});
|
|
8248
8578
|
|
|
8249
|
-
this.#
|
|
8250
|
-
this.#
|
|
8251
|
-
this.#
|
|
8579
|
+
this.#setEditorAssociationAttribute("aria-controls", this.popoverElement.id);
|
|
8580
|
+
this.#setEditorAssociationAttribute("aria-activedescendant", listItem.id);
|
|
8581
|
+
this.#setEditorAssociationAttribute("aria-haspopup", "listbox");
|
|
8252
8582
|
}
|
|
8253
8583
|
|
|
8254
|
-
#
|
|
8584
|
+
#clearListItemSelection() {
|
|
8255
8585
|
this.#listItemElements.forEach((item) => { item.toggleAttribute("aria-selected", false); });
|
|
8586
|
+
}
|
|
8587
|
+
|
|
8588
|
+
#clearSelection() {
|
|
8589
|
+
this.#clearListItemSelection();
|
|
8256
8590
|
this.#editorContentElement.removeAttribute("aria-controls");
|
|
8257
8591
|
this.#editorContentElement.removeAttribute("aria-activedescendant");
|
|
8258
8592
|
this.#editorContentElement.removeAttribute("aria-haspopup");
|
|
8259
8593
|
}
|
|
8260
8594
|
|
|
8595
|
+
#setEditorAssociationAttribute(name, value) {
|
|
8596
|
+
if (this.#editorContentElement.getAttribute(name) !== value) {
|
|
8597
|
+
this.#editorContentElement.setAttribute(name, value);
|
|
8598
|
+
}
|
|
8599
|
+
}
|
|
8600
|
+
|
|
8261
8601
|
#positionPopover() {
|
|
8262
8602
|
const { x, y, fontSize } = this.#selection.cursorPosition;
|
|
8263
8603
|
const editorRect = this.#editorElement.getBoundingClientRect();
|
|
@@ -8353,7 +8693,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8353
8693
|
|
|
8354
8694
|
#showEmptyResults() {
|
|
8355
8695
|
this.popoverElement.classList.add("lexxy-prompt-menu--empty");
|
|
8356
|
-
const el = createElement("li", {
|
|
8696
|
+
const el = createElement("li", { textContent: this.#emptyResultsMessage });
|
|
8357
8697
|
el.classList.add("lexxy-prompt-menu__item--empty");
|
|
8358
8698
|
this.popoverElement.append(el);
|
|
8359
8699
|
}
|
|
@@ -8435,7 +8775,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8435
8775
|
}
|
|
8436
8776
|
|
|
8437
8777
|
#buildEditableTextNodes(template) {
|
|
8438
|
-
return
|
|
8778
|
+
return this.#editorElement.$generateNodesFromDOM(parseHtml(`${template.innerHTML}`))
|
|
8439
8779
|
}
|
|
8440
8780
|
|
|
8441
8781
|
#insertTemplatesAsAttachments(templates, stringToReplace, fallbackSgid = null) {
|
|
@@ -9414,14 +9754,19 @@ class TableTools extends HTMLElement {
|
|
|
9414
9754
|
|
|
9415
9755
|
function defineElements() {
|
|
9416
9756
|
const elements = {
|
|
9757
|
+
// Toolbar must be registered BEFORE Editor
|
|
9417
9758
|
"lexxy-toolbar": LexicalToolbarElement,
|
|
9418
|
-
"lexxy-
|
|
9419
|
-
"lexxy-link-dropdown": LinkDropdown,
|
|
9759
|
+
"lexxy-toolbar-dropdown": ToolbarDropdown,
|
|
9420
9760
|
"lexxy-highlight-dropdown": HighlightDropdown,
|
|
9761
|
+
"lexxy-link-dropdown": LinkDropdown,
|
|
9762
|
+
|
|
9763
|
+
"lexxy-editor": LexicalEditorElement,
|
|
9764
|
+
|
|
9765
|
+
// Prompt must be registered AFTER Editor
|
|
9421
9766
|
"lexxy-prompt": LexicalPromptElement,
|
|
9422
9767
|
"lexxy-code-language-picker": CodeLanguagePicker,
|
|
9423
9768
|
"lexxy-node-delete-button": NodeDeleteButton,
|
|
9424
|
-
"lexxy-table-tools": TableTools
|
|
9769
|
+
"lexxy-table-tools": TableTools
|
|
9425
9770
|
};
|
|
9426
9771
|
|
|
9427
9772
|
Object.entries(elements).forEach(([ name, element ]) => {
|