@37signals/lexxy 0.9.32 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/lexxy.esm.js +1803 -643
- package/dist/stylesheets/lexxy-editor.css +238 -74
- package/dist/stylesheets/lexxy-variables.css +5 -4
- package/package.json +1 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export { highlightCode, highlightElement } from './lexxy_helpers.esm.js';
|
|
2
2
|
import createDOMPurify from 'dompurify';
|
|
3
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, defineExtension, $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $
|
|
4
|
+
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, $isTextNode, DecoratorNode, $createTextNode, defineExtension, $createNodeSelection, $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $isDecoratorNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $normalizeSelection__EXPERIMENTAL, $isNodeSelection, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $getCommonAncestor, $findMatchingParent, TextNode, createCommand, COMMAND_PRIORITY_EDITOR, $getEditor, $getNodeByKey, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $createRangeSelection, $setSelection, ElementNode, $splitNode, createState, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, INSERT_LINE_BREAK_COMMAND, COMMAND_PRIORITY_HIGH, INSERT_PARAGRAPH_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_CRITICAL, $addUpdateTag, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $getChildCaretAtIndex, $createLineBreakNode, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, PASTE_COMMAND, $onUpdate, ParagraphNode, RootNode, DRAGSTART_COMMAND, DROP_COMMAND, KEY_DOWN_COMMAND, flipDirection, mergeRegister as mergeRegister$1, $createRangeSelectionFromDom, CLEAR_HISTORY_COMMAND, KEY_ENTER_COMMAND, KEY_SPACE_COMMAND, INPUT_COMMAND, HISTORY_PUSH_TAG, KEY_BACKSPACE_COMMAND } from 'lexical';
|
|
5
5
|
import * as lexical from 'lexical';
|
|
6
6
|
export { lexical as Lexical };
|
|
7
7
|
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
|
|
8
8
|
import { buildEditorFromExtensions } from '@lexical/extension';
|
|
9
9
|
import { ListNode, ListItemNode, $getListDepth, $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, $createListItemNode, registerList } from '@lexical/list';
|
|
10
|
-
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, $descendantsMatching,
|
|
10
|
+
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, $descendantsMatching, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, mergeRegister, $insertNodeToNearestRoot, $getNearestBlockElementAncestorOrThrow, IS_APPLE } from '@lexical/utils';
|
|
11
11
|
import { registerPlainText } from '@lexical/plain-text';
|
|
12
12
|
import { RichTextExtension, $isQuoteNode, QuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
13
13
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
@@ -74,10 +74,6 @@ class Configuration {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function range(from, to) {
|
|
78
|
-
return [ ...Array(1 + to - from).keys() ].map(i => i + from)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
77
|
const global = new Configuration({
|
|
82
78
|
attachmentTagName: "action-text-attachment",
|
|
83
79
|
attachmentContentTypeNamespace: "actiontext",
|
|
@@ -98,8 +94,28 @@ const presets = new Configuration({
|
|
|
98
94
|
headings: [ "h2", "h3", "h4" ],
|
|
99
95
|
highlight: {
|
|
100
96
|
buttons: {
|
|
101
|
-
color:
|
|
102
|
-
|
|
97
|
+
color: [
|
|
98
|
+
{ value: "var(--highlight-1)", label: "Yellow" },
|
|
99
|
+
{ value: "var(--highlight-2)", label: "Orange" },
|
|
100
|
+
{ value: "var(--highlight-3)", label: "Red" },
|
|
101
|
+
{ value: "var(--highlight-4)", label: "Magenta" },
|
|
102
|
+
{ value: "var(--highlight-5)", label: "Purple" },
|
|
103
|
+
{ value: "var(--highlight-6)", label: "Blue" },
|
|
104
|
+
{ value: "var(--highlight-7)", label: "Green" },
|
|
105
|
+
{ value: "var(--highlight-8)", label: "Sand" },
|
|
106
|
+
{ value: "var(--highlight-9)", label: "Gray" },
|
|
107
|
+
],
|
|
108
|
+
"background-color": [
|
|
109
|
+
{ value: "var(--highlight-bg-1)", label: "Yellow" },
|
|
110
|
+
{ value: "var(--highlight-bg-2)", label: "Orange" },
|
|
111
|
+
{ value: "var(--highlight-bg-3)", label: "Red" },
|
|
112
|
+
{ value: "var(--highlight-bg-4)", label: "Magenta" },
|
|
113
|
+
{ value: "var(--highlight-bg-5)", label: "Purple" },
|
|
114
|
+
{ value: "var(--highlight-bg-6)", label: "Blue" },
|
|
115
|
+
{ value: "var(--highlight-bg-7)", label: "Green" },
|
|
116
|
+
{ value: "var(--highlight-bg-8)", label: "Sand" },
|
|
117
|
+
{ value: "var(--highlight-bg-9)", label: "Gray" },
|
|
118
|
+
],
|
|
103
119
|
},
|
|
104
120
|
permit: {
|
|
105
121
|
color: [],
|
|
@@ -568,11 +584,45 @@ function checkVisibility(element, options) {
|
|
|
568
584
|
}
|
|
569
585
|
}
|
|
570
586
|
|
|
571
|
-
|
|
587
|
+
// Keyboard and programmatic clicks carry a synthetic pointer.
|
|
588
|
+
function isKeyboardActivation(event) {
|
|
589
|
+
return event instanceof PointerEvent && event.pointerId === -1
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function trapFocusAtTabBoundary(container, event) {
|
|
593
|
+
if (event.key !== "Tab") return
|
|
594
|
+
|
|
595
|
+
const tabStops = Array.from(container.querySelectorAll("a[href], area[href], button, input:not([type='hidden']), select, textarea, iframe, object, embed, [contenteditable]:not([contenteditable='false']), [tabindex], summary, audio[controls], video[controls]"))
|
|
596
|
+
.filter(element => element.tabIndex >= 0 && !element.matches(":disabled") && !element.closest("[inert]") && isActiveAndVisible(element))
|
|
597
|
+
.sort((first, second) => {
|
|
598
|
+
if (first.tabIndex > 0 && second.tabIndex > 0) {
|
|
599
|
+
return first.tabIndex - second.tabIndex
|
|
600
|
+
} else if (first.tabIndex > 0) {
|
|
601
|
+
return -1
|
|
602
|
+
} else if (second.tabIndex > 0) {
|
|
603
|
+
return 1
|
|
604
|
+
} else {
|
|
605
|
+
return 0
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const firstTabStop = tabStops[0];
|
|
610
|
+
const lastTabStop = tabStops.at(-1);
|
|
611
|
+
|
|
612
|
+
if (event.shiftKey && document.activeElement === firstTabStop) {
|
|
613
|
+
event.preventDefault();
|
|
614
|
+
lastTabStop.focus();
|
|
615
|
+
} else if (!event.shiftKey && document.activeElement === lastTabStop) {
|
|
616
|
+
event.preventDefault();
|
|
617
|
+
firstTabStop.focus();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function handleRollingTabIndex(elements, event, { orientation = "horizontal", wrap = false } = {}) {
|
|
572
622
|
const previousActiveElement = document.activeElement;
|
|
573
623
|
|
|
574
624
|
if (elements.includes(previousActiveElement)) {
|
|
575
|
-
const finder = new NextElementFinder(elements, event.key);
|
|
625
|
+
const finder = new NextElementFinder(elements, event.key, { orientation, wrap });
|
|
576
626
|
|
|
577
627
|
if (finder.selectNext(previousActiveElement)) {
|
|
578
628
|
event.preventDefault();
|
|
@@ -581,9 +631,11 @@ function handleRollingTabIndex(elements, event) {
|
|
|
581
631
|
}
|
|
582
632
|
|
|
583
633
|
class NextElementFinder {
|
|
584
|
-
constructor(elements, key) {
|
|
634
|
+
constructor(elements, key, { orientation, wrap }) {
|
|
585
635
|
this.elements = elements;
|
|
586
636
|
this.key = key;
|
|
637
|
+
this.orientation = orientation;
|
|
638
|
+
this.wrap = wrap;
|
|
587
639
|
}
|
|
588
640
|
|
|
589
641
|
selectNext(fromElement) {
|
|
@@ -600,23 +652,36 @@ class NextElementFinder {
|
|
|
600
652
|
}
|
|
601
653
|
|
|
602
654
|
#findNextElement(fromElement) {
|
|
603
|
-
switch (this.key) {
|
|
604
|
-
case "
|
|
605
|
-
case "ArrowDown":
|
|
655
|
+
switch (this.#directionFor(this.key)) {
|
|
656
|
+
case "next":
|
|
606
657
|
return this.#findNextSibling(fromElement)
|
|
607
658
|
|
|
608
|
-
case "
|
|
609
|
-
case "ArrowUp":
|
|
659
|
+
case "previous":
|
|
610
660
|
return this.#findPreviousSibling(fromElement)
|
|
611
661
|
|
|
612
|
-
case "
|
|
662
|
+
case "first":
|
|
613
663
|
return this.#findFirst()
|
|
614
664
|
|
|
615
|
-
case "
|
|
665
|
+
case "last":
|
|
616
666
|
return this.#findLast()
|
|
617
667
|
}
|
|
618
668
|
}
|
|
619
669
|
|
|
670
|
+
#directionFor(key) {
|
|
671
|
+
if (key === "Home") return "first"
|
|
672
|
+
if (key === "End") return "last"
|
|
673
|
+
if (this.#nextKeys.includes(key)) return "next"
|
|
674
|
+
if (this.#previousKeys.includes(key)) return "previous"
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
get #nextKeys() {
|
|
678
|
+
return this.orientation === "both" ? [ "ArrowRight", "ArrowDown" ] : [ "ArrowRight" ]
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
get #previousKeys() {
|
|
682
|
+
return this.orientation === "both" ? [ "ArrowLeft", "ArrowUp" ] : [ "ArrowLeft" ]
|
|
683
|
+
}
|
|
684
|
+
|
|
620
685
|
#findFirst(elements = this.elements) {
|
|
621
686
|
return elements.find(isActiveAndVisible)
|
|
622
687
|
}
|
|
@@ -627,12 +692,20 @@ class NextElementFinder {
|
|
|
627
692
|
|
|
628
693
|
#findNextSibling(element) {
|
|
629
694
|
const afterElements = this.elements.slice(this.#indexOf(element) + 1);
|
|
630
|
-
return this.#findFirst(afterElements)
|
|
695
|
+
return this.#findFirst(afterElements) ?? this.#wrapToFirst()
|
|
631
696
|
}
|
|
632
697
|
|
|
633
698
|
#findPreviousSibling(element) {
|
|
634
699
|
const beforeElements = this.elements.slice(0, this.#indexOf(element));
|
|
635
|
-
return this.#findLast(beforeElements)
|
|
700
|
+
return this.#findLast(beforeElements) ?? this.#wrapToLast()
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
#wrapToFirst() {
|
|
704
|
+
return this.wrap ? this.#findFirst() : undefined
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
#wrapToLast() {
|
|
708
|
+
return this.wrap ? this.#findLast() : undefined
|
|
636
709
|
}
|
|
637
710
|
|
|
638
711
|
#indexOf(element) {
|
|
@@ -653,29 +726,29 @@ class NextElementFinder {
|
|
|
653
726
|
|
|
654
727
|
var ToolbarIcons = {
|
|
655
728
|
"bold":
|
|
656
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
729
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
657
730
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.05273 1.88232C10.6866 1.88237 12.0033 2.20353 12.9529 2.89673L13.1272 3.0293C13.974 3.70864 14.4008 4.63245 14.4009 5.76562C14.4008 6.49354 14.2316 7.15281 13.8845 7.73145C13.6683 8.09188 13.3997 8.40162 13.0818 8.66016C13.5902 8.92606 14.0196 9.28599 14.3635 9.74121C14.8586 10.3834 15.0945 11.1743 15.0945 12.0879C15.0944 13.3698 14.5922 14.3931 13.5879 15.1106L13.5857 15.1128C12.5967 15.805 11.196 16.125 9.43799 16.125H3.10547V1.88232L9.05273 1.88232ZM6.36108 13.4084H9.28418C10.224 13.4084 10.8634 13.2491 11.2581 12.9851C11.6259 12.7389 11.8198 12.3768 11.8198 11.8367C11.8197 11.2968 11.6259 10.9351 11.2581 10.689C10.8634 10.425 10.2241 10.2649 9.28418 10.2649H6.36108V13.4084ZM6.36108 7.56812H8.78247C9.5163 7.56809 10.0547 7.45371 10.429 7.25757L10.5791 7.16895C10.9438 6.92178 11.1255 6.57934 11.1255 6.09302C11.1254 5.59017 10.9414 5.25227 10.5835 5.02002L10.5784 5.01636L10.5732 5.01343C10.1994 4.75387 9.61878 4.59818 8.78247 4.59814H6.36108V7.56812Z"/>
|
|
658
731
|
</svg>`,
|
|
659
732
|
|
|
660
733
|
"italic":
|
|
661
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
734
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
662
735
|
<path d="M14.1379 3.91187L14.1086 4.06421H11.4668L9.49805 13.9431H12.0981L11.7473 15.7852L11.7188 15.9375H4.16675L4.51758 14.0955L4.54614 13.9431H7.18799L9.17505 4.06421H6.55664L6.90747 2.22217L6.93677 2.06982H14.4888L14.1379 3.91187Z"/>
|
|
663
736
|
</svg>`,
|
|
664
737
|
|
|
665
738
|
"strikethrough":
|
|
666
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
739
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
667
740
|
<path d="M14.3723 11.8015C14.3771 11.8858 14.3811 11.9756 14.3811 12.0681C14.3811 12.811 14.1777 13.4959 13.7725 14.1174L13.7717 14.1189C13.3624 14.7329 12.7463 15.2162 11.9377 15.5742L11.9348 15.5757C11.1214 15.9223 10.1306 16.092 8.96997 16.092C7.9356 16.092 6.93308 15.9348 5.96338 15.6204L5.96045 15.6189C5.00593 15.292 4.24112 14.8699 3.67676 14.3459L3.57568 14.2522L3.63501 14.1277L4.45605 12.397L4.64282 12.5654C5.13492 13.0083 5.76733 13.3759 6.54492 13.6648C7.33475 13.9406 8.14322 14.0786 8.96997 14.0786C10.0731 14.0786 10.8638 13.8932 11.3708 13.5513C11.8757 13.1982 12.1172 12.7464 12.1172 12.1838C12.1172 12.0662 12.1049 11.9556 12.0828 11.8513L12.0344 11.625H14.3621L14.3723 11.8015Z"/>
|
|
668
741
|
<path d="M9.2981 1.91602C10.111 1.91604 10.9109 2.02122 11.6975 2.23096C12.4855 2.44111 13.1683 2.74431 13.7417 3.14429L13.8655 3.23071L13.8083 3.36987L13.1726 4.91235L13.0869 5.1189L12.8987 4.99878C12.3487 4.64881 11.761 4.38633 11.1365 4.21143L11.1328 4.20996C10.585 4.04564 10.0484 3.95419 9.52295 3.93384L9.2981 3.92944C8.22329 3.92944 7.44693 4.12611 6.94043 4.49121C6.44619 4.85665 6.20874 5.31616 6.20874 5.88135L6.21533 6.03296C6.24495 6.37662 6.37751 6.65526 6.61011 6.87964L6.72144 6.97632C6.98746 7.19529 7.30625 7.37584 7.68018 7.51538L8.05151 7.63184C8.45325 7.75061 8.94669 7.87679 9.53247 8.01123L9.53467 8.01196C10.1213 8.15305 10.6426 8.29569 11.0991 8.4375H15C15.5178 8.4375 15.9375 8.85723 15.9375 9.375C15.9375 9.89277 15.5178 10.3125 15 10.3125H3C2.48223 10.3125 2.0625 9.89277 2.0625 9.375C2.0625 8.85723 2.48223 8.4375 3 8.4375H4.93726C4.83783 8.34526 4.74036 8.24896 4.64795 8.146L4.64502 8.14233C4.1721 7.58596 3.94482 6.85113 3.94482 5.95825C3.94483 5.20441 4.14059 4.51965 4.53369 3.90967L4.53516 3.90747C4.94397 3.29427 5.55262 2.81114 6.34863 2.45288C7.15081 2.0919 8.13683 1.91602 9.2981 1.91602Z"/>
|
|
669
742
|
</svg>`,
|
|
670
743
|
|
|
671
744
|
"underline":
|
|
672
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
745
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
673
746
|
<path d="M14 14C14.5523 14 15 14.4477 15 15C15 15.5523 14.5523 16 14 16H4C3.44772 16 3 15.5523 3 15C3 14.4477 3.44772 14 4 14H14Z"/>
|
|
674
747
|
<path d="M12.625 1.59375C13.25 1.59375 13.625 1.97656 13.625 2.64062V9.02344C13.625 11.4844 11.8516 13.1875 8.99219 13.1875C6.14062 13.1875 4.35938 11.4844 4.35938 9.02344V2.64062C4.35938 1.97656 4.74219 1.59375 5.36719 1.59375C6 1.59375 6.375 1.97656 6.375 2.64062V8.84375C6.375 10.3828 7.32031 11.4297 8.99219 11.4297C10.6641 11.4297 11.6172 10.3828 11.6172 8.84375V2.64062C11.6172 1.97656 11.9922 1.59375 12.625 1.59375Z"/>
|
|
675
748
|
</svg>`,
|
|
676
749
|
|
|
677
750
|
"heading":
|
|
678
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
751
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
679
752
|
<path d="M11.5 2C12.0523 2 12.5 2.44772 12.5 3V3.5C12.5 4.05228 12.0523 4.5 11.5 4.5H8V15C8 15.5523 7.55228 16 7 16H6.5C5.94772 16 5.5 15.5523 5.5 15V4.5H2C1.44772 4.5 1 4.05228 1 3.5V3C1 2.44772 1.44772 2 2 2H11.5ZM16 7C16.5523 7 17 7.44772 17 8V8.5C17 9.05228 16.5523 9.5 16 9.5H15V15C15 15.5523 14.5523 16 14 16H13.5C12.9477 16 12.5 15.5523 12.5 15V9.5H11.5C10.9477 9.5 10.5 9.05228 10.5 8.5V8C10.5 7.44772 10.9477 7 11.5 7H16Z"/>
|
|
680
753
|
</svg>`,
|
|
681
754
|
|
|
@@ -685,17 +758,17 @@ var ToolbarIcons = {
|
|
|
685
758
|
</svg>`,
|
|
686
759
|
|
|
687
760
|
"h2":
|
|
688
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
761
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
689
762
|
<path d="M8.12207 4.30078C8.84668 4.30078 9.25684 4.74512 9.25684 5.51758V12.5518C9.25677 13.3241 8.84662 13.7686 8.12207 13.7686C7.39752 13.7686 6.9942 13.3309 6.99414 12.5518V9.87207H3.56934V12.5518C3.56927 13.3241 3.15912 13.7686 2.43457 13.7686C1.71002 13.7686 1.3067 13.3309 1.30664 12.5518V5.51758C1.30664 4.73828 1.70996 4.30078 2.43457 4.30078C3.15918 4.30078 3.56934 4.74512 3.56934 5.51758V8.07422H6.99414V5.51758C6.99414 4.73828 7.39746 4.30078 8.12207 4.30078ZM13.6445 4.19824C15.5244 4.19824 16.8984 5.34668 16.8984 6.91211C16.8984 7.8759 16.4335 8.7237 15.292 9.84473L13.3438 11.8135V11.9092H16.1875C16.8232 11.9092 17.2197 12.251 17.2197 12.8115C17.2196 13.3651 16.83 13.7002 16.1875 13.7002H11.5117C10.8487 13.7002 10.4112 13.3241 10.4111 12.75C10.4111 12.3399 10.6368 11.9843 11.3203 11.3145L13.6855 8.88086C14.4169 8.13583 14.7245 7.64349 14.7246 7.12402C14.7246 6.4541 14.2393 6.00293 13.5215 6.00293C12.9404 6.00293 12.5166 6.29688 12.2158 6.90527C11.9151 7.37002 11.6552 7.54785 11.2588 7.54785C10.7188 7.54785 10.3429 7.17861 10.3428 6.65918C10.3428 5.3877 11.7783 4.19824 13.6445 4.19824Z"/>
|
|
690
763
|
</svg>`,
|
|
691
764
|
|
|
692
765
|
"h3":
|
|
693
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
766
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
694
767
|
<path d="M13.5967 4.19824C15.5928 4.19824 16.9873 5.23047 16.9873 6.7002C16.9872 7.705 16.2421 8.60059 15.2988 8.7168V8.86719C16.4199 8.94923 17.2607 9.89942 17.2607 11.0889C17.2606 12.7362 15.7362 13.9053 13.583 13.9053C11.6827 13.9053 10.1925 12.873 10.1924 11.7041C10.1924 11.1846 10.5547 10.8154 11.0537 10.8154C11.3818 10.8154 11.6553 10.9727 11.9629 11.3555C12.3799 11.9159 12.92 12.2031 13.583 12.2031C14.4853 12.2031 15.0731 11.7313 15.0732 11C15.0732 10.2754 14.4785 9.7832 13.5898 9.7832H13.0361C12.5645 9.7832 12.2159 9.4208 12.2158 8.92188C12.2158 8.44336 12.5576 8.07422 13.0361 8.07422H13.5693C14.3075 8.07422 14.8544 7.60928 14.8545 6.97363C14.8545 6.33789 14.3213 5.90039 13.5557 5.90039C12.9678 5.90039 12.5029 6.16016 12.0859 6.71387C11.8399 7.03508 11.5527 7.17871 11.1973 7.17871C10.671 7.17871 10.295 6.82314 10.2949 6.31738C10.2949 5.18945 11.751 4.19824 13.5967 4.19824ZM8.0332 4.30078C8.75781 4.30078 9.16797 4.74512 9.16797 5.51758V12.5518C9.1679 13.3241 8.75776 13.7686 8.0332 13.7686C7.30865 13.7686 6.90534 13.3309 6.90527 12.5518V9.87207H3.48047V12.5518C3.4804 13.3241 3.07026 13.7686 2.3457 13.7686C1.62115 13.7686 1.21784 13.3309 1.21777 12.5518V5.51758C1.21777 4.73828 1.62109 4.30078 2.3457 4.30078C3.07031 4.30078 3.48047 4.74512 3.48047 5.51758V8.07422H6.90527V5.51758C6.90527 4.73828 7.30859 4.30078 8.0332 4.30078Z"/>
|
|
695
768
|
</svg>`,
|
|
696
769
|
|
|
697
770
|
"h4":
|
|
698
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
771
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
699
772
|
<path d="M14.6357 4.22559C15.7432 4.22559 16.4336 4.80664 16.4336 5.73633V10.3164H16.7275C17.2881 10.3164 17.6436 10.6787 17.6436 11.2256C17.6435 11.7655 17.3017 12.1006 16.7275 12.1006H16.4336V12.6611C16.4335 13.3515 16.0234 13.7891 15.374 13.7891C14.7247 13.7891 14.3282 13.3583 14.3281 12.6611V12.1006H11.04C10.2335 12.1006 9.76863 11.6766 9.76855 10.918C9.76855 10.5762 9.85064 10.3026 10.1104 9.74219C10.7666 8.42289 11.5733 7.0146 12.5713 5.54492C13.2549 4.56738 13.7812 4.22559 14.6357 4.22559ZM7.88965 4.30078C8.61426 4.30078 9.02441 4.74512 9.02441 5.51758V12.5518C9.02435 13.3241 8.6142 13.7686 7.88965 13.7686C7.1651 13.7686 6.76178 13.3309 6.76172 12.5518V9.87207H3.33691V12.5518C3.33685 13.3241 2.9267 13.7686 2.20215 13.7686C1.4776 13.7686 1.07428 13.3309 1.07422 12.5518V5.51758C1.07422 4.73828 1.47754 4.30078 2.20215 4.30078C2.92676 4.30078 3.33691 4.74512 3.33691 5.51758V8.07422H6.76172V5.51758C6.76172 4.73828 7.16504 4.30078 7.88965 4.30078ZM14.2188 6.07812C13.6035 7.02841 12.2158 9.48929 11.7988 10.2686V10.3164H14.3281V6.07812H14.2188Z"/>
|
|
700
773
|
</svg>`,
|
|
701
774
|
|
|
@@ -710,44 +783,44 @@ var ToolbarIcons = {
|
|
|
710
783
|
</svg>`,
|
|
711
784
|
|
|
712
785
|
"paragraph":
|
|
713
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
786
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
714
787
|
<path d="M9 12C9.55228 12 10 12.4477 10 13C10 13.5523 9.55228 14 9 14H3C2.44772 14 2 13.5523 2 13C2 12.4477 2.44772 12 3 12H9ZM15 8C15.5523 8 16 8.44772 16 9C16 9.55228 15.5523 10 15 10H3C2.44772 10 2 9.55228 2 9C2 8.44772 2.44772 8 3 8H15ZM15 4C15.5523 4 16 4.44772 16 5C16 5.55228 15.5523 6 15 6H3C2.44772 6 2 5.55228 2 5C2 4.44772 2.44772 4 3 4H15Z"/>
|
|
715
788
|
</svg>`,
|
|
716
789
|
|
|
717
790
|
"clearFormatting":
|
|
718
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
791
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
719
792
|
<path d="M10.0607 2.07533C10.8417 1.29432 12.1078 1.2943 12.8888 2.07533L16.424 5.61049C17.205 6.39154 17.205 7.65854 16.424 8.43959L9.44937 15.4142C9.07435 15.7891 8.5656 16.0001 8.03531 16.0001H5.0148C4.55074 16.0001 4.10309 15.8385 3.74722 15.547L3.60074 15.4142L1.57534 13.3888C0.79431 12.6078 0.794336 11.3417 1.57534 10.5607L10.0607 2.07533ZM2.98941 11.9747L5.0148 14.0001H8.03531L9.71792 12.3165L6.18179 8.78139L2.98941 11.9747Z"/>
|
|
720
793
|
</svg>`,
|
|
721
794
|
|
|
722
795
|
"highlight":
|
|
723
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
796
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
724
797
|
<path d="M16.4564 14.4272C17.1356 15.5592 16.3204 17.0002 15.0003 17.0004C13.68 17.0004 12.864 15.5593 13.5433 14.4272L15.0003 12.0004L16.4564 14.4272ZM5.1214 1.70746C5.51192 1.31693 6.14494 1.31693 6.53546 1.70746L9.7171 4.8891L13.2532 8.42426C14.2295 9.40056 14.2295 10.9841 13.2532 11.9604L9.7171 15.4955C8.74078 16.4718 7.15822 16.4718 6.18195 15.4955L2.64679 11.9604C1.67048 10.9841 1.67048 9.40057 2.64679 8.42426L6.18195 4.8891C6.30299 4.76805 6.43323 4.66177 6.57062 4.57074L5.1214 3.12152C4.73091 2.73104 4.73099 2.09799 5.1214 1.70746ZM8.30304 6.30316C8.10776 6.10815 7.79119 6.10799 7.59601 6.30316L4.06085 9.83929L3.9964 9.91742C3.88661 10.0838 3.88645 10.3019 3.9964 10.4682L4.02277 10.5004H11.8763C12.0312 10.3043 12.02 10.0205 11.8392 9.83929L8.30304 6.30316Z"/>
|
|
725
798
|
</svg>`,
|
|
726
799
|
|
|
727
800
|
"link":
|
|
728
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
801
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
729
802
|
<path d="M12.8885 7.23091L13.9479 6.17155C14.5337 5.58576 14.5337 4.63602 13.9479 4.05023C13.3621 3.46444 12.4124 3.46444 11.8266 4.05023L8.29235 7.58446C7.9263 7.95051 7.90312 8.52994 8.2233 8.92271L8.36141 9.07463C8.68158 9.4674 8.65841 10.0468 8.29235 10.4129C7.90183 10.8034 7.26866 10.8034 6.87814 10.4129C5.70657 9.24131 5.70657 7.34182 6.87814 6.17025L10.4124 2.63602C11.7792 1.26918 13.9953 1.26918 15.3621 2.63602C16.729 4.00285 16.729 6.21893 15.3621 7.58576L14.3028 8.64512C13.9122 9.03564 13.2791 9.03564 12.8885 8.64512C12.498 8.2546 12.498 7.62143 12.8885 7.23091Z"/>
|
|
730
803
|
<path d="M5.11038 10.7664L4.04843 11.8284C3.46264 12.4142 3.46264 13.3639 4.04842 13.9497C4.63421 14.5355 5.58396 14.5355 6.16975 13.9497L9.70657 10.4129C10.0726 10.0468 10.0958 9.46741 9.77563 9.07464L9.63752 8.92272C9.31734 8.52995 9.34052 7.95052 9.70657 7.58446C10.0971 7.19394 10.7303 7.19394 11.1208 7.58446C12.2924 8.75604 12.2924 10.6555 11.1208 11.8271L7.58396 15.3639C6.21712 16.7308 4.00105 16.7308 2.63421 15.3639C1.26738 13.9971 1.26738 11.781 2.63421 10.4142L3.69617 9.35223C4.08669 8.96171 4.71986 8.96171 5.11038 9.35223C5.5009 9.74275 5.5009 10.3759 5.11038 10.7664Z"/>
|
|
731
804
|
</svg>`,
|
|
732
805
|
|
|
733
806
|
"quote":
|
|
734
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
807
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
735
808
|
<path d="M4.96387 4.23438C6.8769 4.23438 8.42767 5.78522 8.42773 7.69824C8.42773 8.32925 8.25769 8.92015 7.96289 9.42969L7.96387 9.43066L5.11816 14.3584C4.77659 14.95 4.02038 15.153 3.42871 14.8115C2.83701 14.4699 2.63397 13.7128 2.97559 13.1211L4.16113 11.0674C2.63532 10.7052 1.5 9.33485 1.5 7.69824C1.50006 5.78524 3.05086 4.2344 4.96387 4.23438ZM13.0361 4.23438C14.9491 4.23449 16.4999 5.7853 16.5 7.69824C16.5 8.32921 16.3299 8.92017 16.0352 9.42969L16.0361 9.43066L13.1904 14.3584C12.8488 14.9501 12.0917 15.1531 11.5 14.8115C10.9085 14.4698 10.7063 13.7127 11.0479 13.1211L12.2324 11.0674C10.7069 10.7049 9.57227 9.33461 9.57227 7.69824C9.57233 5.78522 11.1231 4.23438 13.0361 4.23438Z"/>
|
|
736
809
|
</svg>`,
|
|
737
810
|
|
|
738
811
|
"code":
|
|
739
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
812
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
740
813
|
<path d="M6.29289 3.79295C6.68342 3.40243 7.31643 3.40243 7.70696 3.79295C8.09748 4.18348 8.09748 4.81649 7.70696 5.20702L3.91399 8.99999L7.70696 12.793C8.09748 13.1835 8.09748 13.8165 7.70696 14.207C7.31643 14.5975 6.68342 14.5975 6.29289 14.207L1.79289 9.70702C1.40237 9.31649 1.40237 8.68348 1.79289 8.29295L6.29289 3.79295Z"/>
|
|
741
814
|
<path d="M11.707 3.79295C11.3164 3.40243 10.6834 3.40243 10.2929 3.79295C9.90237 4.18348 9.90237 4.81649 10.2929 5.20702L14.0859 8.99999L10.2929 12.793C9.90237 13.1835 9.90237 13.8165 10.2929 14.207C10.6834 14.5975 11.3164 14.5975 11.707 14.207L16.207 9.70702C16.5975 9.31649 16.5975 8.68348 16.207 8.29295L11.707 3.79295Z"/>
|
|
742
815
|
</svg>`,
|
|
743
816
|
|
|
744
817
|
"ul":
|
|
745
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
818
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
746
819
|
<path d="M3 12.5C3.82843 12.5 4.5 13.1716 4.5 14C4.5 14.8284 3.82843 15.5 3 15.5C2.17157 15.5 1.5 14.8284 1.5 14C1.5 13.1716 2.17157 12.5 3 12.5ZM15.5 13C16.0523 13 16.5 13.4477 16.5 14C16.5 14.5523 16.0523 15 15.5 15H7C6.44772 15 6 14.5523 6 14C6 13.4477 6.44772 13 7 13H15.5ZM3 7.5C3.82843 7.5 4.5 8.17157 4.5 9C4.5 9.82843 3.82843 10.5 3 10.5C2.17157 10.5 1.5 9.82843 1.5 9C1.5 8.17157 2.17157 7.5 3 7.5ZM15.5 8C16.0523 8 16.5 8.44772 16.5 9C16.5 9.55228 16.0523 10 15.5 10H7C6.44772 10 6 9.55228 6 9C6 8.44772 6.44772 8 7 8H15.5ZM3 2.5C3.82843 2.5 4.5 3.17157 4.5 4C4.5 4.82843 3.82843 5.5 3 5.5C2.17157 5.5 1.5 4.82843 1.5 4C1.5 3.17157 2.17157 2.5 3 2.5ZM15.5 3C16.0523 3 16.5 3.44772 16.5 4C16.5 4.55228 16.0523 5 15.5 5H7C6.44772 5 6 4.55228 6 4C6 3.44772 6.44772 3 7 3H15.5Z"/>
|
|
747
820
|
</svg>`,
|
|
748
821
|
|
|
749
822
|
"ol":
|
|
750
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
823
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
751
824
|
<path d="M15.5 13C16.0523 13 16.5 13.4477 16.5 14C16.5 14.5523 16.0523 15 15.5 15H7C6.44772 15 6 14.5523 6 14C6 13.4477 6.44772 13 7 13H15.5ZM15.5 8C16.0523 8 16.5 8.44772 16.5 9C16.5 9.55228 16.0523 10 15.5 10H7C6.44772 10 6 9.55228 6 9C6 8.44772 6.44772 8 7 8H15.5ZM15.5 3C16.0523 3 16.5 3.44772 16.5 4C16.5 4.55228 16.0523 5 15.5 5H7C6.44772 5 6 4.55228 6 4C6 3.44772 6.44772 3 7 3H15.5Z"/>
|
|
752
825
|
<path d="M2.98657 16.0967C2.68042 16.0967 2.41187 16.0465 2.18091 15.9463C1.95174 15.846 1.77002 15.7046 1.63574 15.522C1.50146 15.3376 1.42448 15.1227 1.40479 14.8774L1.4021 14.8452H2.34204L2.34741 14.8748C2.35815 14.9589 2.39038 15.035 2.44409 15.103C2.49959 15.1711 2.5721 15.2248 2.66162 15.2642C2.75293 15.3035 2.86035 15.3232 2.98389 15.3232C3.10563 15.3232 3.21037 15.3027 3.2981 15.2615C3.38761 15.2185 3.45654 15.1603 3.50488 15.0869C3.55322 15.0135 3.57739 14.9294 3.57739 14.8345V14.8291C3.57739 14.6715 3.51921 14.5516 3.40283 14.4692C3.28646 14.3869 3.12085 14.3457 2.90601 14.3457H2.48706V13.677H2.90063C3.02775 13.677 3.13607 13.6582 3.22559 13.6206C3.31689 13.583 3.38672 13.5302 3.43506 13.4622C3.48519 13.3941 3.51025 13.3153 3.51025 13.2258V13.2205C3.51025 13.1256 3.48877 13.0441 3.4458 12.9761C3.40462 12.9062 3.34375 12.8534 3.26318 12.8176C3.18441 12.78 3.08952 12.7612 2.97852 12.7612C2.86572 12.7612 2.76636 12.7809 2.68042 12.8203C2.59627 12.8579 2.52913 12.9125 2.479 12.9841C2.43066 13.054 2.40112 13.1363 2.39038 13.2312L2.3877 13.2581H1.49341L1.49609 13.2205C1.514 12.977 1.58561 12.7666 1.71094 12.5894C1.83805 12.4103 2.00903 12.2725 2.22388 12.1758C2.44051 12.0773 2.69206 12.0281 2.97852 12.0281C3.27393 12.0281 3.52995 12.0728 3.74658 12.1624C3.96322 12.2501 4.13062 12.3727 4.24878 12.5303C4.36694 12.6878 4.42603 12.8722 4.42603 13.0835V13.0889C4.42603 13.2518 4.38932 13.3941 4.31592 13.5159C4.2443 13.6358 4.14762 13.7343 4.02588 13.8113C3.90592 13.8883 3.77254 13.942 3.62573 13.9724V13.9912C3.91756 14.0199 4.14941 14.1121 4.32129 14.2678C4.49316 14.4236 4.5791 14.6295 4.5791 14.8855V14.8909C4.5791 15.1344 4.51375 15.3474 4.38306 15.53C4.25236 15.7109 4.06795 15.8505 3.82983 15.949C3.59172 16.0474 3.31063 16.0967 2.98657 16.0967Z"/>
|
|
753
826
|
<path d="M1.54443 11V10.342L2.76099 9.20874C2.95076 9.03507 3.09757 8.89274 3.20142 8.78174C3.30705 8.66895 3.37956 8.57316 3.41895 8.49438C3.46012 8.41382 3.48071 8.33415 3.48071 8.25537V8.24463C3.48071 8.14795 3.46012 8.0638 3.41895 7.99219C3.37777 7.92057 3.31779 7.86507 3.23901 7.82568C3.16024 7.7863 3.06714 7.7666 2.95972 7.7666C2.84692 7.7666 2.74756 7.78988 2.66162 7.83643C2.57747 7.88298 2.51123 7.94743 2.46289 8.02979C2.41455 8.11035 2.39038 8.20345 2.39038 8.30908V8.33057L1.48804 8.32788V8.31177C1.48804 8.05396 1.5507 7.82837 1.67603 7.63501C1.80314 7.44165 1.97949 7.29126 2.20508 7.18384C2.43245 7.07463 2.69653 7.02002 2.99731 7.02002C3.28556 7.02002 3.53711 7.06836 3.75195 7.16504C3.96859 7.25993 4.13688 7.39331 4.25684 7.56519C4.37858 7.73706 4.43945 7.93758 4.43945 8.16675V8.18018C4.43945 8.3252 4.40902 8.46932 4.34814 8.61255C4.28727 8.75578 4.18701 8.90885 4.04736 9.07178C3.90771 9.23291 3.71883 9.41642 3.48071 9.62231L2.58374 10.4092L2.85498 9.98486V10.4092L2.58374 10.2319H4.49048V11H1.54443Z"/>
|
|
@@ -755,37 +828,37 @@ var ToolbarIcons = {
|
|
|
755
828
|
</svg>`,
|
|
756
829
|
|
|
757
830
|
"image":
|
|
758
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
831
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
759
832
|
<path d="M14 2C15.6569 2 17 3.34315 17 5V13C17 14.6569 15.6569 16 14 16H4C2.34315 16 1 14.6569 1 13V5C1 3.34315 2.34315 2 4 2H14ZM3.06348 13.3496C3.2053 13.7294 3.57078 14 4 14H13.5859L11 11.4141L9.70703 12.707C9.31651 13.0976 8.68349 13.0976 8.29297 12.707C7.90244 12.3165 7.90244 11.6835 8.29297 11.293L8.58594 11L7 9.41406L3.06348 13.3496ZM4 4C3.44772 4 3 4.44772 3 5V10.5859L6.29297 7.29297L6.36914 7.22461C6.76191 6.90427 7.34092 6.92686 7.70703 7.29297L10 9.58594L10.293 9.29297L10.3691 9.22461C10.7619 8.90427 11.3409 8.92686 11.707 9.29297L15 12.5859V5C15 4.44772 14.5523 4 14 4H4ZM12.5 5C13.3284 5 14 5.67157 14 6.5C14 7.32843 13.3284 8 12.5 8C11.6716 8 11 7.32843 11 6.5C11 5.67157 11.6716 5 12.5 5Z"/>
|
|
760
833
|
</svg>`,
|
|
761
834
|
|
|
762
835
|
"attachment":
|
|
763
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
836
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
764
837
|
<path d="M13 13.5V6C13 4.067 11.433 2.5 9.5 2.5C7.567 2.5 6 4.067 6 6V13.5C6 14.6046 6.89543 15.5 8 15.5H8.23047C9.20759 15.5 10 14.7076 10 13.7305V7C10 6.72386 9.77614 6.5 9.5 6.5C9.22386 6.5 9 6.72386 9 7V12.5C9 13.0523 8.55228 13.5 8 13.5C7.44772 13.5 7 13.0523 7 12.5V7C7 5.61929 8.11929 4.5 9.5 4.5C10.8807 4.5 12 5.61929 12 7V13.7305C12 15.8122 10.3122 17.5 8.23047 17.5H8C5.79086 17.5 4 15.7091 4 13.5V6C4 2.96243 6.46243 0.5 9.5 0.5C12.5376 0.5 15 2.96243 15 6V13.5C15 14.0523 14.5523 14.5 14 14.5C13.4477 14.5 13 14.0523 13 13.5Z"/>
|
|
765
838
|
</svg>`,
|
|
766
839
|
|
|
767
840
|
"table":
|
|
768
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
841
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
769
842
|
<path d="M15 1C16.1046 1 17 1.89543 17 3V15C17 16.1046 16.1046 17 15 17H3C1.89543 17 1 16.1046 1 15V3C1 1.89543 1.89543 1 3 1H15ZM3 15H8V10H3V15ZM10 10V15H15V10H10ZM10 8H15V3H10V8ZM3 8H8V3H3V8Z"/>
|
|
770
843
|
</svg>`,
|
|
771
844
|
|
|
772
845
|
"hr":
|
|
773
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
846
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
774
847
|
<path d="M12.75 12C13.1642 12 13.5 12.3358 13.5 12.75V14.25C13.5 14.6642 13.1642 15 12.75 15H5.25C4.83579 15 4.5 14.6642 4.5 14.25V12.75C4.5 12.3358 4.83579 12 5.25 12H12.75ZM15.4863 8C16.0461 8 16.5 8.44771 16.5 9C16.5 9.55229 16.0461 10 15.4863 10H2.51367C1.95392 10 1.5 9.55229 1.5 9C1.5 8.44771 1.95392 8 2.51367 8H15.4863ZM12.75 3C13.1642 3 13.5 3.33579 13.5 3.75V5.25C13.5 5.66421 13.1642 6 12.75 6H5.25C4.83579 6 4.5 5.66421 4.5 5.25V3.75C4.5 3.33579 4.83579 3 5.25 3H12.75Z"/>
|
|
775
848
|
</svg>`,
|
|
776
849
|
|
|
777
850
|
"undo":
|
|
778
|
-
`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
851
|
+
`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
779
852
|
<path d="M8.36612 5.36612C8.85427 4.87796 9.64554 4.87796 10.1337 5.36612C10.6218 5.85428 10.6218 6.64557 10.1337 7.13369L7.26748 9.9999H15.2499C18.1494 9.99996 20.4999 12.3504 20.4999 15.2499V19.2499C20.4999 19.9402 19.9402 20.4999 19.2499 20.4999C18.5596 20.4999 18 19.9402 17.9999 19.2499V15.2499C17.9999 13.7312 16.7686 12.5 15.2499 12.4999H7.26748L10.1337 15.3661C10.6218 15.8543 10.6218 16.6456 10.1337 17.1337C9.64557 17.6218 8.85428 17.6218 8.36612 17.1337L3.36612 12.1337C2.87796 11.6455 2.87796 10.8543 3.36612 10.3661L8.36612 5.36612Z"/>
|
|
780
853
|
</svg>`,
|
|
781
854
|
|
|
782
855
|
"redo":
|
|
783
|
-
`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
856
|
+
`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
784
857
|
<path d="M15.6338 5.1163C15.1456 4.62814 14.3543 4.62814 13.8662 5.1163C13.3781 5.60446 13.3781 6.39575 13.8662 6.88388L16.7324 9.75009H8.74997C5.85052 9.75014 3.49997 12.1006 3.49997 15.0001V19.0001C3.50002 19.6904 4.05969 20.25 4.74997 20.2501C5.4403 20.2501 5.99992 19.6904 5.99997 19.0001V15.0001C5.99997 13.4813 7.23123 12.2501 8.74997 12.2501H16.7324L13.8662 15.1163C13.3781 15.6045 13.3781 16.3958 13.8662 16.8839C14.3543 17.372 15.1456 17.3719 15.6338 16.8839L20.6338 11.8839C21.1219 11.3957 21.1219 10.6045 20.6338 10.1163L15.6338 5.1163Z" />
|
|
785
858
|
</svg>`,
|
|
786
859
|
|
|
787
860
|
"overflow":
|
|
788
|
-
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
861
|
+
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
789
862
|
<path d="M3 6.75C4.24264 6.75 5.25 7.75736 5.25 9C5.25 10.2426 4.24264 11.25 3 11.25C1.75736 11.25 0.75 10.2426 0.75 9C0.75 7.75736 1.75736 6.75 3 6.75ZM9 6.75C10.2426 6.75 11.25 7.75736 11.25 9C11.25 10.2426 10.2426 11.25 9 11.25C7.75736 11.25 6.75 10.2426 6.75 9C6.75 7.75736 7.75736 6.75 9 6.75ZM15 6.75C16.2426 6.75 17.25 7.75736 17.25 9C17.25 10.2426 16.2426 11.25 15 11.25C13.7574 11.25 12.75 10.2426 12.75 9C12.75 7.75736 13.7574 6.75 15 6.75Z"/>
|
|
790
863
|
</svg>`
|
|
791
864
|
};
|
|
@@ -912,14 +985,21 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
912
985
|
}
|
|
913
986
|
}
|
|
914
987
|
|
|
915
|
-
#dispatchButtonCommand(event,
|
|
916
|
-
|
|
988
|
+
#dispatchButtonCommand(event, button) {
|
|
989
|
+
if (button.ariaDisabled === "true") return
|
|
990
|
+
|
|
991
|
+
const { command, payload } = button.dataset;
|
|
992
|
+
const shouldKeepToolbarFocus = isKeyboardActivation(event) && !this.#belongsToDropdown(button);
|
|
917
993
|
|
|
918
994
|
this.editor.update(() => {
|
|
919
995
|
this.editor.dispatchCommand(command, payload);
|
|
920
|
-
}, { tag:
|
|
996
|
+
}, { tag: shouldKeepToolbarFocus ? SKIP_DOM_SELECTION_TAG : undefined });
|
|
997
|
+
|
|
998
|
+
if (!shouldKeepToolbarFocus) this.editor.focus();
|
|
999
|
+
}
|
|
921
1000
|
|
|
922
|
-
|
|
1001
|
+
#belongsToDropdown(button) {
|
|
1002
|
+
return button.closest("[data-dropdown-panel]") != null
|
|
923
1003
|
}
|
|
924
1004
|
|
|
925
1005
|
#bindHotkeys() {
|
|
@@ -959,8 +1039,8 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
959
1039
|
}
|
|
960
1040
|
|
|
961
1041
|
#handleEditorFocus = () => {
|
|
962
|
-
const
|
|
963
|
-
if (
|
|
1042
|
+
const firstVisibleButton = this.#toolbarButtons.find(isActiveAndVisible);
|
|
1043
|
+
if (firstVisibleButton) firstVisibleButton.tabIndex = 0;
|
|
964
1044
|
}
|
|
965
1045
|
|
|
966
1046
|
#handleEditorBlur = () => {
|
|
@@ -968,11 +1048,11 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
968
1048
|
}
|
|
969
1049
|
|
|
970
1050
|
#handleKeydown = (event) => {
|
|
971
|
-
handleRollingTabIndex(this.#
|
|
1051
|
+
handleRollingTabIndex(this.#toolbarButtons, event);
|
|
972
1052
|
}
|
|
973
1053
|
|
|
974
1054
|
#resetTabIndexValues() {
|
|
975
|
-
this.#
|
|
1055
|
+
this.#toolbarButtons.forEach((button) => {
|
|
976
1056
|
button.tabIndex = -1;
|
|
977
1057
|
});
|
|
978
1058
|
}
|
|
@@ -1009,7 +1089,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
1009
1089
|
this.#setButtonPressed("underline", isUnderline);
|
|
1010
1090
|
|
|
1011
1091
|
this.#setButtonPressed("format", isInHeading);
|
|
1012
|
-
this.#
|
|
1092
|
+
this.#setButtonChecked("paragraph", !isInHeading);
|
|
1013
1093
|
|
|
1014
1094
|
this.querySelector("lexxy-heading-dropdown")
|
|
1015
1095
|
?.updateActiveHeading(headingTag);
|
|
@@ -1028,25 +1108,17 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
1028
1108
|
|
|
1029
1109
|
#setButtonPressed(name, isPressed) {
|
|
1030
1110
|
const button = this.querySelector(`[name="${name}"]`);
|
|
1031
|
-
if (button)
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1111
|
+
if (button) button.ariaPressed = isPressed;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
#setButtonChecked(name, isChecked) {
|
|
1115
|
+
const button = this.querySelector(`[name="${name}"]`);
|
|
1116
|
+
if (button) button.ariaChecked = isChecked;
|
|
1037
1117
|
}
|
|
1038
1118
|
|
|
1039
1119
|
#setButtonDisabled(name, isDisabled) {
|
|
1040
1120
|
const button = this.querySelector(`[name="${name}"]`);
|
|
1041
|
-
if (button)
|
|
1042
|
-
if (button.disabled !== isDisabled) {
|
|
1043
|
-
button.disabled = isDisabled;
|
|
1044
|
-
}
|
|
1045
|
-
const next = isDisabled.toString();
|
|
1046
|
-
if (button.getAttribute("aria-disabled") !== next) {
|
|
1047
|
-
button.setAttribute("aria-disabled", next);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1121
|
+
if (button) button.ariaDisabled = isDisabled;
|
|
1050
1122
|
}
|
|
1051
1123
|
|
|
1052
1124
|
#refreshOverflow() {
|
|
@@ -1147,8 +1219,10 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
1147
1219
|
return Array.from(this.querySelectorAll(":scope > button:not([data-prevent-overflow])"))
|
|
1148
1220
|
}
|
|
1149
1221
|
|
|
1150
|
-
get #
|
|
1151
|
-
return Array.from(this.querySelectorAll(":scope button"))
|
|
1222
|
+
get #toolbarButtons() {
|
|
1223
|
+
return Array.from(this.querySelectorAll(":scope button")).filter((button) => {
|
|
1224
|
+
return !button.closest("[data-dropdown-panel]")
|
|
1225
|
+
})
|
|
1152
1226
|
}
|
|
1153
1227
|
|
|
1154
1228
|
get #toolbarItems() {
|
|
@@ -1187,8 +1261,8 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
1187
1261
|
<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">
|
|
1188
1262
|
${ToolbarIcons.heading}
|
|
1189
1263
|
</button>
|
|
1190
|
-
<div data-dropdown-panel role="menu" class="lexxy-editor__toolbar-dropdown-list" hidden>
|
|
1191
|
-
<button type="button" name="paragraph" data-command="setFormatParagraph" title="Paragraph" role="
|
|
1264
|
+
<div data-dropdown-panel role="menu" aria-label="Text formatting" class="lexxy-editor__toolbar-dropdown-list" hidden>
|
|
1265
|
+
<button type="button" name="paragraph" data-command="setFormatParagraph" title="Paragraph" role="menuitemradio">
|
|
1192
1266
|
${ToolbarIcons.paragraph} <span>Normal</span>
|
|
1193
1267
|
</button>
|
|
1194
1268
|
<lexxy-heading-dropdown>
|
|
@@ -1205,7 +1279,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
1205
1279
|
<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">
|
|
1206
1280
|
${ToolbarIcons.highlight}
|
|
1207
1281
|
</button>
|
|
1208
|
-
<div data-dropdown-panel role="menu" hidden>
|
|
1282
|
+
<div data-dropdown-panel role="menu" aria-label="Color highlight" hidden>
|
|
1209
1283
|
<div class="lexxy-highlight-colors"></div>
|
|
1210
1284
|
<button data-command="removeHighlight" type="button" class="lexxy-editor__toolbar-button lexxy-editor__toolbar-dropdown-reset" role="menuitem">Remove all coloring</button>
|
|
1211
1285
|
</div>
|
|
@@ -1247,19 +1321,19 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
1247
1321
|
${ToolbarIcons.hr}
|
|
1248
1322
|
</button>
|
|
1249
1323
|
|
|
1250
|
-
<button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-button--push-right" type="button" name="undo" data-command="undo" title="Undo"
|
|
1324
|
+
<button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-button--push-right" type="button" name="undo" data-command="undo" title="Undo" aria-disabled="true">
|
|
1251
1325
|
${ToolbarIcons.undo}
|
|
1252
1326
|
</button>
|
|
1253
1327
|
|
|
1254
|
-
<button class="lexxy-editor__toolbar-button" type="button" name="redo" data-command="redo" title="Redo"
|
|
1328
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="redo" data-command="redo" title="Redo" aria-disabled="true">
|
|
1255
1329
|
${ToolbarIcons.redo}
|
|
1256
1330
|
</button>
|
|
1257
1331
|
|
|
1258
1332
|
<lexxy-toolbar-dropdown class="lexxy-editor__toolbar-dropdown lexxy-editor__toolbar-button--push-right lexxy-editor__toolbar-overflow">
|
|
1259
|
-
<button data-dropdown-trigger class="lexxy-editor__toolbar-button" type="button" aria-haspopup="menu" aria-expanded="false"
|
|
1333
|
+
<button data-dropdown-trigger class="lexxy-editor__toolbar-button" type="button" title="More options" aria-haspopup="menu" aria-expanded="false">
|
|
1260
1334
|
${ToolbarIcons.overflow}
|
|
1261
1335
|
</button>
|
|
1262
|
-
<div data-dropdown-panel role="menu" class="lexxy-editor__toolbar-overflow-menu" aria-label="More
|
|
1336
|
+
<div data-dropdown-panel role="menu" class="lexxy-editor__toolbar-overflow-menu" aria-label="More options" hidden></div>
|
|
1263
1337
|
</lexxy-toolbar-dropdown>
|
|
1264
1338
|
`
|
|
1265
1339
|
}
|
|
@@ -1303,9 +1377,10 @@ function nextFrame() {
|
|
|
1303
1377
|
|
|
1304
1378
|
class ToolbarDropdown extends HTMLElement {
|
|
1305
1379
|
#listeners = new ListenerBin()
|
|
1380
|
+
#shouldReturnFocusToTrigger = false
|
|
1306
1381
|
|
|
1307
1382
|
connectedCallback() {
|
|
1308
|
-
this.#
|
|
1383
|
+
this.#onHostEditor(() => {
|
|
1309
1384
|
this.#registerListeners();
|
|
1310
1385
|
this.editorReady();
|
|
1311
1386
|
});
|
|
@@ -1327,16 +1402,16 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
1327
1402
|
return this.querySelector(":scope > [data-dropdown-panel]")
|
|
1328
1403
|
}
|
|
1329
1404
|
|
|
1330
|
-
get
|
|
1331
|
-
return this.closest("lexxy-toolbar")
|
|
1405
|
+
get host() {
|
|
1406
|
+
return this.closest("lexxy-toolbar, lexxy-table-tools")
|
|
1332
1407
|
}
|
|
1333
1408
|
|
|
1334
1409
|
get editorElement() {
|
|
1335
|
-
return this.
|
|
1410
|
+
return this.host?.editorElement
|
|
1336
1411
|
}
|
|
1337
1412
|
|
|
1338
1413
|
get editor() {
|
|
1339
|
-
return this.
|
|
1414
|
+
return this.host?.editor
|
|
1340
1415
|
}
|
|
1341
1416
|
|
|
1342
1417
|
get isOpen() {
|
|
@@ -1353,7 +1428,7 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
1353
1428
|
|
|
1354
1429
|
open() {
|
|
1355
1430
|
if (this.isOpen) return
|
|
1356
|
-
this.trigger.
|
|
1431
|
+
this.trigger.ariaExpanded = true;
|
|
1357
1432
|
this.panel.hidden = false;
|
|
1358
1433
|
this.onOpen();
|
|
1359
1434
|
this.#focusFirstInteractive();
|
|
@@ -1363,7 +1438,7 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
1363
1438
|
if (focusEditor) this.editor?.focus();
|
|
1364
1439
|
|
|
1365
1440
|
if (this.isClosed) return
|
|
1366
|
-
this.trigger.
|
|
1441
|
+
this.trigger.ariaExpanded = false;
|
|
1367
1442
|
this.panel.hidden = true;
|
|
1368
1443
|
this.onClose();
|
|
1369
1444
|
}
|
|
@@ -1375,32 +1450,49 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
1375
1450
|
);
|
|
1376
1451
|
}
|
|
1377
1452
|
|
|
1378
|
-
#handleTriggerClick = () => {
|
|
1453
|
+
#handleTriggerClick = (event) => {
|
|
1379
1454
|
if (this.isOpen) {
|
|
1380
1455
|
this.close({ focusEditor: false });
|
|
1381
1456
|
} else {
|
|
1382
|
-
this
|
|
1457
|
+
this.#shouldReturnFocusToTrigger = this.#isOpenedFromHost(event);
|
|
1458
|
+
this.host?.closeDropdowns({ except: this });
|
|
1383
1459
|
this.open();
|
|
1384
1460
|
}
|
|
1385
1461
|
}
|
|
1386
1462
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1463
|
+
#isOpenedFromHost(event) {
|
|
1464
|
+
return isKeyboardActivation(event) && this.host?.contains(document.activeElement)
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
async #onHostEditor(callback) {
|
|
1468
|
+
if (!this.host) return
|
|
1389
1469
|
|
|
1390
|
-
await this.
|
|
1391
|
-
if (this.isConnected && this.
|
|
1470
|
+
await this.host.getEditorElement();
|
|
1471
|
+
if (this.isConnected && this.host) callback();
|
|
1392
1472
|
}
|
|
1393
1473
|
|
|
1394
1474
|
#handleKeyDown = (event) => {
|
|
1395
|
-
if (event.key === "Escape") {
|
|
1475
|
+
if (event.key === "Escape" && this.isOpen) {
|
|
1476
|
+
event.stopPropagation();
|
|
1477
|
+
this.close({ focusEditor: !this.#shouldReturnFocusToTrigger });
|
|
1478
|
+
if (this.#shouldReturnFocusToTrigger) this.trigger?.focus();
|
|
1479
|
+
} else if (this.isOpen && this.panel.role === "dialog") {
|
|
1480
|
+
trapFocusAtTabBoundary(this.panel, event);
|
|
1481
|
+
} else if (this.#isNavigatingMenu(event)) {
|
|
1396
1482
|
event.stopPropagation();
|
|
1397
|
-
this
|
|
1483
|
+
handleRollingTabIndex(this.#buttons, event, { orientation: "both", wrap: true });
|
|
1398
1484
|
}
|
|
1399
1485
|
}
|
|
1400
1486
|
|
|
1487
|
+
#isNavigatingMenu(event) {
|
|
1488
|
+
return this.panel.role === "menu" && this.panel.contains(event.target)
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1401
1491
|
async #focusFirstInteractive() {
|
|
1402
|
-
|
|
1403
|
-
|
|
1492
|
+
// Ask for the ring explicitly: opening the menu with the mouse otherwise leaves the
|
|
1493
|
+
// first item focused without a focus ring, since focus() inherits the mouse modality.
|
|
1494
|
+
this.#interactiveElements[0]?.focus({ focusVisible: true });
|
|
1495
|
+
if (this.panel.role === "menu") await this.#resetTabIndexValues();
|
|
1404
1496
|
}
|
|
1405
1497
|
|
|
1406
1498
|
async #resetTabIndexValues() {
|
|
@@ -1482,8 +1574,8 @@ class HeadingDropdown extends HTMLElement {
|
|
|
1482
1574
|
updateActiveHeading(tag) {
|
|
1483
1575
|
this.#headingButtons.forEach(button => {
|
|
1484
1576
|
const next = (button.dataset.heading === tag).toString();
|
|
1485
|
-
if (button.getAttribute("aria-
|
|
1486
|
-
button.setAttribute("aria-
|
|
1577
|
+
if (button.getAttribute("aria-checked") !== next) {
|
|
1578
|
+
button.setAttribute("aria-checked", next);
|
|
1487
1579
|
}
|
|
1488
1580
|
});
|
|
1489
1581
|
}
|
|
@@ -1528,7 +1620,7 @@ class HeadingDropdown extends HTMLElement {
|
|
|
1528
1620
|
button.classList.add("lexxy-heading-button");
|
|
1529
1621
|
button.name = name;
|
|
1530
1622
|
button.title = label;
|
|
1531
|
-
button.setAttribute("role", "
|
|
1623
|
+
button.setAttribute("role", "menuitemradio");
|
|
1532
1624
|
button.innerHTML = `${icon} <span>${label}</span>`;
|
|
1533
1625
|
return button
|
|
1534
1626
|
}
|
|
@@ -1563,6 +1655,143 @@ class HeadingDropdown extends HTMLElement {
|
|
|
1563
1655
|
}
|
|
1564
1656
|
}
|
|
1565
1657
|
|
|
1658
|
+
// Shared, strictly-contained element used to attach ephemeral nodes when we
|
|
1659
|
+
// need to read computed styles (e.g. canonicalizing style values, resolving
|
|
1660
|
+
// CSS custom properties). The container is created once and attached to
|
|
1661
|
+
// `document.body` once; subsequent child mutations happen *inside* the
|
|
1662
|
+
// contained subtree so they do not invalidate style on the rest of the page.
|
|
1663
|
+
//
|
|
1664
|
+
// Without this, `document.body.appendChild(...)` / `element.remove()` calls
|
|
1665
|
+
// forced the browser to re-evaluate every ancestor-dependent selector (`:has()`,
|
|
1666
|
+
// descendant combinators, universal sibling rules) across the document on each
|
|
1667
|
+
// invocation — a 13,000+ element style recalc per call on a typical Basecamp
|
|
1668
|
+
// page.
|
|
1669
|
+
|
|
1670
|
+
let resolverRoot = null;
|
|
1671
|
+
|
|
1672
|
+
function styleResolverRoot() {
|
|
1673
|
+
if (resolverRoot && resolverRoot.isConnected) return resolverRoot
|
|
1674
|
+
|
|
1675
|
+
resolverRoot = document.createElement("div");
|
|
1676
|
+
resolverRoot.setAttribute("aria-hidden", "true");
|
|
1677
|
+
resolverRoot.setAttribute("data-lexxy-style-resolver", "");
|
|
1678
|
+
// `contain: strict` (size, layout, paint, style) isolates everything.
|
|
1679
|
+
// The root itself paints nothing (visibility hidden), has zero
|
|
1680
|
+
// geometric impact (position fixed, intrinsic size via contain), and
|
|
1681
|
+
// never leaks style invalidation to its ancestors.
|
|
1682
|
+
resolverRoot.style.cssText = "contain: strict; position: fixed; top: 0; left: 0; visibility: hidden; pointer-events: none; width: 0; height: 0;";
|
|
1683
|
+
document.body.appendChild(resolverRoot);
|
|
1684
|
+
return resolverRoot
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function isSelectionHighlighted(selection) {
|
|
1688
|
+
if (!$isRangeSelection(selection)) return false
|
|
1689
|
+
|
|
1690
|
+
if (selection.isCollapsed()) {
|
|
1691
|
+
return hasHighlightStyles(selection.style)
|
|
1692
|
+
} else {
|
|
1693
|
+
return selection.hasFormat("highlight")
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function getHighlightStyles(selection) {
|
|
1698
|
+
if (!$isRangeSelection(selection)) return null
|
|
1699
|
+
|
|
1700
|
+
let styles = getStyleObjectFromCSS(selection.style);
|
|
1701
|
+
if (!styles.color && !styles["background-color"]) {
|
|
1702
|
+
const anchorNode = selection.anchor.getNode();
|
|
1703
|
+
if ($isTextNode(anchorNode)) {
|
|
1704
|
+
styles = getStyleObjectFromCSS(anchorNode.getStyle());
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
const color = styles.color || null;
|
|
1709
|
+
const backgroundColor = styles["background-color"] || null;
|
|
1710
|
+
if (!color && !backgroundColor) return null
|
|
1711
|
+
|
|
1712
|
+
return { color, backgroundColor }
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function hasHighlightStyles(cssOrStyles) {
|
|
1716
|
+
const styles = typeof cssOrStyles === "string" ? getStyleObjectFromCSS(cssOrStyles) : cssOrStyles;
|
|
1717
|
+
return !!(styles.color || styles["background-color"])
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function normalizeColorButtons(buttons) {
|
|
1721
|
+
return buttons.map((button) => (typeof button === "string" ? { value: button, label: button } : button))
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function applyCanonicalizers(styles, canonicalizers = []) {
|
|
1725
|
+
return canonicalizers.reduce((css, canonicalizer) => {
|
|
1726
|
+
return canonicalizer.applyCanonicalization(css)
|
|
1727
|
+
}, styles)
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
class StyleCanonicalizer {
|
|
1731
|
+
constructor(property, allowedValues= []) {
|
|
1732
|
+
this._property = property;
|
|
1733
|
+
this._allowedValues = allowedValues;
|
|
1734
|
+
this._canonicalValues = this.#allowedValuesIdentityObject;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
applyCanonicalization(css) {
|
|
1738
|
+
const styles = { ...getStyleObjectFromCSS(css) };
|
|
1739
|
+
|
|
1740
|
+
styles[this._property] = this.getCanonicalAllowedValue(styles[this._property]);
|
|
1741
|
+
if (!styles[this._property]) {
|
|
1742
|
+
delete styles[this._property];
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
return getCSSFromStyleObject(styles)
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
getCanonicalAllowedValue(value) {
|
|
1749
|
+
return this._canonicalValues[value] ||= this.#resolveCannonicalValue(value)
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Private
|
|
1753
|
+
|
|
1754
|
+
get #allowedValuesIdentityObject() {
|
|
1755
|
+
return this._allowedValues.reduce((object, value) => ({ ...object, [value]: value }), {})
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
#resolveCannonicalValue(value) {
|
|
1759
|
+
let index = this.#computedAllowedValues.indexOf(value);
|
|
1760
|
+
if (index === -1) {
|
|
1761
|
+
index = this.#computedAllowedValues.indexOf(computeStyleValues(this._property, [ value ])[0]);
|
|
1762
|
+
}
|
|
1763
|
+
return index === -1 ? null : this._allowedValues[index]
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
get #computedAllowedValues() {
|
|
1767
|
+
return this._computedAllowedValues ||= computeStyleValues(this._property, this._allowedValues)
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// Separates DOM writes from layout reads to avoid forced reflows, and attaches
|
|
1772
|
+
// resolver elements to a strictly-contained root (outside the normal document
|
|
1773
|
+
// flow) so neither the attach nor the detach invalidate styles on the rest of
|
|
1774
|
+
// the page. Without containment, appending to `document.body` triggered a
|
|
1775
|
+
// page-wide style recalc on every canonicalization pass.
|
|
1776
|
+
function computeStyleValues(property, values) {
|
|
1777
|
+
const fragment = document.createDocumentFragment();
|
|
1778
|
+
|
|
1779
|
+
const elements = values.map(value => {
|
|
1780
|
+
const element = createElement("span", { style: `display: none; ${property}: ${value};` });
|
|
1781
|
+
fragment.appendChild(element);
|
|
1782
|
+
return element
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
styleResolverRoot().appendChild(fragment);
|
|
1786
|
+
|
|
1787
|
+
const computed = elements.map(element =>
|
|
1788
|
+
window.getComputedStyle(element).getPropertyValue(property)
|
|
1789
|
+
);
|
|
1790
|
+
|
|
1791
|
+
elements.forEach(element => element.remove());
|
|
1792
|
+
return computed
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1566
1795
|
const APPLY_HIGHLIGHT_SELECTOR = "button.lexxy-highlight-button";
|
|
1567
1796
|
const REMOVE_HIGHLIGHT_SELECTOR = "[data-command='removeHighlight']";
|
|
1568
1797
|
|
|
@@ -1594,27 +1823,36 @@ class HighlightDropdown extends ToolbarDropdown {
|
|
|
1594
1823
|
|
|
1595
1824
|
const colorGroups = this.editorElement.config.get("highlight.buttons");
|
|
1596
1825
|
|
|
1597
|
-
this.#
|
|
1598
|
-
this.#
|
|
1826
|
+
this.#appendColorGroup("Text color", "color", colorGroups.color);
|
|
1827
|
+
this.#appendColorGroup("Background color", "background-color", colorGroups["background-color"]);
|
|
1599
1828
|
|
|
1600
1829
|
const maxNumberOfColors = Math.max(colorGroups.color.length, colorGroups["background-color"].length);
|
|
1601
1830
|
this.panel.style.setProperty("--max-colors", maxNumberOfColors);
|
|
1602
1831
|
}
|
|
1603
1832
|
|
|
1604
|
-
#
|
|
1605
|
-
|
|
1606
|
-
|
|
1833
|
+
#appendColorGroup(label, attribute, buttons) {
|
|
1834
|
+
const group = createElement("div", {
|
|
1835
|
+
role: "group",
|
|
1836
|
+
class: "lexxy-highlight-colors__group",
|
|
1837
|
+
"aria-label": label
|
|
1838
|
+
});
|
|
1839
|
+
|
|
1840
|
+
normalizeColorButtons(buttons).forEach((button, index) => {
|
|
1841
|
+
group.appendChild(this.#createButton(attribute, button, index));
|
|
1607
1842
|
});
|
|
1843
|
+
|
|
1844
|
+
this.#buttonContainer.append(group);
|
|
1608
1845
|
}
|
|
1609
1846
|
|
|
1610
|
-
#createButton(attribute,
|
|
1847
|
+
#createButton(attribute, button, index) {
|
|
1611
1848
|
return createElement("button", {
|
|
1612
1849
|
type: "button",
|
|
1613
|
-
dataset: { value, style: attribute },
|
|
1614
|
-
style: `${attribute}: ${value}`,
|
|
1850
|
+
dataset: { value: button.value, style: attribute },
|
|
1851
|
+
style: `${attribute}: ${button.value}`,
|
|
1615
1852
|
class: "lexxy-editor__toolbar-button lexxy-highlight-button",
|
|
1616
1853
|
name: `${attribute}-${index}`,
|
|
1617
|
-
role: "
|
|
1854
|
+
role: "menuitemcheckbox",
|
|
1855
|
+
"aria-label": button.label
|
|
1618
1856
|
})
|
|
1619
1857
|
}
|
|
1620
1858
|
|
|
@@ -1638,11 +1876,7 @@ class HighlightDropdown extends ToolbarDropdown {
|
|
|
1638
1876
|
const backgroundColor = $getSelectionStyleValueForProperty(selection, "background-color", NO_STYLE);
|
|
1639
1877
|
|
|
1640
1878
|
this.#colorButtons.forEach(button => {
|
|
1641
|
-
|
|
1642
|
-
const next = matchesSelection.toString();
|
|
1643
|
-
if (button.getAttribute("aria-pressed") !== next) {
|
|
1644
|
-
button.setAttribute("aria-pressed", next);
|
|
1645
|
-
}
|
|
1879
|
+
button.ariaChecked = button.dataset.value === textColor || button.dataset.value === backgroundColor;
|
|
1646
1880
|
});
|
|
1647
1881
|
|
|
1648
1882
|
const hasHighlight = textColor !== NO_STYLE || backgroundColor !== NO_STYLE;
|
|
@@ -1939,13 +2173,35 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
|
1939
2173
|
// is cosmetic: it is a nested attachment's rendering inside this one, not the
|
|
1940
2174
|
// attribute anything re-imports from.
|
|
1941
2175
|
figure.insertAdjacentHTML("beforeend", EditorSanitizer.for(editor).sanitize(this.innerHtml, { safeForXml: true }));
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
figure
|
|
2176
|
+
this.#markImagesAsDecorative(figure);
|
|
2177
|
+
this.#tagLabelImage(figure);
|
|
2178
|
+
this.#tagLabelMirrors(figure);
|
|
1945
2179
|
|
|
1946
2180
|
return figure
|
|
1947
2181
|
}
|
|
1948
2182
|
|
|
2183
|
+
exposeLabel(figure) {
|
|
2184
|
+
const labelImage = figure.querySelector("[data-lexxy-label-image]");
|
|
2185
|
+
if (labelImage) labelImage.alt = this.label;
|
|
2186
|
+
|
|
2187
|
+
for (const span of figure.querySelectorAll("[data-lexxy-label-mirror]")) {
|
|
2188
|
+
span.setAttribute("aria-hidden", "true");
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
get label() {
|
|
2193
|
+
return this.getReadableTextContent()
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
restoreLabel(figure) {
|
|
2197
|
+
const labelImage = figure.querySelector("[data-lexxy-label-image]");
|
|
2198
|
+
if (labelImage) labelImage.alt = labelImage.dataset.lexxyOriginalAlt;
|
|
2199
|
+
|
|
2200
|
+
for (const span of figure.querySelectorAll("[data-lexxy-label-mirror]")) {
|
|
2201
|
+
span.removeAttribute("aria-hidden");
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
|
|
1949
2205
|
updateDOM() {
|
|
1950
2206
|
return false
|
|
1951
2207
|
}
|
|
@@ -1986,6 +2242,55 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
|
1986
2242
|
decorate() {
|
|
1987
2243
|
return null
|
|
1988
2244
|
}
|
|
2245
|
+
|
|
2246
|
+
#markImagesAsDecorative(figure) {
|
|
2247
|
+
for (const img of figure.querySelectorAll("img:not([alt])")) {
|
|
2248
|
+
img.alt = "";
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// Images injected later by other modules must not become the announcement image.
|
|
2253
|
+
#tagLabelImage(figure) {
|
|
2254
|
+
const image = figure.querySelector("img");
|
|
2255
|
+
if (image) {
|
|
2256
|
+
image.setAttribute("data-lexxy-label-image", "");
|
|
2257
|
+
image.dataset.lexxyOriginalAlt = image.alt;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// Duplicate labels would be spoken twice. Only authored spans should be silenced,
|
|
2262
|
+
// since the fake selection injects its own label later.
|
|
2263
|
+
#tagLabelMirrors(figure) {
|
|
2264
|
+
const trimmedLabel = this.label.trim();
|
|
2265
|
+
const matches = [ ...figure.querySelectorAll("span") ].filter((span) => span.textContent.trim() === trimmedLabel);
|
|
2266
|
+
const deepest = matches.filter((span) => !matches.some((other) => other !== span && span.contains(other)));
|
|
2267
|
+
const labelImage = figure.querySelector("[data-lexxy-label-image]");
|
|
2268
|
+
|
|
2269
|
+
for (const span of deepest) {
|
|
2270
|
+
if (span.getAttribute("aria-hidden") !== "true") {
|
|
2271
|
+
if (span.contains(labelImage)) {
|
|
2272
|
+
this.#wrapLabelTextNodesIn(span);
|
|
2273
|
+
} else {
|
|
2274
|
+
span.setAttribute("data-lexxy-label-mirror", "");
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
#wrapLabelTextNodesIn(element) {
|
|
2281
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
2282
|
+
const textNodes = [];
|
|
2283
|
+
|
|
2284
|
+
while (walker.nextNode()) {
|
|
2285
|
+
if (walker.currentNode.textContent.trim()) textNodes.push(walker.currentNode);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
for (const textNode of textNodes) {
|
|
2289
|
+
const mirror = createElement("span", { "data-lexxy-label-mirror": "" });
|
|
2290
|
+
textNode.replaceWith(mirror);
|
|
2291
|
+
mirror.append(textNode);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
1989
2294
|
}
|
|
1990
2295
|
|
|
1991
2296
|
function $isCustomActionTextAttachmentNode(node) {
|
|
@@ -2089,16 +2394,45 @@ function $createNodeSelectionWith(...nodes) {
|
|
|
2089
2394
|
return selection
|
|
2090
2395
|
}
|
|
2091
2396
|
|
|
2092
|
-
function $
|
|
2093
|
-
|
|
2397
|
+
function $singleSelectedNode() {
|
|
2398
|
+
const selection = $getSelection();
|
|
2399
|
+
if ($isNodeSelection(selection)) {
|
|
2400
|
+
const nodes = selection.getNodes();
|
|
2401
|
+
if (nodes.length === 1) {
|
|
2402
|
+
return nodes[0]
|
|
2403
|
+
} else {
|
|
2404
|
+
return null
|
|
2405
|
+
}
|
|
2406
|
+
} else {
|
|
2407
|
+
return null
|
|
2408
|
+
}
|
|
2094
2409
|
}
|
|
2095
2410
|
|
|
2096
|
-
function $
|
|
2097
|
-
|
|
2411
|
+
function $selectedLabelledDecoratorNode() {
|
|
2412
|
+
const node = $singleSelectedNode();
|
|
2413
|
+
if (typeof node?.label === "string") {
|
|
2414
|
+
return { key: node.getKey(), label: node.label }
|
|
2415
|
+
} else {
|
|
2416
|
+
return null
|
|
2417
|
+
}
|
|
2098
2418
|
}
|
|
2099
2419
|
|
|
2100
|
-
function
|
|
2101
|
-
|
|
2420
|
+
function registerLabelledDecoratorSelection(editor, onChange) {
|
|
2421
|
+
return editor.registerUpdateListener(({ editorState }) => {
|
|
2422
|
+
onChange(editorState.read($selectedLabelledDecoratorNode));
|
|
2423
|
+
})
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
function $isShadowRoot(node) {
|
|
2427
|
+
return $isElementNode(node) && $isRootOrShadowRoot(node) && !$isRootNode(node)
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
function $isSafeForRoot(node) {
|
|
2431
|
+
return ($isElementNode(node) || $isDecoratorNode(node)) && !node.isParentRequired()
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
function $makeSafeForRoot(node) {
|
|
2435
|
+
if ($isSafeForRoot(node)) {
|
|
2102
2436
|
return node
|
|
2103
2437
|
} else if (node.getParent()) {
|
|
2104
2438
|
return $wrapNodeInElement(node, () => node.createParentElementNode())
|
|
@@ -2114,6 +2448,10 @@ function getListType(node) {
|
|
|
2114
2448
|
return list?.getListType() ?? null
|
|
2115
2449
|
}
|
|
2116
2450
|
|
|
2451
|
+
function announceFromEditor(editor, message) {
|
|
2452
|
+
editor.getRootElement()?.closest("lexxy-editor")?.announce(message);
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2117
2455
|
function isEditorFocused(editor) {
|
|
2118
2456
|
const rootElement = editor.getRootElement();
|
|
2119
2457
|
return rootElement !== null && rootElement.contains(document.activeElement)
|
|
@@ -2689,7 +3027,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2689
3027
|
node: new ActionTextAttachmentNode({
|
|
2690
3028
|
src: img.getAttribute("src"),
|
|
2691
3029
|
fileName: fileName,
|
|
2692
|
-
|
|
3030
|
+
altText: img.getAttribute("alt"),
|
|
2693
3031
|
contentType: "image/*",
|
|
2694
3032
|
width: img.getAttribute("width"),
|
|
2695
3033
|
height: img.getAttribute("height")
|
|
@@ -2766,14 +3104,27 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2766
3104
|
updateDOM(prevNode, dom) {
|
|
2767
3105
|
if (this.uploadError !== prevNode.uploadError) return true
|
|
2768
3106
|
|
|
2769
|
-
const
|
|
2770
|
-
if (
|
|
2771
|
-
|
|
3107
|
+
const image = dom.querySelector("img");
|
|
3108
|
+
if (image && prevNode.altText !== this.altText) {
|
|
3109
|
+
image.alt = this.altText;
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
if (prevNode.caption !== this.caption) {
|
|
3113
|
+
dom.querySelector("figcaption")?.toggleAttribute("data-placeholder", !this.caption);
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
const captionText = dom.querySelector("figcaption .attachment__caption-text");
|
|
3117
|
+
if (captionText && prevNode.captionLabel !== this.captionLabel) {
|
|
3118
|
+
captionText.textContent = this.captionLabel;
|
|
2772
3119
|
}
|
|
2773
3120
|
|
|
2774
3121
|
return false
|
|
2775
3122
|
}
|
|
2776
3123
|
|
|
3124
|
+
get captionLabel() {
|
|
3125
|
+
return this.caption || this.fileName || ""
|
|
3126
|
+
}
|
|
3127
|
+
|
|
2777
3128
|
getTextContent() {
|
|
2778
3129
|
return `[${this.caption || this.fileName}]\n\n`
|
|
2779
3130
|
}
|
|
@@ -2835,9 +3186,6 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2835
3186
|
figure.draggable = true;
|
|
2836
3187
|
figure.dataset.lexicalNodeKey = this.__key;
|
|
2837
3188
|
|
|
2838
|
-
const deleteButton = createElement("lexxy-node-delete-button");
|
|
2839
|
-
figure.appendChild(deleteButton);
|
|
2840
|
-
|
|
2841
3189
|
return figure
|
|
2842
3190
|
}
|
|
2843
3191
|
|
|
@@ -2853,6 +3201,18 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2853
3201
|
return this.contentType.startsWith("video/")
|
|
2854
3202
|
}
|
|
2855
3203
|
|
|
3204
|
+
get label() {
|
|
3205
|
+
if (this.caption && this.altText && this.altText !== this.caption && this.altText !== this.fileName) {
|
|
3206
|
+
return `${this.caption}. ${this.altText}`
|
|
3207
|
+
} else {
|
|
3208
|
+
return this.caption || this.altText || this.fileName || ""
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
focusCaption() {
|
|
3213
|
+
return this.editor.getRootElement()?.closest("lexxy-editor")?.captionEditor.open(this.getKey()) ?? false
|
|
3214
|
+
}
|
|
3215
|
+
|
|
2856
3216
|
#createDOMForPendingPreview() {
|
|
2857
3217
|
const figure = this.createAttachmentFigure(false);
|
|
2858
3218
|
figure.appendChild(this.#createDOMForFile());
|
|
@@ -2992,8 +3352,9 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2992
3352
|
}
|
|
2993
3353
|
|
|
2994
3354
|
#swapToPreviewDOM(figure, previewSrc) {
|
|
3355
|
+
const altText = this.editor.read(() => this.getLatest().altText);
|
|
2995
3356
|
this.#swapFigureContent(figure, "attachment--file", "attachment--preview", () => {
|
|
2996
|
-
const img = createElement("img", { src: previewSrc, draggable: false, alt:
|
|
3357
|
+
const img = createElement("img", { src: previewSrc, draggable: false, alt: altText });
|
|
2997
3358
|
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
2998
3359
|
const container = createElement("div", { className: "attachment__container" });
|
|
2999
3360
|
container.appendChild(img);
|
|
@@ -3043,54 +3404,17 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
3043
3404
|
}
|
|
3044
3405
|
|
|
3045
3406
|
#createEditableCaption() {
|
|
3046
|
-
const caption = createElement("figcaption", { className: "attachment__caption" });
|
|
3047
|
-
const
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3407
|
+
const caption = createElement("figcaption", { className: "attachment__caption attachment__caption--editable" });
|
|
3408
|
+
const text = createElement("span", { className: "attachment__caption-text", textContent: this.captionLabel });
|
|
3409
|
+
caption.appendChild(text);
|
|
3410
|
+
caption.toggleAttribute("data-placeholder", !this.caption);
|
|
3411
|
+
caption.addEventListener("mousedown", event => event.preventDefault());
|
|
3412
|
+
caption.addEventListener("click", event => {
|
|
3413
|
+
event.stopPropagation();
|
|
3414
|
+
this.focusCaption();
|
|
3051
3415
|
});
|
|
3052
|
-
|
|
3053
|
-
input.addEventListener("focusin", () => input.placeholder = "Add caption...");
|
|
3054
|
-
input.addEventListener("blur", (event) => this.#handleCaptionInputBlurred(event));
|
|
3055
|
-
input.addEventListener("keydown", (event) => this.#handleCaptionInputKeydown(event));
|
|
3056
|
-
input.addEventListener("copy", (event) => event.stopPropagation());
|
|
3057
|
-
input.addEventListener("cut", (event) => event.stopPropagation());
|
|
3058
|
-
input.addEventListener("paste", (event) => event.stopPropagation());
|
|
3059
|
-
|
|
3060
|
-
caption.appendChild(input);
|
|
3061
|
-
|
|
3062
3416
|
return caption
|
|
3063
3417
|
}
|
|
3064
|
-
|
|
3065
|
-
#handleCaptionInputBlurred(event) {
|
|
3066
|
-
this.#updateCaptionValueFromInput(event.target);
|
|
3067
|
-
}
|
|
3068
|
-
|
|
3069
|
-
#updateCaptionValueFromInput(input) {
|
|
3070
|
-
input.placeholder = this.fileName;
|
|
3071
|
-
this.editor.update(() => {
|
|
3072
|
-
this.getWritable().caption = input.value;
|
|
3073
|
-
});
|
|
3074
|
-
}
|
|
3075
|
-
|
|
3076
|
-
#handleCaptionInputKeydown(event) {
|
|
3077
|
-
if (event.key === "Enter") {
|
|
3078
|
-
event.preventDefault();
|
|
3079
|
-
event.target.blur();
|
|
3080
|
-
|
|
3081
|
-
this.editor.update(() => {
|
|
3082
|
-
// Place the cursor after the current image
|
|
3083
|
-
this.selectNext(0, 0);
|
|
3084
|
-
}, {
|
|
3085
|
-
tag: HISTORY_MERGE_TAG
|
|
3086
|
-
});
|
|
3087
|
-
}
|
|
3088
|
-
|
|
3089
|
-
// Stop all keydown events from bubbling to the Lexical root element.
|
|
3090
|
-
// The caption textarea is outside Lexical's content model and should
|
|
3091
|
-
// handle its own keyboard events natively (Ctrl+A, Ctrl+C, Ctrl+X, etc.).
|
|
3092
|
-
event.stopPropagation();
|
|
3093
|
-
}
|
|
3094
3418
|
}
|
|
3095
3419
|
|
|
3096
3420
|
function $createActionTextAttachmentNode(...args) {
|
|
@@ -3210,14 +3534,11 @@ class HorizontalDividerNode extends DecoratorNode {
|
|
|
3210
3534
|
super(key);
|
|
3211
3535
|
}
|
|
3212
3536
|
|
|
3537
|
+
// The hr already provides separator semantics; a figure boundary would be redundant.
|
|
3213
3538
|
createDOM() {
|
|
3214
|
-
const figure = createElement("figure", { className: "horizontal-divider" });
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
figure.appendChild(hr);
|
|
3218
|
-
|
|
3219
|
-
const deleteButton = createElement("lexxy-node-delete-button");
|
|
3220
|
-
figure.appendChild(deleteButton);
|
|
3539
|
+
const figure = createElement("figure", { className: "horizontal-divider", role: "presentation" });
|
|
3540
|
+
figure.appendChild(createElement("hr"));
|
|
3541
|
+
figure.dataset.lexicalNodeKey = this.__key;
|
|
3221
3542
|
|
|
3222
3543
|
return figure
|
|
3223
3544
|
}
|
|
@@ -3234,6 +3555,10 @@ class HorizontalDividerNode extends DecoratorNode {
|
|
|
3234
3555
|
return false
|
|
3235
3556
|
}
|
|
3236
3557
|
|
|
3558
|
+
get label() {
|
|
3559
|
+
return "Horizontal divider"
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3237
3562
|
exportDOM() {
|
|
3238
3563
|
const hr = createElement("hr");
|
|
3239
3564
|
return { element: hr }
|
|
@@ -3499,189 +3824,366 @@ var theme = {
|
|
|
3499
3824
|
}
|
|
3500
3825
|
};
|
|
3501
3826
|
|
|
3502
|
-
class
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3827
|
+
class ImageGalleryNode extends ElementNode {
|
|
3828
|
+
$config() {
|
|
3829
|
+
return this.config("image_gallery", {
|
|
3830
|
+
extends: ElementNode,
|
|
3831
|
+
})
|
|
3507
3832
|
}
|
|
3508
3833
|
|
|
3509
|
-
|
|
3510
|
-
|
|
3834
|
+
static transform() {
|
|
3835
|
+
return (gallery) => {
|
|
3836
|
+
gallery.unwrapEmptyNode()
|
|
3837
|
+
|| gallery.replaceWithSingularChild()
|
|
3838
|
+
|| gallery.splitAroundInvalidChild();
|
|
3839
|
+
}
|
|
3511
3840
|
}
|
|
3512
3841
|
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3842
|
+
static importDOM() {
|
|
3843
|
+
return {
|
|
3844
|
+
div: (element) => {
|
|
3845
|
+
if (!this.#isGalleryElement(element)) return null
|
|
3846
|
+
|
|
3847
|
+
return {
|
|
3848
|
+
conversion: () => {
|
|
3849
|
+
return {
|
|
3850
|
+
node: $createImageGalleryNode()
|
|
3851
|
+
}
|
|
3852
|
+
},
|
|
3853
|
+
priority: 2
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3518
3856
|
}
|
|
3519
3857
|
}
|
|
3520
3858
|
|
|
3521
|
-
|
|
3522
|
-
this
|
|
3859
|
+
static canCollapseWith(node) {
|
|
3860
|
+
return $isImageGalleryNode(node) || this.isValidChild(node)
|
|
3523
3861
|
}
|
|
3524
|
-
}
|
|
3525
3862
|
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
// the stretches no range covers. Ranges must be sorted and non-overlapping.
|
|
3530
|
-
function segmentTextByRanges(text, textStart, ranges) {
|
|
3531
|
-
const segments = [];
|
|
3532
|
-
let cursor = 0;
|
|
3863
|
+
static isValidChild(node) {
|
|
3864
|
+
return $isActionTextAttachmentNode(node) && node.isPreviewableImage
|
|
3865
|
+
}
|
|
3533
3866
|
|
|
3534
|
-
|
|
3535
|
-
const
|
|
3536
|
-
|
|
3867
|
+
static #isGalleryElement(element) {
|
|
3868
|
+
const attachmentChildren = element.querySelectorAll(`:scope > :is(${this.#attachmentTags.join()})`);
|
|
3869
|
+
return element.textContent.trim() === ""
|
|
3870
|
+
&& attachmentChildren.length > 0
|
|
3871
|
+
&& element.children.length === attachmentChildren.length
|
|
3872
|
+
}
|
|
3537
3873
|
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3874
|
+
static get #attachmentTags() {
|
|
3875
|
+
return Object.keys(ActionTextAttachmentNode.importDOM())
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
createDOM() {
|
|
3879
|
+
const div = document.createElement("div");
|
|
3880
|
+
div.className = this.#galleryClassNames;
|
|
3881
|
+
return div
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
updateDOM(_prevNode, dom) {
|
|
3885
|
+
dom.className = this.#galleryClassNames;
|
|
3886
|
+
return false
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
canBeEmpty() {
|
|
3890
|
+
// Return `true` to conform to `$isBlock(node)`
|
|
3891
|
+
// We clean-up empty galleries with a transform
|
|
3892
|
+
return true
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
collapseAtStart(_selection) {
|
|
3896
|
+
return true
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
insertNewAfter(selection, restoreSelection) {
|
|
3900
|
+
const selectionBeforeLastChild = selection.anchor.getNode().is(this) && selection.anchor.offset == this.getChildrenSize() - 1;
|
|
3901
|
+
if (selectionBeforeLastChild) {
|
|
3902
|
+
const paragraph = $createParagraphNode();
|
|
3903
|
+
this.insertAfter(paragraph, false);
|
|
3904
|
+
paragraph.insertAfter(this.getLastChild(), false);
|
|
3905
|
+
paragraph.selectEnd();
|
|
3906
|
+
|
|
3907
|
+
// return null as selection has been managed
|
|
3908
|
+
return null
|
|
3544
3909
|
}
|
|
3910
|
+
|
|
3911
|
+
const newNode = $createImageGalleryNode();
|
|
3912
|
+
this.insertAfter(newNode, restoreSelection);
|
|
3913
|
+
return newNode
|
|
3545
3914
|
}
|
|
3546
3915
|
|
|
3547
|
-
|
|
3548
|
-
|
|
3916
|
+
getImageAttachments() {
|
|
3917
|
+
const children = this.getChildren();
|
|
3918
|
+
return children.filter($isActionTextAttachmentNode)
|
|
3549
3919
|
}
|
|
3550
3920
|
|
|
3551
|
-
|
|
3552
|
-
|
|
3921
|
+
exportDOM() {
|
|
3922
|
+
const div = document.createElement("div");
|
|
3923
|
+
div.className = this.#galleryClassNames;
|
|
3924
|
+
return { element: div }
|
|
3925
|
+
}
|
|
3553
3926
|
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
// CSS custom properties). The container is created once and attached to
|
|
3557
|
-
// `document.body` once; subsequent child mutations happen *inside* the
|
|
3558
|
-
// contained subtree so they do not invalidate style on the rest of the page.
|
|
3559
|
-
//
|
|
3560
|
-
// Without this, `document.body.appendChild(...)` / `element.remove()` calls
|
|
3561
|
-
// forced the browser to re-evaluate every ancestor-dependent selector (`:has()`,
|
|
3562
|
-
// descendant combinators, universal sibling rules) across the document on each
|
|
3563
|
-
// invocation — a 13,000+ element style recalc per call on a typical Basecamp
|
|
3564
|
-
// page.
|
|
3927
|
+
collapseWith(node, backwards) {
|
|
3928
|
+
if (!ImageGalleryNode.canCollapseWith(node)) return false
|
|
3565
3929
|
|
|
3566
|
-
|
|
3930
|
+
if (backwards) {
|
|
3931
|
+
$insertFirst(this, node);
|
|
3932
|
+
} else {
|
|
3933
|
+
this.append(node);
|
|
3934
|
+
}
|
|
3567
3935
|
|
|
3568
|
-
|
|
3569
|
-
if (resolverRoot && resolverRoot.isConnected) return resolverRoot
|
|
3936
|
+
$unwrapAndFilterDescendants(this, ImageGalleryNode.isValidChild);
|
|
3570
3937
|
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
resolverRoot.setAttribute("data-lexxy-style-resolver", "");
|
|
3574
|
-
// `contain: strict` (size, layout, paint, style) isolates everything.
|
|
3575
|
-
// The root itself paints nothing (visibility hidden), has zero
|
|
3576
|
-
// geometric impact (position fixed, intrinsic size via contain), and
|
|
3577
|
-
// never leaks style invalidation to its ancestors.
|
|
3578
|
-
resolverRoot.style.cssText = "contain: strict; position: fixed; top: 0; left: 0; visibility: hidden; pointer-events: none; width: 0; height: 0;";
|
|
3579
|
-
document.body.appendChild(resolverRoot);
|
|
3580
|
-
return resolverRoot
|
|
3581
|
-
}
|
|
3938
|
+
return true
|
|
3939
|
+
}
|
|
3582
3940
|
|
|
3583
|
-
|
|
3584
|
-
|
|
3941
|
+
unwrapEmptyNode() {
|
|
3942
|
+
if (this.isEmpty()) {
|
|
3943
|
+
const paragraph = $createParagraphNode();
|
|
3944
|
+
return this.replace(paragraph)
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3585
3947
|
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3948
|
+
replaceWithSingularChild() {
|
|
3949
|
+
if (this.#hasSingularChild) {
|
|
3950
|
+
const child = this.getFirstChild();
|
|
3951
|
+
return this.replace($makeSafeForRoot(child))
|
|
3952
|
+
}
|
|
3590
3953
|
}
|
|
3591
|
-
}
|
|
3592
3954
|
|
|
3593
|
-
|
|
3594
|
-
|
|
3955
|
+
splitAroundInvalidChild() {
|
|
3956
|
+
for (const child of $firstToLastIterator(this)) {
|
|
3957
|
+
if (ImageGalleryNode.isValidChild(child)) continue
|
|
3595
3958
|
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3959
|
+
const poppedNode = $makeSafeForRoot(child);
|
|
3960
|
+
const [ topGallery, secondGallery ] = this.splitAtIndex(poppedNode.getIndexWithinParent());
|
|
3961
|
+
topGallery.insertAfter(poppedNode);
|
|
3962
|
+
poppedNode.selectEnd();
|
|
3963
|
+
|
|
3964
|
+
// remove an empty gallery rather than let it unwrap to a paragraph
|
|
3965
|
+
if (secondGallery.isEmpty()) secondGallery.remove();
|
|
3966
|
+
|
|
3967
|
+
break
|
|
3601
3968
|
}
|
|
3602
3969
|
}
|
|
3603
3970
|
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3971
|
+
splitAtIndex(index) {
|
|
3972
|
+
return $splitNode(this, index)
|
|
3973
|
+
}
|
|
3607
3974
|
|
|
3608
|
-
|
|
3975
|
+
get #hasSingularChild() {
|
|
3976
|
+
return this.getChildrenSize() === 1
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
get #galleryClassNames() {
|
|
3980
|
+
return `attachment-gallery attachment-gallery--${this.getChildrenSize()}`
|
|
3981
|
+
}
|
|
3609
3982
|
}
|
|
3610
3983
|
|
|
3611
|
-
function
|
|
3612
|
-
|
|
3613
|
-
return !!(styles.color || styles["background-color"])
|
|
3984
|
+
function $createImageGalleryNode() {
|
|
3985
|
+
return new ImageGalleryNode()
|
|
3614
3986
|
}
|
|
3615
3987
|
|
|
3616
|
-
function
|
|
3617
|
-
return
|
|
3618
|
-
return canonicalizer.applyCanonicalization(css)
|
|
3619
|
-
}, styles)
|
|
3988
|
+
function $isImageGalleryNode(node) {
|
|
3989
|
+
return node instanceof ImageGalleryNode
|
|
3620
3990
|
}
|
|
3621
3991
|
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3992
|
+
function $findOrCreateGalleryForImage(node) {
|
|
3993
|
+
if (!ImageGalleryNode.canCollapseWith(node)) return null
|
|
3994
|
+
|
|
3995
|
+
const existingGallery = $getNearestNodeOfType(node, ImageGalleryNode);
|
|
3996
|
+
return existingGallery ?? $wrapNodeInElement(node, $createImageGalleryNode)
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
class CaptionEditor {
|
|
4000
|
+
#editorElement
|
|
4001
|
+
#editor
|
|
4002
|
+
#input = createElement("textarea", { className: "lexxy-caption-editor", hidden: true, tabIndex: -1, rows: 1, placeholder: "Add caption..." })
|
|
4003
|
+
#nodeKey
|
|
4004
|
+
#caption
|
|
4005
|
+
#listeners = new ListenerBin()
|
|
4006
|
+
#resizeObserver = new ResizeObserver(() => this.#updatePosition())
|
|
4007
|
+
|
|
4008
|
+
constructor(editorElement) {
|
|
4009
|
+
this.#editorElement = editorElement;
|
|
4010
|
+
this.#editor = editorElement.editor;
|
|
4011
|
+
editorElement.append(this.#input);
|
|
4012
|
+
this.#listeners.track(
|
|
4013
|
+
registerEventListener(this.#input, "blur", () => this.#save()),
|
|
4014
|
+
registerEventListener(this.#input, "keydown", event => this.#handleKeydown(event)),
|
|
4015
|
+
registerEventListener(this.#input, "input", () => this.#updatePosition()),
|
|
4016
|
+
registerEventListener(editorElement, "scroll", () => this.#updatePosition(), { capture: true, passive: true }),
|
|
4017
|
+
this.#editor.registerUpdateListener(() => this.#checkAttachment())
|
|
4018
|
+
);
|
|
3627
4019
|
}
|
|
3628
4020
|
|
|
3629
|
-
|
|
3630
|
-
|
|
4021
|
+
dispose() {
|
|
4022
|
+
this.#listeners.dispose();
|
|
4023
|
+
this.#close();
|
|
4024
|
+
this.#input.remove();
|
|
4025
|
+
}
|
|
3631
4026
|
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
4027
|
+
open(nodeKey) {
|
|
4028
|
+
const caption = this.#editor.getElementByKey(nodeKey)?.querySelector("figcaption.attachment__caption--editable");
|
|
4029
|
+
if (caption) {
|
|
4030
|
+
if (this.#nodeKey !== nodeKey) {
|
|
4031
|
+
this.#edit(nodeKey, caption);
|
|
4032
|
+
}
|
|
4033
|
+
return true
|
|
4034
|
+
} else {
|
|
4035
|
+
return false
|
|
3635
4036
|
}
|
|
4037
|
+
}
|
|
3636
4038
|
|
|
3637
|
-
|
|
4039
|
+
#save() {
|
|
4040
|
+
if (this.#nodeKey) {
|
|
4041
|
+
const key = this.#nodeKey;
|
|
4042
|
+
const value = this.#input.value;
|
|
4043
|
+
this.#close();
|
|
4044
|
+
this.#editor.update(() => {
|
|
4045
|
+
const node = $getNodeByKey(key);
|
|
4046
|
+
if (node && node.caption !== value) node.getWritable().caption = value;
|
|
4047
|
+
}, { tag: SKIP_DOM_SELECTION_TAG, discrete: true });
|
|
4048
|
+
}
|
|
3638
4049
|
}
|
|
3639
4050
|
|
|
3640
|
-
|
|
3641
|
-
|
|
4051
|
+
#close() {
|
|
4052
|
+
this.#nodeKey = null;
|
|
4053
|
+
this.#resizeObserver.disconnect();
|
|
4054
|
+
if (this.#caption) {
|
|
4055
|
+
this.#caption.classList.remove("attachment__caption--editing");
|
|
4056
|
+
this.#caption.style.removeProperty("min-height");
|
|
4057
|
+
this.#caption = null;
|
|
4058
|
+
}
|
|
4059
|
+
this.#input.hidden = true;
|
|
3642
4060
|
}
|
|
3643
4061
|
|
|
3644
|
-
|
|
4062
|
+
#handleKeydown(event) {
|
|
4063
|
+
if (!event.isComposing && [ "Enter", "Escape" ].includes(event.key)) {
|
|
4064
|
+
event.preventDefault();
|
|
4065
|
+
const key = this.#nodeKey;
|
|
4066
|
+
this.#input.blur();
|
|
4067
|
+
this.#editor.getRootElement()?.focus({ preventScroll: true });
|
|
4068
|
+
this.#editor.update(() => {
|
|
4069
|
+
const node = $getNodeByKey(key);
|
|
4070
|
+
if (node) {
|
|
4071
|
+
if (event.key === "Enter") {
|
|
4072
|
+
if ($isImageGalleryNode(node.getParent()) && !node.getNextSibling()) {
|
|
4073
|
+
node.getParent().selectNext(0, 0);
|
|
4074
|
+
} else {
|
|
4075
|
+
node.selectNext(0, 0);
|
|
4076
|
+
}
|
|
4077
|
+
} else {
|
|
4078
|
+
$setSelection($createNodeSelectionWith(node));
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
}, { tag: HISTORY_MERGE_TAG });
|
|
4082
|
+
}
|
|
4083
|
+
event.stopPropagation();
|
|
4084
|
+
}
|
|
3645
4085
|
|
|
3646
|
-
|
|
3647
|
-
|
|
4086
|
+
#updatePosition() {
|
|
4087
|
+
if (this.#caption?.isConnected) {
|
|
4088
|
+
const rect = this.#caption.getBoundingClientRect();
|
|
4089
|
+
const editorRect = this.#editorElement.getBoundingClientRect();
|
|
4090
|
+
this.#input.style.left = `${rect.left - editorRect.left - this.#editorElement.clientLeft + this.#editorElement.scrollLeft}px`;
|
|
4091
|
+
this.#input.style.top = `${rect.top - editorRect.top - this.#editorElement.clientTop + this.#editorElement.scrollTop}px`;
|
|
4092
|
+
this.#input.style.width = `${rect.width}px`;
|
|
4093
|
+
this.#input.style.height = "0px";
|
|
4094
|
+
const height = this.#input.scrollHeight;
|
|
4095
|
+
this.#input.style.height = `${height}px`;
|
|
4096
|
+
this.#caption.style.minHeight = `${height}px`;
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
#checkAttachment() {
|
|
4101
|
+
if (this.#nodeKey && this.#editor.getElementByKey(this.#nodeKey)?.querySelector("figcaption") !== this.#caption) {
|
|
4102
|
+
this.#close();
|
|
4103
|
+
} else {
|
|
4104
|
+
this.#updatePosition();
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
#edit(nodeKey, caption) {
|
|
4109
|
+
this.#save();
|
|
4110
|
+
this.#nodeKey = nodeKey;
|
|
4111
|
+
this.#caption = caption;
|
|
4112
|
+
this.#editor.getEditorState().read(() => {
|
|
4113
|
+
const node = $getNodeByKey(nodeKey);
|
|
4114
|
+
this.#input.value = node.caption;
|
|
4115
|
+
if (node.isVideo) {
|
|
4116
|
+
this.#input.ariaLabel = "Video caption";
|
|
4117
|
+
} else {
|
|
4118
|
+
this.#input.ariaLabel = "Image caption";
|
|
4119
|
+
}
|
|
4120
|
+
});
|
|
4121
|
+
|
|
4122
|
+
caption.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
4123
|
+
const style = getComputedStyle(caption);
|
|
4124
|
+
this.#input.style.font = style.font;
|
|
4125
|
+
this.#input.style.padding = style.padding;
|
|
4126
|
+
this.#input.style.color = style.color;
|
|
4127
|
+
this.#input.style.textAlign = getComputedStyle(caption.firstElementChild).textAlign;
|
|
4128
|
+
this.#input.hidden = false;
|
|
4129
|
+
caption.classList.add("attachment__caption--editing");
|
|
4130
|
+
this.#resizeObserver.observe(caption);
|
|
4131
|
+
this.#resizeObserver.observe(this.#editorElement);
|
|
4132
|
+
this.#updatePosition();
|
|
4133
|
+
this.#input.focus({ preventScroll: true });
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
class UploadRequests {
|
|
4138
|
+
#requestsByKey = new Map()
|
|
4139
|
+
|
|
4140
|
+
track(key, request) {
|
|
4141
|
+
this.#requestsByKey.set(key, request);
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
forget(key) {
|
|
4145
|
+
this.#requestsByKey.delete(key);
|
|
3648
4146
|
}
|
|
3649
4147
|
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
if (
|
|
3653
|
-
|
|
4148
|
+
abort(key) {
|
|
4149
|
+
const request = this.#requestsByKey.get(key);
|
|
4150
|
+
if (request) {
|
|
4151
|
+
this.#requestsByKey.delete(key);
|
|
4152
|
+
request.abort();
|
|
3654
4153
|
}
|
|
3655
|
-
return index === -1 ? null : this._allowedValues[index]
|
|
3656
4154
|
}
|
|
3657
4155
|
|
|
3658
|
-
|
|
3659
|
-
|
|
4156
|
+
clear() {
|
|
4157
|
+
this.#requestsByKey.clear();
|
|
3660
4158
|
}
|
|
3661
4159
|
}
|
|
3662
4160
|
|
|
3663
|
-
//
|
|
3664
|
-
//
|
|
3665
|
-
//
|
|
3666
|
-
// the
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
4161
|
+
// Partition a stretch of text into consecutive { start, end, range } segments,
|
|
4162
|
+
// where offsets are relative to the text and range is the covering source
|
|
4163
|
+
// range — expressed in outer coordinates as { start, end, ... } — or null for
|
|
4164
|
+
// the stretches no range covers. Ranges must be sorted and non-overlapping.
|
|
4165
|
+
function segmentTextByRanges(text, textStart, ranges) {
|
|
4166
|
+
const segments = [];
|
|
4167
|
+
let cursor = 0;
|
|
3670
4168
|
|
|
3671
|
-
const
|
|
3672
|
-
const
|
|
3673
|
-
|
|
3674
|
-
return element
|
|
3675
|
-
});
|
|
4169
|
+
for (const range of ranges) {
|
|
4170
|
+
const from = Math.max(range.start - textStart, cursor);
|
|
4171
|
+
const to = Math.min(range.end - textStart, text.length);
|
|
3676
4172
|
|
|
3677
|
-
|
|
4173
|
+
if (from < to) {
|
|
4174
|
+
if (from > cursor) {
|
|
4175
|
+
segments.push({ start: cursor, end: from, range: null });
|
|
4176
|
+
}
|
|
4177
|
+
segments.push({ start: from, end: to, range });
|
|
4178
|
+
cursor = to;
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
3678
4181
|
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
4182
|
+
if (cursor < text.length) {
|
|
4183
|
+
segments.push({ start: cursor, end: text.length, range: null });
|
|
4184
|
+
}
|
|
3682
4185
|
|
|
3683
|
-
|
|
3684
|
-
return computed
|
|
4186
|
+
return segments
|
|
3685
4187
|
}
|
|
3686
4188
|
|
|
3687
4189
|
const TOGGLE_HIGHLIGHT_COMMAND = createCommand();
|
|
@@ -3966,11 +4468,15 @@ function $extractHighlightRangesFromCodeNode(codeNode) {
|
|
|
3966
4468
|
|
|
3967
4469
|
function buildCanonicalizers(config) {
|
|
3968
4470
|
return [
|
|
3969
|
-
new StyleCanonicalizer("color", [ ...config.buttons.color, ...config.permit.color ]),
|
|
3970
|
-
new StyleCanonicalizer("background-color", [ ...config.buttons["background-color"], ...config.permit["background-color"] ])
|
|
4471
|
+
new StyleCanonicalizer("color", [ ...colorValues(config.buttons.color), ...config.permit.color ]),
|
|
4472
|
+
new StyleCanonicalizer("background-color", [ ...colorValues(config.buttons["background-color"]), ...config.permit["background-color"] ])
|
|
3971
4473
|
]
|
|
3972
4474
|
}
|
|
3973
4475
|
|
|
4476
|
+
function colorValues(buttons) {
|
|
4477
|
+
return normalizeColorButtons(buttons).map((button) => button.value)
|
|
4478
|
+
}
|
|
4479
|
+
|
|
3974
4480
|
function $toggleSelectionStyles(editor, styles) {
|
|
3975
4481
|
const selection = $getSelection();
|
|
3976
4482
|
if (!$isRangeSelection(selection)) return
|
|
@@ -4909,29 +5415,25 @@ class Selection {
|
|
|
4909
5415
|
}, COMMAND_PRIORITY_LOW),
|
|
4910
5416
|
|
|
4911
5417
|
this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
4912
|
-
this.#
|
|
5418
|
+
this.#preserveFocusOutsideEditor();
|
|
4913
5419
|
return false
|
|
4914
5420
|
}, COMMAND_PRIORITY_CRITICAL)
|
|
4915
5421
|
);
|
|
4916
5422
|
}
|
|
4917
5423
|
|
|
4918
|
-
//
|
|
4919
|
-
//
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
// tag the update so Lexical leaves the DOM selection (and with it, focus) alone.
|
|
4923
|
-
#preserveFocusOfOtherFields() {
|
|
4924
|
-
if (this.#anotherFieldIsFocused) {
|
|
5424
|
+
// Selection changes can arrive after Tab moves focus to a toolbar or another field.
|
|
5425
|
+
// Reconciling that stale selection would pull focus back into the editor.
|
|
5426
|
+
#preserveFocusOutsideEditor() {
|
|
5427
|
+
if (this.#isAnotherElementFocused) {
|
|
4925
5428
|
$addUpdateTag(SKIP_DOM_SELECTION_TAG);
|
|
4926
5429
|
}
|
|
4927
5430
|
}
|
|
4928
5431
|
|
|
4929
|
-
get #
|
|
5432
|
+
get #isAnotherElementFocused() {
|
|
4930
5433
|
const rootElement = this.editor.getRootElement();
|
|
4931
5434
|
const activeElement = rootElement?.ownerDocument.activeElement;
|
|
4932
5435
|
|
|
4933
|
-
return activeElement && !rootElement.contains(activeElement)
|
|
4934
|
-
(activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.isContentEditable)
|
|
5436
|
+
return activeElement && activeElement !== rootElement.ownerDocument.body && !rootElement.contains(activeElement)
|
|
4935
5437
|
}
|
|
4936
5438
|
|
|
4937
5439
|
#listenForNodeSelections() {
|
|
@@ -5461,243 +5963,71 @@ class Selection {
|
|
|
5461
5963
|
|
|
5462
5964
|
// Get a reliable bounding rect for the first or last character in a DOM
|
|
5463
5965
|
// element by creating a non-collapsed range around it.
|
|
5464
|
-
#getEdgeCharRect(element, edge) {
|
|
5465
|
-
const walker = document.createTreeWalker(element, 4 /* NodeFilter.SHOW_TEXT */);
|
|
5466
|
-
let textNode;
|
|
5467
|
-
|
|
5468
|
-
if (edge === "first") {
|
|
5469
|
-
textNode = walker.nextNode();
|
|
5470
|
-
} else {
|
|
5471
|
-
while (walker.nextNode()) textNode = walker.currentNode;
|
|
5472
|
-
}
|
|
5473
|
-
|
|
5474
|
-
if (!textNode || textNode.length === 0) return null
|
|
5475
|
-
|
|
5476
|
-
const range = document.createRange();
|
|
5477
|
-
if (edge === "first") {
|
|
5478
|
-
range.setStart(textNode, 0);
|
|
5479
|
-
range.setEnd(textNode, 1);
|
|
5480
|
-
} else {
|
|
5481
|
-
range.setStart(textNode, textNode.length - 1);
|
|
5482
|
-
range.setEnd(textNode, textNode.length);
|
|
5483
|
-
}
|
|
5484
|
-
|
|
5485
|
-
return range.getBoundingClientRect()
|
|
5486
|
-
}
|
|
5487
|
-
}
|
|
5488
|
-
|
|
5489
|
-
class EditorConfiguration {
|
|
5490
|
-
#editorElement
|
|
5491
|
-
#config
|
|
5492
|
-
|
|
5493
|
-
constructor(editorElement) {
|
|
5494
|
-
this.#editorElement = editorElement;
|
|
5495
|
-
this.#config = new Configuration(
|
|
5496
|
-
Lexxy.presets.get("default"),
|
|
5497
|
-
Lexxy.presets.get(editorElement.preset),
|
|
5498
|
-
this.#overrides
|
|
5499
|
-
);
|
|
5500
|
-
}
|
|
5501
|
-
|
|
5502
|
-
get(path) {
|
|
5503
|
-
return this.#config.get(path)
|
|
5504
|
-
}
|
|
5505
|
-
|
|
5506
|
-
get #overrides() {
|
|
5507
|
-
const overrides = {};
|
|
5508
|
-
for (const option of this.#defaultOptions) {
|
|
5509
|
-
const attribute = dasherize(option);
|
|
5510
|
-
if (this.#editorElement.hasAttribute(attribute)) {
|
|
5511
|
-
overrides[option] = this.#parseAttribute(attribute);
|
|
5512
|
-
}
|
|
5513
|
-
}
|
|
5514
|
-
return overrides
|
|
5515
|
-
}
|
|
5516
|
-
|
|
5517
|
-
get #defaultOptions() {
|
|
5518
|
-
return Object.keys(Lexxy.presets.get("default"))
|
|
5519
|
-
}
|
|
5520
|
-
|
|
5521
|
-
#parseAttribute(attribute) {
|
|
5522
|
-
const value = this.#editorElement.getAttribute(attribute);
|
|
5523
|
-
try {
|
|
5524
|
-
return JSON.parse(value)
|
|
5525
|
-
} catch {
|
|
5526
|
-
return value
|
|
5527
|
-
}
|
|
5528
|
-
}
|
|
5529
|
-
}
|
|
5530
|
-
|
|
5531
|
-
class ImageGalleryNode extends ElementNode {
|
|
5532
|
-
$config() {
|
|
5533
|
-
return this.config("image_gallery", {
|
|
5534
|
-
extends: ElementNode,
|
|
5535
|
-
})
|
|
5536
|
-
}
|
|
5537
|
-
|
|
5538
|
-
static transform() {
|
|
5539
|
-
return (gallery) => {
|
|
5540
|
-
gallery.unwrapEmptyNode()
|
|
5541
|
-
|| gallery.replaceWithSingularChild()
|
|
5542
|
-
|| gallery.splitAroundInvalidChild();
|
|
5543
|
-
}
|
|
5544
|
-
}
|
|
5545
|
-
|
|
5546
|
-
static importDOM() {
|
|
5547
|
-
return {
|
|
5548
|
-
div: (element) => {
|
|
5549
|
-
if (!this.#isGalleryElement(element)) return null
|
|
5550
|
-
|
|
5551
|
-
return {
|
|
5552
|
-
conversion: () => {
|
|
5553
|
-
return {
|
|
5554
|
-
node: $createImageGalleryNode()
|
|
5555
|
-
}
|
|
5556
|
-
},
|
|
5557
|
-
priority: 2
|
|
5558
|
-
}
|
|
5559
|
-
}
|
|
5560
|
-
}
|
|
5561
|
-
}
|
|
5562
|
-
|
|
5563
|
-
static canCollapseWith(node) {
|
|
5564
|
-
return $isImageGalleryNode(node) || this.isValidChild(node)
|
|
5565
|
-
}
|
|
5566
|
-
|
|
5567
|
-
static isValidChild(node) {
|
|
5568
|
-
return $isActionTextAttachmentNode(node) && node.isPreviewableImage
|
|
5569
|
-
}
|
|
5570
|
-
|
|
5571
|
-
static #isGalleryElement(element) {
|
|
5572
|
-
const attachmentChildren = element.querySelectorAll(`:scope > :is(${this.#attachmentTags.join()})`);
|
|
5573
|
-
return element.textContent.trim() === ""
|
|
5574
|
-
&& attachmentChildren.length > 0
|
|
5575
|
-
&& element.children.length === attachmentChildren.length
|
|
5576
|
-
}
|
|
5577
|
-
|
|
5578
|
-
static get #attachmentTags() {
|
|
5579
|
-
return Object.keys(ActionTextAttachmentNode.importDOM())
|
|
5580
|
-
}
|
|
5581
|
-
|
|
5582
|
-
createDOM() {
|
|
5583
|
-
const div = document.createElement("div");
|
|
5584
|
-
div.className = this.#galleryClassNames;
|
|
5585
|
-
return div
|
|
5586
|
-
}
|
|
5587
|
-
|
|
5588
|
-
updateDOM(_prevNode, dom) {
|
|
5589
|
-
dom.className = this.#galleryClassNames;
|
|
5590
|
-
return false
|
|
5591
|
-
}
|
|
5592
|
-
|
|
5593
|
-
canBeEmpty() {
|
|
5594
|
-
// Return `true` to conform to `$isBlock(node)`
|
|
5595
|
-
// We clean-up empty galleries with a transform
|
|
5596
|
-
return true
|
|
5597
|
-
}
|
|
5598
|
-
|
|
5599
|
-
collapseAtStart(_selection) {
|
|
5600
|
-
return true
|
|
5601
|
-
}
|
|
5602
|
-
|
|
5603
|
-
insertNewAfter(selection, restoreSelection) {
|
|
5604
|
-
const selectionBeforeLastChild = selection.anchor.getNode().is(this) && selection.anchor.offset == this.getChildrenSize() - 1;
|
|
5605
|
-
if (selectionBeforeLastChild) {
|
|
5606
|
-
const paragraph = $createParagraphNode();
|
|
5607
|
-
this.insertAfter(paragraph, false);
|
|
5608
|
-
paragraph.insertAfter(this.getLastChild(), false);
|
|
5609
|
-
paragraph.selectEnd();
|
|
5610
|
-
|
|
5611
|
-
// return null as selection has been managed
|
|
5612
|
-
return null
|
|
5613
|
-
}
|
|
5614
|
-
|
|
5615
|
-
const newNode = $createImageGalleryNode();
|
|
5616
|
-
this.insertAfter(newNode, restoreSelection);
|
|
5617
|
-
return newNode
|
|
5618
|
-
}
|
|
5619
|
-
|
|
5620
|
-
getImageAttachments() {
|
|
5621
|
-
const children = this.getChildren();
|
|
5622
|
-
return children.filter($isActionTextAttachmentNode)
|
|
5623
|
-
}
|
|
5624
|
-
|
|
5625
|
-
exportDOM() {
|
|
5626
|
-
const div = document.createElement("div");
|
|
5627
|
-
div.className = this.#galleryClassNames;
|
|
5628
|
-
return { element: div }
|
|
5629
|
-
}
|
|
5630
|
-
|
|
5631
|
-
collapseWith(node, backwards) {
|
|
5632
|
-
if (!ImageGalleryNode.canCollapseWith(node)) return false
|
|
5633
|
-
|
|
5634
|
-
if (backwards) {
|
|
5635
|
-
$insertFirst(this, node);
|
|
5636
|
-
} else {
|
|
5637
|
-
this.append(node);
|
|
5638
|
-
}
|
|
5639
|
-
|
|
5640
|
-
$unwrapAndFilterDescendants(this, ImageGalleryNode.isValidChild);
|
|
5641
|
-
|
|
5642
|
-
return true
|
|
5643
|
-
}
|
|
5644
|
-
|
|
5645
|
-
unwrapEmptyNode() {
|
|
5646
|
-
if (this.isEmpty()) {
|
|
5647
|
-
const paragraph = $createParagraphNode();
|
|
5648
|
-
return this.replace(paragraph)
|
|
5649
|
-
}
|
|
5650
|
-
}
|
|
5651
|
-
|
|
5652
|
-
replaceWithSingularChild() {
|
|
5653
|
-
if (this.#hasSingularChild) {
|
|
5654
|
-
const child = this.getFirstChild();
|
|
5655
|
-
return this.replace($makeSafeForRoot(child))
|
|
5656
|
-
}
|
|
5657
|
-
}
|
|
5658
|
-
|
|
5659
|
-
splitAroundInvalidChild() {
|
|
5660
|
-
for (const child of $firstToLastIterator(this)) {
|
|
5661
|
-
if (ImageGalleryNode.isValidChild(child)) continue
|
|
5966
|
+
#getEdgeCharRect(element, edge) {
|
|
5967
|
+
const walker = document.createTreeWalker(element, 4 /* NodeFilter.SHOW_TEXT */);
|
|
5968
|
+
let textNode;
|
|
5662
5969
|
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5970
|
+
if (edge === "first") {
|
|
5971
|
+
textNode = walker.nextNode();
|
|
5972
|
+
} else {
|
|
5973
|
+
while (walker.nextNode()) textNode = walker.currentNode;
|
|
5974
|
+
}
|
|
5667
5975
|
|
|
5668
|
-
|
|
5669
|
-
if (secondGallery.isEmpty()) secondGallery.remove();
|
|
5976
|
+
if (!textNode || textNode.length === 0) return null
|
|
5670
5977
|
|
|
5671
|
-
|
|
5978
|
+
const range = document.createRange();
|
|
5979
|
+
if (edge === "first") {
|
|
5980
|
+
range.setStart(textNode, 0);
|
|
5981
|
+
range.setEnd(textNode, 1);
|
|
5982
|
+
} else {
|
|
5983
|
+
range.setStart(textNode, textNode.length - 1);
|
|
5984
|
+
range.setEnd(textNode, textNode.length);
|
|
5672
5985
|
}
|
|
5673
|
-
}
|
|
5674
5986
|
|
|
5675
|
-
|
|
5676
|
-
return $splitNode(this, index)
|
|
5987
|
+
return range.getBoundingClientRect()
|
|
5677
5988
|
}
|
|
5989
|
+
}
|
|
5678
5990
|
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5991
|
+
class EditorConfiguration {
|
|
5992
|
+
#editorElement
|
|
5993
|
+
#config
|
|
5682
5994
|
|
|
5683
|
-
|
|
5684
|
-
|
|
5995
|
+
constructor(editorElement) {
|
|
5996
|
+
this.#editorElement = editorElement;
|
|
5997
|
+
this.#config = new Configuration(
|
|
5998
|
+
Lexxy.presets.get("default"),
|
|
5999
|
+
Lexxy.presets.get(editorElement.preset),
|
|
6000
|
+
this.#overrides
|
|
6001
|
+
);
|
|
5685
6002
|
}
|
|
5686
|
-
}
|
|
5687
6003
|
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
}
|
|
6004
|
+
get(path) {
|
|
6005
|
+
return this.#config.get(path)
|
|
6006
|
+
}
|
|
5691
6007
|
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
6008
|
+
get #overrides() {
|
|
6009
|
+
const overrides = {};
|
|
6010
|
+
for (const option of this.#defaultOptions) {
|
|
6011
|
+
const attribute = dasherize(option);
|
|
6012
|
+
if (this.#editorElement.hasAttribute(attribute)) {
|
|
6013
|
+
overrides[option] = this.#parseAttribute(attribute);
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
return overrides
|
|
6017
|
+
}
|
|
5695
6018
|
|
|
5696
|
-
|
|
5697
|
-
|
|
6019
|
+
get #defaultOptions() {
|
|
6020
|
+
return Object.keys(Lexxy.presets.get("default"))
|
|
6021
|
+
}
|
|
5698
6022
|
|
|
5699
|
-
|
|
5700
|
-
|
|
6023
|
+
#parseAttribute(attribute) {
|
|
6024
|
+
const value = this.#editorElement.getAttribute(attribute);
|
|
6025
|
+
try {
|
|
6026
|
+
return JSON.parse(value)
|
|
6027
|
+
} catch {
|
|
6028
|
+
return value
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
5701
6031
|
}
|
|
5702
6032
|
|
|
5703
6033
|
class Uploader {
|
|
@@ -5907,6 +6237,11 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
5907
6237
|
updateDOM(prevNode, dom) {
|
|
5908
6238
|
if (this.uploadError !== prevNode.uploadError) return true
|
|
5909
6239
|
|
|
6240
|
+
const image = dom.querySelector("img");
|
|
6241
|
+
if (image && prevNode.altText !== this.altText) {
|
|
6242
|
+
image.alt = this.altText;
|
|
6243
|
+
}
|
|
6244
|
+
|
|
5910
6245
|
if (prevNode.progress !== this.progress) {
|
|
5911
6246
|
const progress = dom.querySelector("progress");
|
|
5912
6247
|
progress.value = this.progress ?? 0;
|
|
@@ -5940,7 +6275,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
5940
6275
|
}
|
|
5941
6276
|
|
|
5942
6277
|
#createDOMForImage() {
|
|
5943
|
-
return createElement("img")
|
|
6278
|
+
return createElement("img", { alt: this.altText })
|
|
5944
6279
|
}
|
|
5945
6280
|
|
|
5946
6281
|
#createDOMForFile() {
|
|
@@ -6105,7 +6440,6 @@ class AttachmentNodeConversion {
|
|
|
6105
6440
|
const { blob } = this;
|
|
6106
6441
|
return {
|
|
6107
6442
|
sgid: blob.attachable_sgid,
|
|
6108
|
-
altText: blob.filename,
|
|
6109
6443
|
contentType: blob.content_type,
|
|
6110
6444
|
fileName: blob.filename,
|
|
6111
6445
|
fileSize: blob.byte_size,
|
|
@@ -6983,7 +7317,7 @@ class Contents {
|
|
|
6983
7317
|
this.#performTextReplacement(anchorNode, lastIndex, stringToReplace, replacementNodes);
|
|
6984
7318
|
}
|
|
6985
7319
|
|
|
6986
|
-
uploadFiles(files, { selectLast } = {}) {
|
|
7320
|
+
uploadFiles(files, { selectLast, altText } = {}) {
|
|
6987
7321
|
if (!this.editorElement) return // Disposed (e.g. on turbo:before-cache); a late drop can still land here
|
|
6988
7322
|
|
|
6989
7323
|
if (!this.editorElement.supportsAttachments) {
|
|
@@ -6996,6 +7330,10 @@ class Contents {
|
|
|
6996
7330
|
const uploader = Uploader.for(this.editorElement, validFiles);
|
|
6997
7331
|
uploader.$uploadFiles();
|
|
6998
7332
|
|
|
7333
|
+
if (altText && files.length === 1 && uploader.nodes.length === 1) {
|
|
7334
|
+
uploader.nodes[0].getWritable().altText = altText;
|
|
7335
|
+
}
|
|
7336
|
+
|
|
6999
7337
|
if (selectLast && uploader.nodes?.length) {
|
|
7000
7338
|
const lastNode = uploader.nodes.at(-1);
|
|
7001
7339
|
lastNode.selectEnd();
|
|
@@ -7708,46 +8046,51 @@ class Clipboard {
|
|
|
7708
8046
|
}
|
|
7709
8047
|
|
|
7710
8048
|
#handlePastedFiles(clipboardData) {
|
|
7711
|
-
if (
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
if (files.length && this.#isCopiedImageHTML(html)) {
|
|
7717
|
-
this.#uploadFilesPreservingScroll(files);
|
|
7718
|
-
return true
|
|
7719
|
-
}
|
|
7720
|
-
|
|
7721
|
-
if (html && !this.#isLexicalClipboardData(clipboardData)) {
|
|
7722
|
-
this.contents.insertHtml(html, { tag: PASTE_TAG });
|
|
7723
|
-
return true
|
|
7724
|
-
}
|
|
8049
|
+
if (this.editorElement.supportsAttachments) {
|
|
8050
|
+
const html = clipboardData.getData("text/html");
|
|
8051
|
+
const files = clipboardData.files;
|
|
8052
|
+
const copiedImage = files.length && this.#copiedImage(html);
|
|
7725
8053
|
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
8054
|
+
if (copiedImage) {
|
|
8055
|
+
this.#uploadFilesPreservingScroll(files, copiedImage.getAttribute("alt"));
|
|
8056
|
+
return true
|
|
8057
|
+
} else if (html && !this.#isLexicalClipboardData(clipboardData)) {
|
|
8058
|
+
this.contents.insertHtml(html, { tag: PASTE_TAG });
|
|
8059
|
+
return true
|
|
8060
|
+
} else if (files.length) {
|
|
8061
|
+
this.#uploadFilesPreservingScroll(files);
|
|
8062
|
+
return true
|
|
8063
|
+
} else {
|
|
8064
|
+
return false
|
|
8065
|
+
}
|
|
8066
|
+
} else {
|
|
8067
|
+
return false
|
|
7729
8068
|
}
|
|
7730
|
-
|
|
7731
|
-
return false
|
|
7732
8069
|
}
|
|
7733
8070
|
|
|
7734
8071
|
#isLexicalClipboardData(clipboardData) {
|
|
7735
8072
|
return Array.from(clipboardData.types).includes("application/x-lexical-editor")
|
|
7736
8073
|
}
|
|
7737
8074
|
|
|
7738
|
-
#
|
|
7739
|
-
if (
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
const elementChildren = Array.from(doc.body.children);
|
|
8075
|
+
#copiedImage(html) {
|
|
8076
|
+
if (html) {
|
|
8077
|
+
const doc = parseHtml(html);
|
|
8078
|
+
const elementChildren = Array.from(doc.body.children);
|
|
7743
8079
|
|
|
7744
|
-
|
|
8080
|
+
if (elementChildren.length === 1 && elementChildren[0].tagName === "IMG") {
|
|
8081
|
+
return elementChildren[0]
|
|
8082
|
+
} else {
|
|
8083
|
+
return null
|
|
8084
|
+
}
|
|
8085
|
+
} else {
|
|
8086
|
+
return null
|
|
8087
|
+
}
|
|
7745
8088
|
}
|
|
7746
8089
|
|
|
7747
|
-
#uploadFilesPreservingScroll(files) {
|
|
8090
|
+
#uploadFilesPreservingScroll(files, altText) {
|
|
7748
8091
|
this.#preservingScrollPosition(() => {
|
|
7749
8092
|
if (files.length) {
|
|
7750
|
-
this.contents.uploadFiles(files, { selectLast: true });
|
|
8093
|
+
this.contents.uploadFiles(files, { selectLast: true, altText });
|
|
7751
8094
|
}
|
|
7752
8095
|
});
|
|
7753
8096
|
}
|
|
@@ -8860,6 +9203,318 @@ class AttachmentDragAndDrop {
|
|
|
8860
9203
|
}
|
|
8861
9204
|
}
|
|
8862
9205
|
|
|
9206
|
+
// Lexical clears the DOM range on NodeSelection, leaving screen readers in focus
|
|
9207
|
+
// mode to read stray adjacent characters unless we provide a labelled range.
|
|
9208
|
+
class AttachmentFakeSelection {
|
|
9209
|
+
#editor
|
|
9210
|
+
#listeners = new ListenerBin()
|
|
9211
|
+
#container = createElement("span", { className: "lexxy-fake-selection" })
|
|
9212
|
+
|
|
9213
|
+
constructor(editor) {
|
|
9214
|
+
this.#editor = editor;
|
|
9215
|
+
this.#listeners.track(
|
|
9216
|
+
registerLabelledDecoratorSelection(editor, (target) => this.#update(target))
|
|
9217
|
+
);
|
|
9218
|
+
}
|
|
9219
|
+
|
|
9220
|
+
destroy() {
|
|
9221
|
+
this.#listeners.dispose();
|
|
9222
|
+
this.#container.remove();
|
|
9223
|
+
this.#container = null;
|
|
9224
|
+
}
|
|
9225
|
+
|
|
9226
|
+
#update(target) {
|
|
9227
|
+
if (this.#container) {
|
|
9228
|
+
const figure = target && this.#editor.getElementByKey(target.key);
|
|
9229
|
+
if (figure) {
|
|
9230
|
+
const label = target.label || "Attachment";
|
|
9231
|
+
if (this.#container.textContent !== label) this.#container.textContent = label;
|
|
9232
|
+
if (this.#container.parentNode !== figure) figure.append(this.#container);
|
|
9233
|
+
this.#parkSelection();
|
|
9234
|
+
} else {
|
|
9235
|
+
this.#container.remove();
|
|
9236
|
+
}
|
|
9237
|
+
}
|
|
9238
|
+
}
|
|
9239
|
+
|
|
9240
|
+
#parkSelection() {
|
|
9241
|
+
const textNode = this.#container.firstChild;
|
|
9242
|
+
if (textNode && this.#isEditorFocused && !this.#isAlreadyParkedHere) {
|
|
9243
|
+
const range = document.createRange();
|
|
9244
|
+
range.setStart(textNode, 0);
|
|
9245
|
+
range.setEnd(textNode, textNode.length);
|
|
9246
|
+
|
|
9247
|
+
const selection = window.getSelection();
|
|
9248
|
+
selection.removeAllRanges();
|
|
9249
|
+
selection.addRange(range);
|
|
9250
|
+
}
|
|
9251
|
+
}
|
|
9252
|
+
|
|
9253
|
+
// Parking the range while a nested control is focused steals its screen reader cursor.
|
|
9254
|
+
get #isEditorFocused() {
|
|
9255
|
+
const root = this.#editor.getRootElement();
|
|
9256
|
+
return root != null && document.activeElement === root
|
|
9257
|
+
}
|
|
9258
|
+
|
|
9259
|
+
get #isAlreadyParkedHere() {
|
|
9260
|
+
const selection = window.getSelection();
|
|
9261
|
+
if (selection?.rangeCount === 1) {
|
|
9262
|
+
const textNode = this.#container.firstChild;
|
|
9263
|
+
return selection.anchorNode === textNode
|
|
9264
|
+
&& selection.focusNode === textNode
|
|
9265
|
+
&& selection.anchorOffset === 0
|
|
9266
|
+
&& selection.focusOffset === textNode.length
|
|
9267
|
+
} else {
|
|
9268
|
+
return false
|
|
9269
|
+
}
|
|
9270
|
+
}
|
|
9271
|
+
}
|
|
9272
|
+
|
|
9273
|
+
class AttachmentKeyboardMove {
|
|
9274
|
+
#editor
|
|
9275
|
+
#listeners = new ListenerBin()
|
|
9276
|
+
#scrollFrame
|
|
9277
|
+
|
|
9278
|
+
constructor(editor) {
|
|
9279
|
+
this.#editor = editor;
|
|
9280
|
+
|
|
9281
|
+
this.#listeners.track(
|
|
9282
|
+
editor.registerCommand(KEY_DOWN_COMMAND, this.#tryReorder, COMMAND_PRIORITY_HIGH)
|
|
9283
|
+
);
|
|
9284
|
+
}
|
|
9285
|
+
|
|
9286
|
+
destroy() {
|
|
9287
|
+
this.#listeners.dispose();
|
|
9288
|
+
cancelAnimationFrame(this.#scrollFrame);
|
|
9289
|
+
}
|
|
9290
|
+
|
|
9291
|
+
#tryReorder = (event) => {
|
|
9292
|
+
const node = $singleSelectedNode();
|
|
9293
|
+
if (event.altKey && event.shiftKey && event.key.startsWith("Arrow") && $isActionTextAttachmentNode(node)) {
|
|
9294
|
+
event.preventDefault();
|
|
9295
|
+
const message = this.#executeMove(event.key, node);
|
|
9296
|
+
$setSelection($createNodeSelectionWith(node));
|
|
9297
|
+
announceFromEditor(this.#editor, message);
|
|
9298
|
+
this.#keepInView(node);
|
|
9299
|
+
return true
|
|
9300
|
+
} else {
|
|
9301
|
+
return false
|
|
9302
|
+
}
|
|
9303
|
+
}
|
|
9304
|
+
|
|
9305
|
+
#executeMove(key, node) {
|
|
9306
|
+
switch (key) {
|
|
9307
|
+
case "ArrowUp": return new AttachmentReorder(node, "previous").moveVertically()
|
|
9308
|
+
case "ArrowDown": return new AttachmentReorder(node, "next").moveVertically()
|
|
9309
|
+
case "ArrowLeft": return new AttachmentReorder(node, "previous").reorderInGallery()
|
|
9310
|
+
case "ArrowRight": return new AttachmentReorder(node, "next").reorderInGallery()
|
|
9311
|
+
default: return null
|
|
9312
|
+
}
|
|
9313
|
+
}
|
|
9314
|
+
|
|
9315
|
+
// Node selections don't scroll into view automatically like a caret does.
|
|
9316
|
+
#keepInView(node) {
|
|
9317
|
+
const key = node.getKey();
|
|
9318
|
+
cancelAnimationFrame(this.#scrollFrame);
|
|
9319
|
+
this.#scrollFrame = requestAnimationFrame(() => {
|
|
9320
|
+
this.#editor.getElementByKey(key)?.scrollIntoView({ block: "nearest" });
|
|
9321
|
+
});
|
|
9322
|
+
}
|
|
9323
|
+
}
|
|
9324
|
+
|
|
9325
|
+
class AttachmentReorder {
|
|
9326
|
+
constructor(node, direction) {
|
|
9327
|
+
this.node = node;
|
|
9328
|
+
this.direction = direction;
|
|
9329
|
+
}
|
|
9330
|
+
|
|
9331
|
+
moveVertically() {
|
|
9332
|
+
if ($isImageGalleryNode(this.node.getParent())) {
|
|
9333
|
+
return this.#extractFromGallery()
|
|
9334
|
+
} else {
|
|
9335
|
+
return this.#moveBlock() || this.#blockedMessage
|
|
9336
|
+
}
|
|
9337
|
+
}
|
|
9338
|
+
|
|
9339
|
+
reorderInGallery() {
|
|
9340
|
+
const sibling = $getSiblingCaret(this.node, this.direction).getNodeAtCaret();
|
|
9341
|
+
if ($isImageGalleryNode(this.node.getParent()) && sibling) {
|
|
9342
|
+
$getSiblingCaret(sibling, this.direction).insert(this.node);
|
|
9343
|
+
return "Image reordered in gallery"
|
|
9344
|
+
} else {
|
|
9345
|
+
return this.#blockedMessage
|
|
9346
|
+
}
|
|
9347
|
+
}
|
|
9348
|
+
|
|
9349
|
+
#extractFromGallery() {
|
|
9350
|
+
const gallery = this.node.getParent();
|
|
9351
|
+
$getSiblingCaret(gallery, this.direction).insert(this.node);
|
|
9352
|
+
return "Image removed from gallery"
|
|
9353
|
+
}
|
|
9354
|
+
|
|
9355
|
+
#moveBlock() {
|
|
9356
|
+
const target = this.#skipProvisional($getSiblingCaret(this.node, this.direction).getNodeAtCaret());
|
|
9357
|
+
if (this.#canJoinGallery(target)) {
|
|
9358
|
+
return this.#joinGallery(target)
|
|
9359
|
+
} else if (this.#canFormGalleryWith(target)) {
|
|
9360
|
+
return this.#formGalleryWith(target)
|
|
9361
|
+
} else if (target) {
|
|
9362
|
+
return this.#swapWith(target)
|
|
9363
|
+
} else {
|
|
9364
|
+
return null
|
|
9365
|
+
}
|
|
9366
|
+
}
|
|
9367
|
+
|
|
9368
|
+
#skipProvisional(node) {
|
|
9369
|
+
let current = node;
|
|
9370
|
+
while (current && $isProvisionalParagraphNode(current)) {
|
|
9371
|
+
current = $getSiblingCaret(current, this.direction).getNodeAtCaret();
|
|
9372
|
+
}
|
|
9373
|
+
return current
|
|
9374
|
+
}
|
|
9375
|
+
|
|
9376
|
+
#canJoinGallery(target) {
|
|
9377
|
+
return $isImageGalleryNode(target) && ImageGalleryNode.isValidChild(this.node)
|
|
9378
|
+
}
|
|
9379
|
+
|
|
9380
|
+
#joinGallery(gallery) {
|
|
9381
|
+
$getChildCaret(gallery, this.direction).insert(this.node);
|
|
9382
|
+
return "Image added to gallery"
|
|
9383
|
+
}
|
|
9384
|
+
|
|
9385
|
+
#canFormGalleryWith(target) {
|
|
9386
|
+
return $isActionTextAttachmentNode(target)
|
|
9387
|
+
&& target.isPreviewableImage
|
|
9388
|
+
&& this.node.isPreviewableImage
|
|
9389
|
+
}
|
|
9390
|
+
|
|
9391
|
+
#formGalleryWith(target) {
|
|
9392
|
+
if ($findOrCreateGalleryForImage(target)) {
|
|
9393
|
+
$getSiblingCaret(target, flipDirection(this.direction)).insert(this.node);
|
|
9394
|
+
return "Gallery created"
|
|
9395
|
+
} else {
|
|
9396
|
+
return null
|
|
9397
|
+
}
|
|
9398
|
+
}
|
|
9399
|
+
|
|
9400
|
+
#swapWith(target) {
|
|
9401
|
+
$getSiblingCaret(target, this.direction).insert(this.node);
|
|
9402
|
+
if (this.direction === "next") {
|
|
9403
|
+
return "Attachment moved down"
|
|
9404
|
+
} else {
|
|
9405
|
+
return "Attachment moved up"
|
|
9406
|
+
}
|
|
9407
|
+
}
|
|
9408
|
+
|
|
9409
|
+
get #blockedMessage() {
|
|
9410
|
+
if (this.direction === "next") {
|
|
9411
|
+
return "Already at the end"
|
|
9412
|
+
} else {
|
|
9413
|
+
return "Already at the start"
|
|
9414
|
+
}
|
|
9415
|
+
}
|
|
9416
|
+
}
|
|
9417
|
+
|
|
9418
|
+
// Using a mention's label as avatar alt text can interrupt line-by-line reading,
|
|
9419
|
+
// but character navigation needs it near the caret to announce the mention.
|
|
9420
|
+
class CustomAttachmentLabels {
|
|
9421
|
+
#editor
|
|
9422
|
+
#listeners = new ListenerBin()
|
|
9423
|
+
#active = null
|
|
9424
|
+
|
|
9425
|
+
constructor(editor) {
|
|
9426
|
+
this.#editor = editor;
|
|
9427
|
+
this.#listeners.track(
|
|
9428
|
+
editor.registerUpdateListener(this.#updateLabel),
|
|
9429
|
+
// A blur command opens a Lexical update that can pull focus back into the editor.
|
|
9430
|
+
editor.registerRootListener(rootElement => {
|
|
9431
|
+
if (rootElement) {
|
|
9432
|
+
return registerEventListener(rootElement, "blur", () => this.#restoreLabel())
|
|
9433
|
+
}
|
|
9434
|
+
})
|
|
9435
|
+
);
|
|
9436
|
+
}
|
|
9437
|
+
|
|
9438
|
+
destroy() {
|
|
9439
|
+
this.#listeners.dispose();
|
|
9440
|
+
this.#restoreLabel();
|
|
9441
|
+
}
|
|
9442
|
+
|
|
9443
|
+
#updateLabel = ({ editorState }) => {
|
|
9444
|
+
if (document.activeElement === this.#editor.getRootElement()) {
|
|
9445
|
+
editorState.read(() => {
|
|
9446
|
+
const selection = $getSelection();
|
|
9447
|
+
const attachment = customAttachmentSelectedBy(selection) || customAttachmentBesideAnchor(selection);
|
|
9448
|
+
|
|
9449
|
+
if (attachment) {
|
|
9450
|
+
this.#exposeLabelOn(attachment);
|
|
9451
|
+
} else {
|
|
9452
|
+
this.#restoreLabel();
|
|
9453
|
+
}
|
|
9454
|
+
});
|
|
9455
|
+
} else {
|
|
9456
|
+
this.#restoreLabel();
|
|
9457
|
+
}
|
|
9458
|
+
}
|
|
9459
|
+
|
|
9460
|
+
#exposeLabelOn(attachment) {
|
|
9461
|
+
const figure = this.#editor.getElementByKey(attachment.getKey());
|
|
9462
|
+
if (figure && figure !== this.#active?.figure) {
|
|
9463
|
+
this.#restoreLabel();
|
|
9464
|
+
attachment.exposeLabel(figure);
|
|
9465
|
+
this.#active = { figure, attachment };
|
|
9466
|
+
}
|
|
9467
|
+
}
|
|
9468
|
+
|
|
9469
|
+
#restoreLabel() {
|
|
9470
|
+
if (this.#active) {
|
|
9471
|
+
const { attachment, figure } = this.#active;
|
|
9472
|
+
attachment.restoreLabel(figure);
|
|
9473
|
+
this.#active = null;
|
|
9474
|
+
}
|
|
9475
|
+
}
|
|
9476
|
+
}
|
|
9477
|
+
|
|
9478
|
+
function customAttachmentSelectedBy(selection) {
|
|
9479
|
+
if ($isNodeSelection(selection)) {
|
|
9480
|
+
const nodes = selection.getNodes();
|
|
9481
|
+
if (nodes.length === 1 && $isCustomActionTextAttachmentNode(nodes[0])) {
|
|
9482
|
+
return nodes[0]
|
|
9483
|
+
} else {
|
|
9484
|
+
return null
|
|
9485
|
+
}
|
|
9486
|
+
} else {
|
|
9487
|
+
return null
|
|
9488
|
+
}
|
|
9489
|
+
}
|
|
9490
|
+
|
|
9491
|
+
// Screen readers need the label one character early to announce it as the caret
|
|
9492
|
+
// crosses the figure.
|
|
9493
|
+
function customAttachmentBesideAnchor(selection) {
|
|
9494
|
+
if ($isRangeSelection(selection) && selection.isCollapsed()) {
|
|
9495
|
+
const anchor = selection.anchor;
|
|
9496
|
+
const parent = anchor.getNode();
|
|
9497
|
+
const candidates = [];
|
|
9498
|
+
|
|
9499
|
+
if (anchor.type === "text") {
|
|
9500
|
+
const toEnd = parent.getTextContentSize() - anchor.offset;
|
|
9501
|
+
if (toEnd <= 1) candidates.push({ node: parent.getNextSibling(), distance: toEnd });
|
|
9502
|
+
if (anchor.offset <= 1) candidates.push({ node: parent.getPreviousSibling(), distance: anchor.offset });
|
|
9503
|
+
} else {
|
|
9504
|
+
candidates.push({ node: parent.getChildAtIndex(anchor.offset), distance: 0 });
|
|
9505
|
+
candidates.push({ node: parent.getChildAtIndex(anchor.offset - 1), distance: 0 });
|
|
9506
|
+
}
|
|
9507
|
+
|
|
9508
|
+
const nearest = candidates
|
|
9509
|
+
.filter(({ node }) => $isCustomActionTextAttachmentNode(node))
|
|
9510
|
+
.sort((a, b) => a.distance - b.distance)[0];
|
|
9511
|
+
|
|
9512
|
+
return nearest?.node ?? null
|
|
9513
|
+
} else {
|
|
9514
|
+
return null
|
|
9515
|
+
}
|
|
9516
|
+
}
|
|
9517
|
+
|
|
8863
9518
|
const ATTACHMENT_ATTRIBUTES = [ "alt", "caption", "content", "content-type", "data-direct-upload-id",
|
|
8864
9519
|
"data-sgid", "filename", "filesize", "height", "presentation", "previewable", "sgid", "url", "width" ];
|
|
8865
9520
|
|
|
@@ -8886,12 +9541,19 @@ class AttachmentsExtension extends LexxyExtension {
|
|
|
8886
9541
|
],
|
|
8887
9542
|
register: (editor) => {
|
|
8888
9543
|
const dragAndDrop = new AttachmentDragAndDrop(editor);
|
|
9544
|
+
const keyboardMove = new AttachmentKeyboardMove(editor);
|
|
9545
|
+
const fakeSelection = new AttachmentFakeSelection(editor);
|
|
9546
|
+
const customAttachmentLabels = new CustomAttachmentLabels(editor);
|
|
8889
9547
|
|
|
8890
9548
|
return mergeRegister(
|
|
8891
9549
|
editor.registerNodeTransform(ActionTextAttachmentNode, $extractAttachmentFromParagraph),
|
|
8892
9550
|
editor.registerCommand(DELETE_CHARACTER_COMMAND, $collapseIntoGallery, COMMAND_PRIORITY_NORMAL),
|
|
9551
|
+
editor.registerCommand(KEY_TAB_COMMAND, $focusCaptionFromSelectedAttachment, COMMAND_PRIORITY_HIGH),
|
|
8893
9552
|
editor.registerMutationListener(ActionTextAttachmentUploadNode, this.#handleUploadMutations.bind(this)),
|
|
8894
|
-
() => dragAndDrop.destroy()
|
|
9553
|
+
() => dragAndDrop.destroy(),
|
|
9554
|
+
() => keyboardMove.destroy(),
|
|
9555
|
+
() => fakeSelection.destroy(),
|
|
9556
|
+
() => customAttachmentLabels.destroy()
|
|
8895
9557
|
)
|
|
8896
9558
|
}
|
|
8897
9559
|
})
|
|
@@ -8989,6 +9651,16 @@ function $collapseAtGalleryEdge(anchor, backwards) {
|
|
|
8989
9651
|
}
|
|
8990
9652
|
}
|
|
8991
9653
|
|
|
9654
|
+
function $focusCaptionFromSelectedAttachment(event) {
|
|
9655
|
+
const node = $singleSelectedNode();
|
|
9656
|
+
if (!event.shiftKey && $isActionTextAttachmentNode(node) && node.focusCaption()) {
|
|
9657
|
+
event.preventDefault();
|
|
9658
|
+
return true
|
|
9659
|
+
} else {
|
|
9660
|
+
return false
|
|
9661
|
+
}
|
|
9662
|
+
}
|
|
9663
|
+
|
|
8992
9664
|
// Manual selection handling to prevent Lexical merging the gallery with a <p> and unwrapping it
|
|
8993
9665
|
function $moveSelectionBeforeGallery(anchor) {
|
|
8994
9666
|
const previousNode = anchor.getNode().getPreviousSibling();
|
|
@@ -9718,8 +10390,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
9718
10390
|
#historyState = { undo: false, redo: false }
|
|
9719
10391
|
|
|
9720
10392
|
#validity = new Map()
|
|
10393
|
+
#validationAttempted = false
|
|
9721
10394
|
#validationTextArea = document.createElement("textarea")
|
|
9722
10395
|
#uploadRequests
|
|
10396
|
+
#liveRegion
|
|
9723
10397
|
|
|
9724
10398
|
constructor() {
|
|
9725
10399
|
super();
|
|
@@ -9750,6 +10424,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
9750
10424
|
this.clipboard = new Clipboard(this);
|
|
9751
10425
|
this.#disposables.push(this.clipboard);
|
|
9752
10426
|
|
|
10427
|
+
this.#liveRegion = this.querySelector("lexxy-live-region") ?? createElement("lexxy-live-region");
|
|
10428
|
+
this.append(this.#liveRegion);
|
|
10429
|
+
this.#disposables.push(this.#liveRegion);
|
|
10430
|
+
|
|
9753
10431
|
this.adapter = new BrowserAdapter();
|
|
9754
10432
|
this.#uploadRequests = new UploadRequests();
|
|
9755
10433
|
|
|
@@ -9797,10 +10475,13 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
9797
10475
|
}
|
|
9798
10476
|
|
|
9799
10477
|
requiredChangedCallback() {
|
|
10478
|
+
if (this.editorContentElement) this.#synchronizeAriaAttribute("aria-required");
|
|
9800
10479
|
if (this.isConnected) this.#requestValidityRefresh();
|
|
9801
10480
|
}
|
|
9802
10481
|
|
|
9803
10482
|
formResetCallback() {
|
|
10483
|
+
this.#validationAttempted = false;
|
|
10484
|
+
this.#synchronizeAriaAttribute("aria-invalid");
|
|
9804
10485
|
this.value = this.#initialValue;
|
|
9805
10486
|
this.editor.dispatchCommand(CLEAR_HISTORY_COMMAND, undefined);
|
|
9806
10487
|
}
|
|
@@ -9827,6 +10508,15 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
9827
10508
|
return this.internals.validity
|
|
9828
10509
|
}
|
|
9829
10510
|
|
|
10511
|
+
get validationMessage() {
|
|
10512
|
+
return this.internals.validationMessage
|
|
10513
|
+
}
|
|
10514
|
+
|
|
10515
|
+
setCustomValidity(message) {
|
|
10516
|
+
this.#validationTextArea.setCustomValidity(message);
|
|
10517
|
+
this.#refreshValidity();
|
|
10518
|
+
}
|
|
10519
|
+
|
|
9830
10520
|
checkValidity() {
|
|
9831
10521
|
return this.internals.checkValidity()
|
|
9832
10522
|
}
|
|
@@ -9895,6 +10585,16 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
9895
10585
|
return dispatch(this, "lexxy:file-accept", { file }, true)
|
|
9896
10586
|
}
|
|
9897
10587
|
|
|
10588
|
+
announce(message) {
|
|
10589
|
+
if (message) {
|
|
10590
|
+
if (typeof document.ariaNotify === "function") {
|
|
10591
|
+
document.ariaNotify(message, { priority: "high" });
|
|
10592
|
+
} else {
|
|
10593
|
+
this.#liveRegion?.announce(message);
|
|
10594
|
+
}
|
|
10595
|
+
}
|
|
10596
|
+
}
|
|
10597
|
+
|
|
9898
10598
|
$generateNodesFromDOM(doc, { editor = this.editor } = {}) {
|
|
9899
10599
|
let nodes = $generateNodesFromDOM(editor, doc);
|
|
9900
10600
|
if ($hasUpdateTag(PASTE_TAG)) nodes = $convertInlineImageDataURIs(nodes, this);
|
|
@@ -10054,6 +10754,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10054
10754
|
this.#attachDebugHooks();
|
|
10055
10755
|
this.#attachToolbar();
|
|
10056
10756
|
this.#resetBeforeTurboCaches();
|
|
10757
|
+
this.#observeAriaAttributes();
|
|
10057
10758
|
|
|
10058
10759
|
this.#setInternalFormValue(this.value, { suppressEvent: true });
|
|
10059
10760
|
this.#synchronizeWithChanges();
|
|
@@ -10126,7 +10827,9 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10126
10827
|
classList: "lexxy-editor__content",
|
|
10127
10828
|
contenteditable: true,
|
|
10128
10829
|
role: "textbox",
|
|
10129
|
-
"aria-multiline":
|
|
10830
|
+
"aria-multiline": this.supportsMultiLine,
|
|
10831
|
+
"aria-required": this.required,
|
|
10832
|
+
"aria-invalid": false,
|
|
10130
10833
|
"aria-label": this.#labelText,
|
|
10131
10834
|
placeholder: this.getAttribute("placeholder")
|
|
10132
10835
|
});
|
|
@@ -10159,6 +10862,46 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10159
10862
|
return Array.from(this.attributes).filter(attribute => attribute.name.startsWith("aria-"))
|
|
10160
10863
|
}
|
|
10161
10864
|
|
|
10865
|
+
#observeAriaAttributes() {
|
|
10866
|
+
const observer = new MutationObserver(mutations => {
|
|
10867
|
+
for (const { attributeName } of mutations) {
|
|
10868
|
+
if (attributeName.startsWith("aria-")) {
|
|
10869
|
+
this.#synchronizeAriaAttribute(attributeName);
|
|
10870
|
+
} else if (attributeName === "single-line") {
|
|
10871
|
+
this.#synchronizeAriaAttribute("aria-multiline");
|
|
10872
|
+
}
|
|
10873
|
+
}
|
|
10874
|
+
});
|
|
10875
|
+
observer.observe(this, { attributes: true });
|
|
10876
|
+
|
|
10877
|
+
this.#listeners.track(
|
|
10878
|
+
() => observer.disconnect(),
|
|
10879
|
+
registerEventListener(this, "invalid", () => {
|
|
10880
|
+
this.#validationAttempted = true;
|
|
10881
|
+
this.#synchronizeAriaAttribute("aria-invalid");
|
|
10882
|
+
})
|
|
10883
|
+
);
|
|
10884
|
+
}
|
|
10885
|
+
|
|
10886
|
+
#synchronizeAriaAttribute(name) {
|
|
10887
|
+
const value = this.getAttribute(name) ?? this.#defaultAriaValue(name);
|
|
10888
|
+
if (value === null) {
|
|
10889
|
+
this.editorContentElement.removeAttribute(name);
|
|
10890
|
+
} else if (this.editorContentElement.getAttribute(name) !== String(value)) {
|
|
10891
|
+
this.editorContentElement.setAttribute(name, value);
|
|
10892
|
+
}
|
|
10893
|
+
}
|
|
10894
|
+
|
|
10895
|
+
#defaultAriaValue(name) {
|
|
10896
|
+
switch (name) {
|
|
10897
|
+
case "aria-label": return this.#labelText
|
|
10898
|
+
case "aria-required": return this.required
|
|
10899
|
+
case "aria-multiline": return this.supportsMultiLine
|
|
10900
|
+
case "aria-invalid": return this.#validationAttempted && !this.validity.valid
|
|
10901
|
+
default: return null
|
|
10902
|
+
}
|
|
10903
|
+
}
|
|
10904
|
+
|
|
10162
10905
|
#setInternalFormValue(html, { suppressEvent = false } = {}) {
|
|
10163
10906
|
const changed = html !== this.#previousInternalFormValue;
|
|
10164
10907
|
|
|
@@ -10215,11 +10958,16 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10215
10958
|
#refreshValidity() {
|
|
10216
10959
|
this.#refreshInternalValidity();
|
|
10217
10960
|
const { validity, message } = this.#calculateValidity();
|
|
10218
|
-
this.internals.setValidity(validity, message, this.editorContentElement);
|
|
10961
|
+
this.internals.setValidity(validity, message, this.editorContentElement ?? undefined);
|
|
10962
|
+
if (this.editorContentElement) {
|
|
10963
|
+
this.#synchronizeAriaAttribute("aria-invalid");
|
|
10964
|
+
}
|
|
10219
10965
|
}
|
|
10220
10966
|
|
|
10221
10967
|
#refreshInternalValidity() {
|
|
10222
|
-
|
|
10968
|
+
if (this.editorContentElement) {
|
|
10969
|
+
this.#validationTextArea.required = this.required && this.isBlank;
|
|
10970
|
+
}
|
|
10223
10971
|
const flags = this.#validationTextArea.validity;
|
|
10224
10972
|
const message = this.#validationTextArea.validationMessage;
|
|
10225
10973
|
|
|
@@ -10234,12 +10982,14 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10234
10982
|
// internal TextArea's ValidityState can contain `valid: true`
|
|
10235
10983
|
if (flags.valid === true) continue
|
|
10236
10984
|
|
|
10985
|
+
let hasError = false;
|
|
10237
10986
|
for (const flag in flags) {
|
|
10238
10987
|
if (flags[flag]) {
|
|
10239
10988
|
validity[flag] = true;
|
|
10240
|
-
|
|
10989
|
+
hasError = true;
|
|
10241
10990
|
}
|
|
10242
10991
|
}
|
|
10992
|
+
if (hasError) messages.push(message);
|
|
10243
10993
|
}
|
|
10244
10994
|
|
|
10245
10995
|
return { validity, message: messages.join("\n") }
|
|
@@ -10260,6 +11010,9 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10260
11010
|
);
|
|
10261
11011
|
this.#registerTableComponents();
|
|
10262
11012
|
this.#registerCodeLanguagePicker();
|
|
11013
|
+
if (this.supportsAttachments) {
|
|
11014
|
+
this.#registerAttachmentToolbar();
|
|
11015
|
+
}
|
|
10263
11016
|
if (this.supportsMarkdown) {
|
|
10264
11017
|
const transformers = [ ...TRANSFORMERS, HORIZONTAL_DIVIDER ];
|
|
10265
11018
|
registered.push(
|
|
@@ -10288,6 +11041,15 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10288
11041
|
this.#disposables.push(codeLanguagePicker);
|
|
10289
11042
|
}
|
|
10290
11043
|
|
|
11044
|
+
#registerAttachmentToolbar() {
|
|
11045
|
+
let attachmentToolbar = this.querySelector("lexxy-attachment-toolbar");
|
|
11046
|
+
attachmentToolbar ??= createElement("lexxy-attachment-toolbar");
|
|
11047
|
+
this.append(attachmentToolbar);
|
|
11048
|
+
this.#disposables.push(attachmentToolbar);
|
|
11049
|
+
this.captionEditor = new CaptionEditor(this);
|
|
11050
|
+
this.#disposables.push(this.captionEditor);
|
|
11051
|
+
}
|
|
11052
|
+
|
|
10291
11053
|
#handleEnter() {
|
|
10292
11054
|
// We can't prevent these externally using regular keydown because Lexical handles it first.
|
|
10293
11055
|
this.#listeners.track(this.editor.registerCommand(
|
|
@@ -10496,21 +11258,22 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10496
11258
|
// — triggering at most one forced reflow. The previous implementation interleaved
|
|
10497
11259
|
// setProperty/getComputedStyle/removeProperty on the same element, forcing a style
|
|
10498
11260
|
// recalc on every iteration during editor initialization.
|
|
10499
|
-
#resolveColors(property,
|
|
11261
|
+
#resolveColors(property, buttons) {
|
|
10500
11262
|
const container = document.createElement("span");
|
|
10501
11263
|
container.style.display = "none";
|
|
10502
11264
|
|
|
10503
|
-
const resolvers =
|
|
11265
|
+
const resolvers = normalizeColorButtons(buttons).map(({ value, label }) => {
|
|
10504
11266
|
const element = document.createElement("span");
|
|
10505
|
-
element.style.setProperty(property,
|
|
11267
|
+
element.style.setProperty(property, value);
|
|
10506
11268
|
container.appendChild(element);
|
|
10507
|
-
return { element, name:
|
|
11269
|
+
return { element, name: value, label }
|
|
10508
11270
|
});
|
|
10509
11271
|
|
|
10510
11272
|
styleResolverRoot().appendChild(container);
|
|
10511
11273
|
|
|
10512
|
-
const resolved = resolvers.map(({ element, name }) => ({
|
|
11274
|
+
const resolved = resolvers.map(({ element, name, label }) => ({
|
|
10513
11275
|
name,
|
|
11276
|
+
label,
|
|
10514
11277
|
value: window.getComputedStyle(element).getPropertyValue(property)
|
|
10515
11278
|
}));
|
|
10516
11279
|
|
|
@@ -10544,6 +11307,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10544
11307
|
|
|
10545
11308
|
#resetValidity() {
|
|
10546
11309
|
this.#validity = new Map();
|
|
11310
|
+
this.#validationAttempted = false;
|
|
10547
11311
|
}
|
|
10548
11312
|
}
|
|
10549
11313
|
|
|
@@ -10574,6 +11338,39 @@ function $getReadableTextContent(node) {
|
|
|
10574
11338
|
return node.getTextContent()
|
|
10575
11339
|
}
|
|
10576
11340
|
|
|
11341
|
+
const REMOVAL_DELAY = 1000;
|
|
11342
|
+
|
|
11343
|
+
class LiveRegion extends HTMLElement {
|
|
11344
|
+
#additions = createElement("span", { ariaLive: "assertive", ariaRelevant: "additions" })
|
|
11345
|
+
#removalTimeouts = new Set()
|
|
11346
|
+
|
|
11347
|
+
connectedCallback() {
|
|
11348
|
+
this.replaceChildren(this.#additions);
|
|
11349
|
+
}
|
|
11350
|
+
|
|
11351
|
+
disconnectedCallback() {
|
|
11352
|
+
this.dispose();
|
|
11353
|
+
}
|
|
11354
|
+
|
|
11355
|
+
dispose() {
|
|
11356
|
+
for (const timeout of this.#removalTimeouts) clearTimeout(timeout);
|
|
11357
|
+
this.#removalTimeouts.clear();
|
|
11358
|
+
this.#additions.replaceChildren();
|
|
11359
|
+
this.replaceChildren();
|
|
11360
|
+
}
|
|
11361
|
+
|
|
11362
|
+
announce(message) {
|
|
11363
|
+
const announcement = createElement("div", { textContent: message });
|
|
11364
|
+
this.#additions.appendChild(announcement);
|
|
11365
|
+
|
|
11366
|
+
const timeout = setTimeout(() => {
|
|
11367
|
+
announcement.remove();
|
|
11368
|
+
this.#removalTimeouts.delete(timeout);
|
|
11369
|
+
}, REMOVAL_DELAY);
|
|
11370
|
+
this.#removalTimeouts.add(timeout);
|
|
11371
|
+
}
|
|
11372
|
+
}
|
|
11373
|
+
|
|
10577
11374
|
class BaseSource {
|
|
10578
11375
|
// Template method to override
|
|
10579
11376
|
async buildListItems(filter = "") {
|
|
@@ -11456,50 +12253,373 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
11456
12253
|
}
|
|
11457
12254
|
}
|
|
11458
12255
|
|
|
12256
|
+
class AlternativeTextDialog {
|
|
12257
|
+
#editor
|
|
12258
|
+
#editorElement
|
|
12259
|
+
#dialog
|
|
12260
|
+
#input
|
|
12261
|
+
#nodeKey
|
|
12262
|
+
#elementToRefocus
|
|
12263
|
+
#listeners = new ListenerBin()
|
|
12264
|
+
|
|
12265
|
+
constructor(editorElement) {
|
|
12266
|
+
this.#editor = editorElement.editor;
|
|
12267
|
+
this.#editorElement = editorElement;
|
|
12268
|
+
this.#dialog = createElement("dialog", { className: "lexxy-alternative-text-dialog", ariaLabel: "Alternative text" });
|
|
12269
|
+
this.#input = createElement("textarea", { rows: 1, autofocus: true, ariaLabel: "Description", placeholder: "Keep it short, but enough to convey the context…" });
|
|
12270
|
+
|
|
12271
|
+
const title = createElement("h2", { textContent: "Describe the image for people who can't see it:" });
|
|
12272
|
+
|
|
12273
|
+
const actions = createElement("div", { className: "lexxy-alternative-text-dialog__actions" });
|
|
12274
|
+
const cancel = createElement("button", { type: "button", textContent: "Cancel" });
|
|
12275
|
+
const save = createElement("button", { type: "button", className: "lexxy-alternative-text-dialog__save", textContent: "Save" });
|
|
12276
|
+
actions.append(cancel, save);
|
|
12277
|
+
|
|
12278
|
+
const controls = createElement("div", { className: "lexxy-alternative-text-dialog__controls" });
|
|
12279
|
+
controls.append(this.#input, actions);
|
|
12280
|
+
this.#dialog.append(title, controls);
|
|
12281
|
+
editorElement.appendChild(this.#dialog);
|
|
12282
|
+
|
|
12283
|
+
this.#listeners.track(
|
|
12284
|
+
registerEventListener(cancel, "click", () => this.#close()),
|
|
12285
|
+
registerEventListener(save, "click", () => this.#save()),
|
|
12286
|
+
registerEventListener(this.#dialog, "keydown", this.#handleKeyDown),
|
|
12287
|
+
registerEventListener(document, "pointerdown", this.#closeOnClickOutside)
|
|
12288
|
+
);
|
|
12289
|
+
}
|
|
12290
|
+
|
|
12291
|
+
dispose() {
|
|
12292
|
+
this.#dialog.remove();
|
|
12293
|
+
this.#listeners.dispose();
|
|
12294
|
+
}
|
|
12295
|
+
|
|
12296
|
+
open(nodeKey) {
|
|
12297
|
+
const description = this.#editor.read(() => {
|
|
12298
|
+
const node = $getNodeByKey(nodeKey);
|
|
12299
|
+
if ($isActionTextAttachmentNode(node)) {
|
|
12300
|
+
return node.altText
|
|
12301
|
+
}
|
|
12302
|
+
});
|
|
12303
|
+
|
|
12304
|
+
if (description !== undefined) {
|
|
12305
|
+
this.#nodeKey = nodeKey;
|
|
12306
|
+
this.#input.value = description;
|
|
12307
|
+
this.#anchorBelowAttachment(nodeKey);
|
|
12308
|
+
this.#elementToRefocus = document.activeElement;
|
|
12309
|
+
this.#editorElement.querySelector("lexxy-toolbar")?.closeDropdowns();
|
|
12310
|
+
this.#dialog.show();
|
|
12311
|
+
this.#input.focus();
|
|
12312
|
+
}
|
|
12313
|
+
}
|
|
12314
|
+
|
|
12315
|
+
#anchorBelowAttachment(nodeKey) {
|
|
12316
|
+
const figureElement = this.#editor.getElementByKey(nodeKey);
|
|
12317
|
+
if (figureElement) {
|
|
12318
|
+
const rect = figureElement.getBoundingClientRect();
|
|
12319
|
+
const editorRect = this.#editorElement.getBoundingClientRect();
|
|
12320
|
+
const bottom = rect.bottom - editorRect.top - this.#editorElement.clientTop + this.#editorElement.scrollTop;
|
|
12321
|
+
this.#dialog.style.setProperty("--lexxy-anchor-bottom", `${bottom}px`);
|
|
12322
|
+
}
|
|
12323
|
+
}
|
|
12324
|
+
|
|
12325
|
+
#save() {
|
|
12326
|
+
const description = this.#input.value.trim();
|
|
12327
|
+
this.#editor.update(() => {
|
|
12328
|
+
const node = $getNodeByKey(this.#nodeKey);
|
|
12329
|
+
if ($isActionTextAttachmentNode(node) && node.altText !== description) {
|
|
12330
|
+
node.getWritable().altText = description;
|
|
12331
|
+
}
|
|
12332
|
+
}, { tag: [ HISTORY_PUSH_TAG, SKIP_DOM_SELECTION_TAG ] });
|
|
12333
|
+
this.#close();
|
|
12334
|
+
}
|
|
12335
|
+
|
|
12336
|
+
#handleKeyDown = (event) => {
|
|
12337
|
+
event.stopPropagation();
|
|
12338
|
+
if (event.key === "Escape") {
|
|
12339
|
+
this.#close();
|
|
12340
|
+
} else {
|
|
12341
|
+
trapFocusAtTabBoundary(this.#dialog, event);
|
|
12342
|
+
}
|
|
12343
|
+
}
|
|
12344
|
+
|
|
12345
|
+
// Dismissing by clicking elsewhere must leave focus where the click landed.
|
|
12346
|
+
#closeOnClickOutside = (event) => {
|
|
12347
|
+
if (this.#dialog.open && !this.#dialog.contains(event.target)) {
|
|
12348
|
+
this.#close({ refocus: false });
|
|
12349
|
+
}
|
|
12350
|
+
}
|
|
12351
|
+
|
|
12352
|
+
#close({ refocus = true } = {}) {
|
|
12353
|
+
this.#dialog.close();
|
|
12354
|
+
if (refocus && this.#elementToRefocus?.isConnected) {
|
|
12355
|
+
this.#elementToRefocus.focus();
|
|
12356
|
+
}
|
|
12357
|
+
this.#elementToRefocus = null;
|
|
12358
|
+
}
|
|
12359
|
+
}
|
|
12360
|
+
|
|
11459
12361
|
const DELETE_ICON = `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
11460
12362
|
<path d="M11.2041 1.01074C12.2128 1.113 13 1.96435 13 3V4H15L15.1025 4.00488C15.6067 4.05621 16 4.48232 16 5C16 5.55228 15.5523 6 15 6H14.8457L14.1416 15.1533C14.0614 16.1953 13.1925 17 12.1475 17H5.85254L5.6582 16.9902C4.76514 16.9041 4.03607 16.2296 3.88184 15.3457L3.8584 15.1533L3.1543 6H3C2.44772 6 2 5.55228 2 5C2 4.44772 2.44772 4 3 4H5V3C5 1.89543 5.89543 1 7 1H11L11.2041 1.01074ZM5.85254 15H12.1475L12.8398 6H5.16016L5.85254 15ZM7 4H11V3H7V4Z"/>
|
|
11461
12363
|
</svg>`;
|
|
11462
12364
|
|
|
11463
|
-
class
|
|
12365
|
+
class AttachmentToolbar extends HTMLElement {
|
|
12366
|
+
#listeners = new ListenerBin()
|
|
12367
|
+
#buttons = []
|
|
12368
|
+
#alternativeTextButton
|
|
12369
|
+
#alternativeTextDialog
|
|
12370
|
+
#currentNodeKey = null
|
|
12371
|
+
#reflowObserver = new ResizeObserver(() => this.#updatePosition())
|
|
12372
|
+
#presentationObserver = new MutationObserver(() => this.#refresh())
|
|
12373
|
+
#observedElement = null
|
|
12374
|
+
|
|
11464
12375
|
connectedCallback() {
|
|
11465
|
-
this.editorElement = this.closest("lexxy-editor");
|
|
11466
|
-
this.editor = this.editorElement.editor;
|
|
11467
12376
|
this.classList.add("lexxy-floating-controls");
|
|
12377
|
+
this.role = "toolbar";
|
|
12378
|
+
this.hidden = true;
|
|
11468
12379
|
|
|
11469
|
-
if (
|
|
11470
|
-
this.#
|
|
12380
|
+
if (this.#editor) {
|
|
12381
|
+
this.#setUpButtons();
|
|
12382
|
+
this.#monitorSelection();
|
|
12383
|
+
this.#followScrolling();
|
|
12384
|
+
this.#registerKeyboardShortcut();
|
|
11471
12385
|
}
|
|
11472
12386
|
}
|
|
11473
12387
|
|
|
11474
12388
|
disconnectedCallback() {
|
|
11475
|
-
this.
|
|
11476
|
-
|
|
12389
|
+
this.dispose();
|
|
12390
|
+
}
|
|
12391
|
+
|
|
12392
|
+
dispose() {
|
|
12393
|
+
this.#listeners.dispose();
|
|
12394
|
+
this.#reflowObserver.disconnect();
|
|
12395
|
+
this.#presentationObserver.disconnect();
|
|
12396
|
+
this.#alternativeTextDialog?.dispose();
|
|
12397
|
+
}
|
|
12398
|
+
|
|
12399
|
+
get #editor() {
|
|
12400
|
+
return this.#editorElement?.editor
|
|
12401
|
+
}
|
|
12402
|
+
|
|
12403
|
+
get #editorElement() {
|
|
12404
|
+
return this.closest("lexxy-editor")
|
|
11477
12405
|
}
|
|
11478
12406
|
|
|
11479
|
-
#
|
|
12407
|
+
#setUpButtons() {
|
|
12408
|
+
this.innerHTML = "";
|
|
12409
|
+
|
|
11480
12410
|
const container = createElement("div", { className: "lexxy-floating-controls__group" });
|
|
12411
|
+
container.appendChild(this.#createAlternativeTextButton());
|
|
12412
|
+
container.appendChild(this.#createRemoveButton());
|
|
12413
|
+
this.#alternativeTextDialog = new AlternativeTextDialog(this.#editorElement);
|
|
12414
|
+
this.appendChild(container);
|
|
12415
|
+
|
|
12416
|
+
this.#buttons = Array.from(this.querySelectorAll("button"));
|
|
12417
|
+
this.#listeners.track(
|
|
12418
|
+
registerEventListener(this, "keydown", this.#navigateOrExit),
|
|
12419
|
+
registerEventListener(this, "focusout", this.#resetTabIndexOnExit)
|
|
12420
|
+
);
|
|
12421
|
+
}
|
|
11481
12422
|
|
|
11482
|
-
|
|
11483
|
-
|
|
12423
|
+
#createRemoveButton() {
|
|
12424
|
+
const button = createElement("button", {
|
|
11484
12425
|
type: "button",
|
|
11485
|
-
|
|
12426
|
+
className: "lexxy-attachment-toolbar__remove",
|
|
12427
|
+
ariaLabel: "Remove",
|
|
12428
|
+
tabIndex: -1
|
|
11486
12429
|
});
|
|
11487
|
-
|
|
11488
|
-
this.
|
|
12430
|
+
button.innerHTML = DELETE_ICON;
|
|
12431
|
+
this.#listeners.track(registerEventListener(button, "click", () => this.#removeSelectedNode()));
|
|
12432
|
+
return button
|
|
12433
|
+
}
|
|
11489
12434
|
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
12435
|
+
#removeSelectedNode() {
|
|
12436
|
+
if (this.#hasSelectedNode) {
|
|
12437
|
+
this.#editor.update(() => $getNodeByKey(this.#currentNodeKey)?.remove());
|
|
12438
|
+
this.#editor.focus();
|
|
12439
|
+
}
|
|
12440
|
+
}
|
|
11493
12441
|
|
|
11494
|
-
|
|
12442
|
+
get #hasSelectedNode() {
|
|
12443
|
+
return this.#currentNodeKey !== null
|
|
11495
12444
|
}
|
|
11496
12445
|
|
|
11497
|
-
#
|
|
11498
|
-
this
|
|
11499
|
-
|
|
11500
|
-
|
|
12446
|
+
#createAlternativeTextButton() {
|
|
12447
|
+
this.#alternativeTextButton = createElement("button", {
|
|
12448
|
+
type: "button",
|
|
12449
|
+
className: "lexxy-attachment-toolbar__alternative-text",
|
|
12450
|
+
ariaLabel: "Alternative text",
|
|
12451
|
+
title: "Alternative text",
|
|
12452
|
+
ariaHasPopup: "dialog",
|
|
12453
|
+
tabIndex: -1
|
|
12454
|
+
});
|
|
12455
|
+
this.#alternativeTextButton.appendChild(createElement("span", null, "ALT"));
|
|
12456
|
+
this.#listeners.track(registerEventListener(this.#alternativeTextButton, "click", () => {
|
|
12457
|
+
this.#alternativeTextDialog.open(this.#currentNodeKey);
|
|
12458
|
+
}));
|
|
12459
|
+
return this.#alternativeTextButton
|
|
12460
|
+
}
|
|
12461
|
+
|
|
12462
|
+
#navigateOrExit = (event) => {
|
|
12463
|
+
if (event.key === "Escape") {
|
|
12464
|
+
event.preventDefault();
|
|
12465
|
+
event.stopPropagation();
|
|
12466
|
+
this.#returnFocusToEditor();
|
|
12467
|
+
} else {
|
|
12468
|
+
handleRollingTabIndex(this.#buttons, event);
|
|
12469
|
+
}
|
|
12470
|
+
}
|
|
12471
|
+
|
|
12472
|
+
#returnFocusToEditor() {
|
|
12473
|
+
this.#editor.getRootElement()?.focus({ preventScroll: true });
|
|
12474
|
+
if (this.#currentNodeKey) {
|
|
12475
|
+
const nodeKey = this.#currentNodeKey;
|
|
12476
|
+
this.#editor.update(() => {
|
|
12477
|
+
const node = $getNodeByKey(nodeKey);
|
|
12478
|
+
if (node) $setSelection($createNodeSelectionWith(node));
|
|
12479
|
+
});
|
|
12480
|
+
}
|
|
12481
|
+
}
|
|
12482
|
+
|
|
12483
|
+
#resetTabIndexOnExit = (event) => {
|
|
12484
|
+
if (!this.contains(event.relatedTarget)) {
|
|
12485
|
+
this.#buttons.forEach(button => button.tabIndex = -1);
|
|
12486
|
+
}
|
|
12487
|
+
}
|
|
12488
|
+
|
|
12489
|
+
#monitorSelection() {
|
|
12490
|
+
this.#listeners.track(
|
|
12491
|
+
registerLabelledDecoratorSelection(this.#editor, (target) => this.#updateForSelection(target))
|
|
12492
|
+
);
|
|
12493
|
+
}
|
|
12494
|
+
|
|
12495
|
+
#updateForSelection(target) {
|
|
12496
|
+
if (target) {
|
|
12497
|
+
this.#show(target.key);
|
|
12498
|
+
} else {
|
|
12499
|
+
this.#hide();
|
|
12500
|
+
}
|
|
12501
|
+
}
|
|
12502
|
+
|
|
12503
|
+
#show(nodeKey) {
|
|
12504
|
+
this.#currentNodeKey = nodeKey;
|
|
12505
|
+
this.#refresh();
|
|
12506
|
+
this.hidden = false;
|
|
12507
|
+
this.#reflowObserver.observe(this.#editorElement);
|
|
12508
|
+
}
|
|
12509
|
+
|
|
12510
|
+
#refresh() {
|
|
12511
|
+
if (this.#hasSelectedNode) {
|
|
12512
|
+
this.#watchPresentation(this.#currentNodeKey);
|
|
12513
|
+
this.#describeNode(this.#currentNodeKey);
|
|
12514
|
+
this.#updatePosition();
|
|
12515
|
+
}
|
|
12516
|
+
}
|
|
12517
|
+
|
|
12518
|
+
// Failed previews change presentation without a Lexical update, and a node
|
|
12519
|
+
// can replace its figure without changing its key.
|
|
12520
|
+
#watchPresentation(nodeKey) {
|
|
12521
|
+
const element = this.#editor.getElementByKey(nodeKey);
|
|
12522
|
+
if (element !== this.#observedElement) {
|
|
12523
|
+
this.#presentationObserver.disconnect();
|
|
12524
|
+
this.#observedElement = element;
|
|
12525
|
+
if (element) this.#presentationObserver.observe(element, { attributes: true, attributeFilter: [ "class" ] });
|
|
12526
|
+
}
|
|
12527
|
+
}
|
|
12528
|
+
|
|
12529
|
+
#describeNode(nodeKey) {
|
|
12530
|
+
const element = this.#editor.getElementByKey(nodeKey);
|
|
12531
|
+
|
|
12532
|
+
this.#editor.getEditorState().read(() => {
|
|
12533
|
+
const node = $getNodeByKey(nodeKey);
|
|
12534
|
+
this.#alternativeTextButton.hidden = !this.#canEditAlternativeText(node);
|
|
12535
|
+
this.dataset.nodeType = node.getType();
|
|
12536
|
+
this.dataset.presentation = this.#presentationOf(node, element);
|
|
12537
|
+
|
|
12538
|
+
if (node.contentType) {
|
|
12539
|
+
this.dataset.contentType = node.contentType;
|
|
12540
|
+
} else {
|
|
12541
|
+
delete this.dataset.contentType;
|
|
12542
|
+
}
|
|
11501
12543
|
});
|
|
11502
12544
|
}
|
|
12545
|
+
|
|
12546
|
+
#canEditAlternativeText(node) {
|
|
12547
|
+
return this.#editorElement.dataset.actionTextSupportsAlt !== "false"
|
|
12548
|
+
&& node.getType() === "action_text_attachment"
|
|
12549
|
+
&& node.isPreviewableAttachment
|
|
12550
|
+
}
|
|
12551
|
+
|
|
12552
|
+
#presentationOf(node, element) {
|
|
12553
|
+
if ($isImageGalleryNode(node.getParent())) {
|
|
12554
|
+
return "gallery"
|
|
12555
|
+
} else if (node.isInline() && this.#rendersInline(element)) {
|
|
12556
|
+
return "inline"
|
|
12557
|
+
} else if (element?.classList.contains("attachment--preview")) {
|
|
12558
|
+
return "preview"
|
|
12559
|
+
} else {
|
|
12560
|
+
return "block"
|
|
12561
|
+
}
|
|
12562
|
+
}
|
|
12563
|
+
|
|
12564
|
+
#rendersInline(element) {
|
|
12565
|
+
return element != null && getComputedStyle(element).display.startsWith("inline")
|
|
12566
|
+
}
|
|
12567
|
+
|
|
12568
|
+
#updatePosition() {
|
|
12569
|
+
const figureElement = this.#editor.getElementByKey(this.#currentNodeKey);
|
|
12570
|
+
if (figureElement) {
|
|
12571
|
+
const rect = figureElement.getBoundingClientRect();
|
|
12572
|
+
const editorRect = this.#editorElement.getBoundingClientRect();
|
|
12573
|
+
// We are positioned against the editor's padding box, but the rect measures its border box.
|
|
12574
|
+
const borderTop = this.#editorElement.clientTop;
|
|
12575
|
+
const borderLeft = this.#editorElement.clientLeft;
|
|
12576
|
+
this.style.setProperty("--lexxy-anchor-top", `${rect.top - editorRect.top - borderTop}px`);
|
|
12577
|
+
this.style.setProperty("--lexxy-anchor-left", `${rect.left - editorRect.left - borderLeft}px`);
|
|
12578
|
+
this.style.setProperty("--lexxy-anchor-width", `${rect.width}px`);
|
|
12579
|
+
this.style.setProperty("--lexxy-anchor-height", `${rect.height}px`);
|
|
12580
|
+
this.#flipWhenOverflowing(editorRect);
|
|
12581
|
+
}
|
|
12582
|
+
}
|
|
12583
|
+
|
|
12584
|
+
#flipWhenOverflowing(editorRect) {
|
|
12585
|
+
this.removeAttribute("data-overflow");
|
|
12586
|
+
if (this.getBoundingClientRect().right > editorRect.right) {
|
|
12587
|
+
this.setAttribute("data-overflow", "");
|
|
12588
|
+
}
|
|
12589
|
+
}
|
|
12590
|
+
|
|
12591
|
+
#hide() {
|
|
12592
|
+
if (this.#hasSelectedNode) {
|
|
12593
|
+
this.#currentNodeKey = null;
|
|
12594
|
+
this.hidden = true;
|
|
12595
|
+
this.#reflowObserver.disconnect();
|
|
12596
|
+
this.#presentationObserver.disconnect();
|
|
12597
|
+
this.#observedElement = null;
|
|
12598
|
+
}
|
|
12599
|
+
}
|
|
12600
|
+
|
|
12601
|
+
// Scrolling inside the editor moves the figure without resizing anything.
|
|
12602
|
+
#followScrolling() {
|
|
12603
|
+
this.#listeners.track(registerEventListener(this.#editorElement, "scroll", () => this.#updatePosition(), { capture: true, passive: true }));
|
|
12604
|
+
}
|
|
12605
|
+
|
|
12606
|
+
#registerKeyboardShortcut() {
|
|
12607
|
+
this.#listeners.track(this.#editor.registerCommand(
|
|
12608
|
+
KEY_DOWN_COMMAND,
|
|
12609
|
+
this.#focusToolbarOnAltF10,
|
|
12610
|
+
COMMAND_PRIORITY_HIGH
|
|
12611
|
+
));
|
|
12612
|
+
}
|
|
12613
|
+
|
|
12614
|
+
#focusToolbarOnAltF10 = (event) => {
|
|
12615
|
+
if (this.#hasSelectedNode && event.altKey && event.key === "F10") {
|
|
12616
|
+
event.preventDefault();
|
|
12617
|
+
this.#buttons.find(isActiveAndVisible)?.focus();
|
|
12618
|
+
return true
|
|
12619
|
+
} else {
|
|
12620
|
+
return false
|
|
12621
|
+
}
|
|
12622
|
+
}
|
|
11503
12623
|
}
|
|
11504
12624
|
|
|
11505
12625
|
class TableController {
|
|
@@ -11904,7 +13024,7 @@ class TableTools extends HTMLElement {
|
|
|
11904
13024
|
#listeners = new ListenerBin()
|
|
11905
13025
|
|
|
11906
13026
|
connectedCallback() {
|
|
11907
|
-
this.tableController = new TableController(this
|
|
13027
|
+
this.tableController = new TableController(this.editorElement);
|
|
11908
13028
|
this.classList.add("lexxy-floating-controls");
|
|
11909
13029
|
this.role = "toolbar";
|
|
11910
13030
|
|
|
@@ -11925,16 +13045,34 @@ class TableTools extends HTMLElement {
|
|
|
11925
13045
|
this.tableController = null;
|
|
11926
13046
|
}
|
|
11927
13047
|
|
|
11928
|
-
get
|
|
11929
|
-
return this
|
|
13048
|
+
get editorElement() {
|
|
13049
|
+
return this.closest("lexxy-editor")
|
|
11930
13050
|
}
|
|
11931
13051
|
|
|
11932
|
-
get
|
|
11933
|
-
return this.
|
|
13052
|
+
get editor() {
|
|
13053
|
+
return this.editorElement.editor
|
|
13054
|
+
}
|
|
13055
|
+
|
|
13056
|
+
getEditorElement() {
|
|
13057
|
+
return this.editorElement
|
|
13058
|
+
}
|
|
13059
|
+
|
|
13060
|
+
closeDropdowns({ except } = {}) {
|
|
13061
|
+
this.#moreMenus.forEach(menu => {
|
|
13062
|
+
if (menu !== except) menu.close({ focusEditor: false });
|
|
13063
|
+
});
|
|
13064
|
+
}
|
|
13065
|
+
|
|
13066
|
+
get #moreMenus() {
|
|
13067
|
+
return this.querySelectorAll("lexxy-toolbar-dropdown")
|
|
13068
|
+
}
|
|
13069
|
+
|
|
13070
|
+
get #hasOpenMoreMenu() {
|
|
13071
|
+
return Array.from(this.#moreMenus).some(menu => menu.isOpen)
|
|
11934
13072
|
}
|
|
11935
13073
|
|
|
11936
13074
|
get #tableToolsButtons() {
|
|
11937
|
-
return Array.from(this.querySelectorAll("button
|
|
13075
|
+
return Array.from(this.querySelectorAll("button")).filter(button => !button.closest("[data-dropdown-panel]"))
|
|
11938
13076
|
}
|
|
11939
13077
|
|
|
11940
13078
|
#setUpButtons() {
|
|
@@ -11953,23 +13091,37 @@ class TableTools extends HTMLElement {
|
|
|
11953
13091
|
const plusButton = this.#createButton(`Add ${childType}`, { action: "insert", childType, direction: "after" }, "+");
|
|
11954
13092
|
const minusButton = this.#createButton(`Remove ${childType}`, { action: "delete", childType }, "−");
|
|
11955
13093
|
|
|
11956
|
-
const dropdown = createElement("details", { className: "lexxy-table-control__more-menu" });
|
|
11957
|
-
dropdown.setAttribute("name", "lexxy-dropdown");
|
|
11958
|
-
dropdown.tabIndex = -1;
|
|
11959
|
-
|
|
11960
|
-
const count = createElement("summary", {}, `_ ${childType}s`);
|
|
11961
|
-
setCountProperty(count);
|
|
11962
|
-
dropdown.appendChild(count);
|
|
11963
|
-
|
|
11964
|
-
dropdown.appendChild(moreMenu);
|
|
11965
|
-
|
|
11966
13094
|
container.appendChild(minusButton);
|
|
11967
|
-
container.appendChild(
|
|
13095
|
+
container.appendChild(this.#createMoreMenu(childType, setCountProperty, moreMenu));
|
|
11968
13096
|
container.appendChild(plusButton);
|
|
11969
13097
|
|
|
11970
13098
|
return container
|
|
11971
13099
|
}
|
|
11972
13100
|
|
|
13101
|
+
#createMoreMenu(childType, setCountProperty, menu) {
|
|
13102
|
+
const dropdown = createElement("lexxy-toolbar-dropdown", { className: "lexxy-table-control__more-menu" });
|
|
13103
|
+
|
|
13104
|
+
const trigger = createElement("button", {
|
|
13105
|
+
type: "button",
|
|
13106
|
+
className: "lexxy-table-control__more-menu-trigger",
|
|
13107
|
+
dataset: { dropdownTrigger: "" },
|
|
13108
|
+
"aria-haspopup": "menu",
|
|
13109
|
+
"aria-expanded": "false"
|
|
13110
|
+
}, `_ ${childType}s`);
|
|
13111
|
+
trigger.tabIndex = -1;
|
|
13112
|
+
setCountProperty(trigger);
|
|
13113
|
+
|
|
13114
|
+
menu.dataset.dropdownPanel = "";
|
|
13115
|
+
menu.role = "menu";
|
|
13116
|
+
menu.setAttribute("aria-label", `${childType} options`);
|
|
13117
|
+
menu.hidden = true;
|
|
13118
|
+
|
|
13119
|
+
dropdown.appendChild(trigger);
|
|
13120
|
+
dropdown.appendChild(menu);
|
|
13121
|
+
|
|
13122
|
+
return dropdown
|
|
13123
|
+
}
|
|
13124
|
+
|
|
11973
13125
|
#createRowButtonsContainer() {
|
|
11974
13126
|
return this.#createButtonsContainer(
|
|
11975
13127
|
"row",
|
|
@@ -11988,15 +13140,18 @@ class TableTools extends HTMLElement {
|
|
|
11988
13140
|
|
|
11989
13141
|
#createMoreMenuSection(childType) {
|
|
11990
13142
|
const section = createElement("div", { className: "lexxy-floating-controls__group lexxy-table-control__more-menu-details" });
|
|
11991
|
-
const addBeforeButton = this.#createButton(`Add ${childType} before`, { action: "insert", childType, direction: "before" });
|
|
11992
|
-
const addAfterButton = this.#createButton(`Add ${childType} after`, { action: "insert", childType, direction: "after" });
|
|
11993
|
-
const toggleStyleButton = this.#createButton(`Toggle ${childType} style`, { action: "toggle", childType });
|
|
11994
|
-
const deleteButton = this.#createButton(`Remove ${childType}`, { action: "delete", childType });
|
|
11995
13143
|
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
13144
|
+
const items = [
|
|
13145
|
+
this.#createButton(`Add ${childType} before`, { action: "insert", childType, direction: "before" }),
|
|
13146
|
+
this.#createButton(`Add ${childType} after`, { action: "insert", childType, direction: "after" }),
|
|
13147
|
+
this.#createButton(`Toggle ${childType} style`, { action: "toggle", childType }),
|
|
13148
|
+
this.#createButton(`Remove ${childType}`, { action: "delete", childType })
|
|
13149
|
+
];
|
|
13150
|
+
|
|
13151
|
+
items.forEach(item => {
|
|
13152
|
+
item.role = "menuitem";
|
|
13153
|
+
section.appendChild(item);
|
|
13154
|
+
});
|
|
12000
13155
|
|
|
12001
13156
|
return section
|
|
12002
13157
|
}
|
|
@@ -12037,7 +13192,7 @@ class TableTools extends HTMLElement {
|
|
|
12037
13192
|
}
|
|
12038
13193
|
|
|
12039
13194
|
#registerKeyboardShortcuts() {
|
|
12040
|
-
this.#listeners.track(this
|
|
13195
|
+
this.#listeners.track(this.editor.registerCommand(KEY_DOWN_COMMAND, this.#focusToolbarOnAltF10, COMMAND_PRIORITY_HIGH));
|
|
12041
13196
|
}
|
|
12042
13197
|
|
|
12043
13198
|
#focusToolbarOnAltF10 = (event) => {
|
|
@@ -12068,9 +13223,9 @@ class TableTools extends HTMLElement {
|
|
|
12068
13223
|
const cell = this.tableController.currentCell;
|
|
12069
13224
|
if (!cell) return
|
|
12070
13225
|
|
|
12071
|
-
this
|
|
13226
|
+
this.editor.update(() => {
|
|
12072
13227
|
cell.select();
|
|
12073
|
-
this
|
|
13228
|
+
this.editor.focus();
|
|
12074
13229
|
});
|
|
12075
13230
|
|
|
12076
13231
|
this.#update();
|
|
@@ -12107,7 +13262,7 @@ class TableTools extends HTMLElement {
|
|
|
12107
13262
|
if (!cellsToHighlight) return
|
|
12108
13263
|
|
|
12109
13264
|
cellsToHighlight.forEach(cell => {
|
|
12110
|
-
const cellElement = this
|
|
13265
|
+
const cellElement = this.editor.getElementByKey(cell.getKey());
|
|
12111
13266
|
if (!cellElement) return
|
|
12112
13267
|
|
|
12113
13268
|
cellElement.classList.toggle(theme.tableCellHighlight, true);
|
|
@@ -12116,8 +13271,8 @@ class TableTools extends HTMLElement {
|
|
|
12116
13271
|
}
|
|
12117
13272
|
|
|
12118
13273
|
#monitorForTableSelection() {
|
|
12119
|
-
this.#listeners.track(this
|
|
12120
|
-
const tableNode = this
|
|
13274
|
+
this.#listeners.track(this.editor.registerUpdateListener(() => {
|
|
13275
|
+
const tableNode = this.editor.getRootElement() && this.tableController.updateSelectedTable();
|
|
12121
13276
|
|
|
12122
13277
|
if (tableNode) {
|
|
12123
13278
|
this.#show();
|
|
@@ -12128,8 +13283,12 @@ class TableTools extends HTMLElement {
|
|
|
12128
13283
|
}
|
|
12129
13284
|
|
|
12130
13285
|
#executeTableCommand(command) {
|
|
13286
|
+
const fromMoreMenu = this.#hasOpenMoreMenu;
|
|
13287
|
+
|
|
12131
13288
|
this.tableController.executeTableCommand(command);
|
|
12132
13289
|
this.#update();
|
|
13290
|
+
|
|
13291
|
+
if (fromMoreMenu) this.editor.focus();
|
|
12133
13292
|
}
|
|
12134
13293
|
|
|
12135
13294
|
#show() {
|
|
@@ -12153,18 +13312,18 @@ class TableTools extends HTMLElement {
|
|
|
12153
13312
|
}
|
|
12154
13313
|
|
|
12155
13314
|
#closeMoreMenu() {
|
|
12156
|
-
this.
|
|
13315
|
+
this.closeDropdowns();
|
|
12157
13316
|
}
|
|
12158
13317
|
|
|
12159
13318
|
#updateButtonsPosition() {
|
|
12160
13319
|
const tableNode = this.tableController.currentTableNode;
|
|
12161
13320
|
if (!tableNode) return
|
|
12162
13321
|
|
|
12163
|
-
const tableElement = this
|
|
13322
|
+
const tableElement = this.editor.getElementByKey(tableNode.getKey());
|
|
12164
13323
|
if (!tableElement) return
|
|
12165
13324
|
|
|
12166
13325
|
const tableRect = tableElement.getBoundingClientRect();
|
|
12167
|
-
const editorRect = this
|
|
13326
|
+
const editorRect = this.editorElement.getBoundingClientRect();
|
|
12168
13327
|
|
|
12169
13328
|
const relativeTop = tableRect.top - editorRect.top;
|
|
12170
13329
|
const relativeCenter = (tableRect.left + tableRect.right) / 2 - editorRect.left;
|
|
@@ -12176,7 +13335,7 @@ class TableTools extends HTMLElement {
|
|
|
12176
13335
|
const tableNode = this.tableController.currentTableNode;
|
|
12177
13336
|
if (!tableNode) return
|
|
12178
13337
|
|
|
12179
|
-
const tableElement = $getElementForTableNode(this
|
|
13338
|
+
const tableElement = $getElementForTableNode(this.editor, tableNode);
|
|
12180
13339
|
if (!tableElement) return
|
|
12181
13340
|
|
|
12182
13341
|
const rowCount = tableElement.rows;
|
|
@@ -12190,18 +13349,18 @@ class TableTools extends HTMLElement {
|
|
|
12190
13349
|
const cell = this.tableController.currentCell;
|
|
12191
13350
|
if (!cell) return
|
|
12192
13351
|
|
|
12193
|
-
const cellElement = this
|
|
12194
|
-
if (!cellElement) return
|
|
13352
|
+
const cellElement = this.editor.getElementByKey(cell.getKey());
|
|
13353
|
+
if (!cellElement?.contains(document.getSelection()?.anchorNode)) return
|
|
12195
13354
|
|
|
12196
13355
|
cellElement.classList.add(theme.tableCellFocus);
|
|
12197
13356
|
}
|
|
12198
13357
|
|
|
12199
13358
|
#clearCellStyles() {
|
|
12200
|
-
this
|
|
13359
|
+
this.editorElement.querySelectorAll(`.${theme.tableCellFocus}`)?.forEach(cell => {
|
|
12201
13360
|
cell.classList.remove(theme.tableCellFocus);
|
|
12202
13361
|
});
|
|
12203
13362
|
|
|
12204
|
-
this
|
|
13363
|
+
this.editorElement.querySelectorAll(`.${theme.tableCellHighlight}`)?.forEach(cell => {
|
|
12205
13364
|
cell.classList.remove(theme.tableCellHighlight);
|
|
12206
13365
|
cell.removeAttribute("data-action");
|
|
12207
13366
|
cell.removeAttribute("data-child-type");
|
|
@@ -12233,7 +13392,8 @@ function defineElements() {
|
|
|
12233
13392
|
// Prompt must be registered AFTER Editor
|
|
12234
13393
|
"lexxy-prompt": LexicalPromptElement,
|
|
12235
13394
|
"lexxy-code-language-picker": CodeLanguagePicker,
|
|
12236
|
-
"lexxy-
|
|
13395
|
+
"lexxy-live-region": LiveRegion,
|
|
13396
|
+
"lexxy-attachment-toolbar": AttachmentToolbar,
|
|
12237
13397
|
"lexxy-table-tools": TableTools
|
|
12238
13398
|
};
|
|
12239
13399
|
|