@37signals/lexxy 0.9.12-beta → 0.9.14-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -3
- package/dist/lexxy.esm.js +457 -242
- package/dist/stylesheets/lexxy-content.css +22 -6
- package/dist/stylesheets/lexxy-editor.css +18 -4
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { highlightCode } from './lexxy_helpers.esm.js';
|
|
2
2
|
import DOMPurify 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, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $
|
|
4
|
+
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $getCommonAncestor, $findMatchingParent, TextNode, createCommand, defineExtension, COMMAND_PRIORITY_EDITOR, $getEditor, $getNodeByKey, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $createRangeSelection, $setSelection, createState, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, $getRoot, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $addUpdateTag, ElementNode, $splitNode, $getChildCaretAtIndex, $createLineBreakNode, PASTE_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, CLEAR_HISTORY_COMMAND, $onUpdate, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, INPUT_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
5
5
|
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
|
|
6
6
|
import { buildEditorFromExtensions } from '@lexical/extension';
|
|
7
7
|
import { ListNode, ListItemNode, $getListDepth, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $isListItemNode, $isListNode, registerList } from '@lexical/list';
|
|
@@ -484,6 +484,14 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
484
484
|
});
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
closeDropdowns({ except } = {}) {
|
|
488
|
+
this.#dropdowns.forEach((dropdown) => {
|
|
489
|
+
if (dropdown !== except) {
|
|
490
|
+
dropdown.close({ focusEditor: false });
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
487
495
|
#reconnect() {
|
|
488
496
|
this.disconnectedCallback();
|
|
489
497
|
this.connectedCallback();
|
|
@@ -611,14 +619,14 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
611
619
|
|
|
612
620
|
this.#setButtonPressed("bold", isBold);
|
|
613
621
|
this.#setButtonPressed("italic", isItalic);
|
|
622
|
+
this.#setButtonPressed("strikethrough", isStrikethrough);
|
|
623
|
+
this.#setButtonPressed("underline", isUnderline);
|
|
614
624
|
|
|
615
|
-
this.#setButtonPressed("format", isInHeading
|
|
625
|
+
this.#setButtonPressed("format", isInHeading);
|
|
616
626
|
this.#setButtonPressed("paragraph", !isInHeading);
|
|
617
627
|
this.#setButtonPressed("heading-large", headingTag === "h2");
|
|
618
628
|
this.#setButtonPressed("heading-medium", headingTag === "h3");
|
|
619
629
|
this.#setButtonPressed("heading-small", headingTag === "h4");
|
|
620
|
-
this.#setButtonPressed("strikethrough", isStrikethrough);
|
|
621
|
-
this.#setButtonPressed("underline", isUnderline);
|
|
622
630
|
|
|
623
631
|
this.#setButtonPressed("lists", isInList);
|
|
624
632
|
this.#setButtonPressed("unordered-list", isInList && listType === "bullet");
|
|
@@ -656,64 +664,38 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
656
664
|
}
|
|
657
665
|
|
|
658
666
|
#refreshOverflow() {
|
|
667
|
+
this.#hideOverflowMenuButton();
|
|
659
668
|
this.#resetToolbarOverflow();
|
|
660
669
|
this.#reindexToolbarItems();
|
|
661
670
|
this.#compactMenu();
|
|
662
671
|
|
|
663
|
-
const isOverflowing = this.#
|
|
672
|
+
const isOverflowing = this.#overflowMenuDropdown.children.length > 0;
|
|
664
673
|
|
|
665
674
|
this.toggleAttribute("overflowing", isOverflowing);
|
|
666
|
-
|
|
667
|
-
this.#
|
|
668
|
-
this.#overflow.setAttribute("nonce", getNonce());
|
|
669
|
-
|
|
670
|
-
this.#overflowMenu.toggleAttribute("disabled", !isOverflowing);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Separates layout reads from DOM writes to avoid forced reflows during init.
|
|
674
|
-
// Measures every button's right edge in a single read pass, figures out which
|
|
675
|
-
// buttons overflow using math, and then moves them in a single write pass.
|
|
676
|
-
#compactMenu() {
|
|
677
|
-
const buttons = this.#overflowButtons;
|
|
678
|
-
if (buttons.length === 0) return
|
|
679
|
-
|
|
680
|
-
const availableWidth = this.clientWidth;
|
|
681
|
-
const buttonRightEdges = buttons.map(button => {
|
|
682
|
-
const style = window.getComputedStyle(button);
|
|
683
|
-
return button.offsetLeft + button.offsetWidth + parseFloat(style.marginRight)
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
let firstOverflowing = -1;
|
|
687
|
-
for (let i = 0; i < buttons.length; i++) {
|
|
688
|
-
if (buttonRightEdges[i] > availableWidth) {
|
|
689
|
-
firstOverflowing = i;
|
|
690
|
-
break
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
if (firstOverflowing === -1) return
|
|
695
|
-
|
|
696
|
-
// Move one extra button to reserve space for the overflow control, which is
|
|
697
|
-
// `display: none` until we show it
|
|
698
|
-
const overflowIndex = Math.max(0, firstOverflowing - 1);
|
|
699
|
-
const overflowButtons = buttons.slice(overflowIndex).reverse();
|
|
700
|
-
for (const button of overflowButtons) {
|
|
701
|
-
this.#overflowMenu.prepend(button);
|
|
702
|
-
button.role = "menuitem";
|
|
703
|
-
}
|
|
675
|
+
this.#setOverflowMenuNonce();
|
|
676
|
+
this.#showOverflowMenuButton(isOverflowing);
|
|
704
677
|
}
|
|
705
678
|
|
|
706
679
|
#resetToolbarOverflow() {
|
|
707
|
-
const items = Array.from(this.#
|
|
680
|
+
const items = Array.from(this.#overflowMenuDropdown.children);
|
|
708
681
|
items.sort((a, b) => this.#itemPosition(b) - this.#itemPosition(a));
|
|
709
682
|
|
|
710
683
|
for (const item of items) {
|
|
711
|
-
const nextItem = this.querySelector(`[data-position="${this.#itemPosition(item) + 1}"]`) ?? this.#
|
|
684
|
+
const nextItem = this.querySelector(`[data-position="${this.#itemPosition(item) + 1}"]`) ?? this.#overflowMenuButton;
|
|
712
685
|
item.removeAttribute("role");
|
|
713
686
|
this.insertBefore(item, nextItem);
|
|
714
687
|
}
|
|
715
688
|
}
|
|
716
689
|
|
|
690
|
+
#showOverflowMenuButton(show = true) {
|
|
691
|
+
this.#overflowMenuDropdown.toggleAttribute("disabled", !show);
|
|
692
|
+
this.#overflowMenuButton.style.display = show ? "block" : "none";
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
#hideOverflowMenuButton() {
|
|
696
|
+
this.#showOverflowMenuButton(false);
|
|
697
|
+
}
|
|
698
|
+
|
|
717
699
|
#itemPosition(item) {
|
|
718
700
|
return parseInt(item.dataset.position ?? "999")
|
|
719
701
|
}
|
|
@@ -724,28 +706,59 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
724
706
|
});
|
|
725
707
|
}
|
|
726
708
|
|
|
727
|
-
|
|
728
|
-
this.#
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
709
|
+
#compactMenu() {
|
|
710
|
+
const overflowWidth = this.#getOverflowWidth();
|
|
711
|
+
|
|
712
|
+
if (overflowWidth > 0) {
|
|
713
|
+
this.#showOverflowMenuButton();
|
|
714
|
+
const gap = this.#getToolbarGap();
|
|
715
|
+
const spaceForOverflow = gap + this.#overflowMenuButton.offsetWidth;
|
|
716
|
+
this.#reclaimWidth(overflowWidth + spaceForOverflow, { gap });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
#getOverflowWidth() {
|
|
721
|
+
return this.scrollWidth - this.clientWidth
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
#reclaimWidth(overflowWidth, { gap }) {
|
|
725
|
+
const buttons = this.#overflowableButtons;
|
|
726
|
+
const overflowButtons = [];
|
|
727
|
+
let recoveredWidth = 0;
|
|
728
|
+
|
|
729
|
+
while (recoveredWidth < overflowWidth && buttons.length) {
|
|
730
|
+
const button = buttons.pop();
|
|
731
|
+
|
|
732
|
+
overflowButtons.push(button);
|
|
733
|
+
button.role = "menuitem";
|
|
734
|
+
recoveredWidth += button.offsetWidth + gap;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
this.#overflowMenuDropdown.append(...overflowButtons.reverse());
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
#setOverflowMenuNonce() {
|
|
741
|
+
this.#overflowMenuButton.setAttribute("nonce", getNonce());
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
#getToolbarGap() {
|
|
745
|
+
return parseFloat(window.getComputedStyle(this).columnGap) || 0
|
|
733
746
|
}
|
|
734
747
|
|
|
735
748
|
get #dropdowns() {
|
|
736
749
|
return this.querySelectorAll(":scope .lexxy-editor__toolbar-dropdown")
|
|
737
750
|
}
|
|
738
751
|
|
|
739
|
-
get #
|
|
752
|
+
get #overflowMenuButton() {
|
|
740
753
|
return this.querySelector(".lexxy-editor__toolbar-overflow")
|
|
741
754
|
}
|
|
742
755
|
|
|
743
|
-
get #
|
|
744
|
-
return this.#
|
|
756
|
+
get #overflowMenuDropdown() {
|
|
757
|
+
return this.#overflowMenuButton?.querySelector(":scope > [data-dropdown-panel]")
|
|
745
758
|
}
|
|
746
759
|
|
|
747
|
-
get #
|
|
748
|
-
return Array.from(this.querySelectorAll(":scope > button:not([data-prevent-overflow
|
|
760
|
+
get #overflowableButtons() {
|
|
761
|
+
return Array.from(this.querySelectorAll(":scope > button:not([data-prevent-overflow])"))
|
|
749
762
|
}
|
|
750
763
|
|
|
751
764
|
get #buttons() {
|
|
@@ -776,6 +789,14 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
776
789
|
${ToolbarIcons.italic}
|
|
777
790
|
</button>
|
|
778
791
|
|
|
792
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="strikethrough" data-command="strikethrough" title="Strikethrough">
|
|
793
|
+
${ToolbarIcons.strikethrough}
|
|
794
|
+
</button>
|
|
795
|
+
|
|
796
|
+
<button class="lexxy-editor__toolbar-button lexxy-editor__toolbar-group-end" type="button" name="underline" data-command="underline" title="Underline">
|
|
797
|
+
${ToolbarIcons.underline}
|
|
798
|
+
</button>
|
|
799
|
+
|
|
779
800
|
<lexxy-toolbar-dropdown class="lexxy-editor__toolbar-dropdown">
|
|
780
801
|
<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">
|
|
781
802
|
${ToolbarIcons.heading}
|
|
@@ -794,13 +815,6 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
794
815
|
${ToolbarIcons.h4} <span>Small Heading</span>
|
|
795
816
|
</button>
|
|
796
817
|
<div class="lexxy-editor__toolbar-separator" role="separator"></div>
|
|
797
|
-
<button type="button" name="strikethrough" data-command="strikethrough" title="Strikethrough" role="menuitem">
|
|
798
|
-
${ToolbarIcons.strikethrough} <span>Strikethrough</span>
|
|
799
|
-
</button>
|
|
800
|
-
<button type="button" name="underline" data-command="underline" title="Underline" role="menuitem">
|
|
801
|
-
${ToolbarIcons.underline} <span>Underline</span>
|
|
802
|
-
</button>
|
|
803
|
-
<div class="lexxy-editor__toolbar-separator" role="separator"></div>
|
|
804
818
|
<button type="button" name="clear-formatting" data-command="clearFormatting" title="Clear formatting" role="menuitem">
|
|
805
819
|
${ToolbarIcons.clearFormatting} <span>Clear formatting</span>
|
|
806
820
|
</button>
|
|
@@ -1486,6 +1500,10 @@ class LexxyExtension {
|
|
|
1486
1500
|
|
|
1487
1501
|
}
|
|
1488
1502
|
|
|
1503
|
+
setEditorValidity(flags, message) {
|
|
1504
|
+
this.editorElement.setElementValidity(this, flags, message);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1489
1507
|
dispose() {
|
|
1490
1508
|
}
|
|
1491
1509
|
}
|
|
@@ -1638,40 +1656,166 @@ function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
|
|
|
1638
1656
|
&& previousNode instanceof CustomActionTextAttachmentNode
|
|
1639
1657
|
}
|
|
1640
1658
|
|
|
1641
|
-
function $
|
|
1659
|
+
function $splitSelectedParagraphsAtInnerLineBreaks(selection) {
|
|
1660
|
+
const topLevelElements = new Set();
|
|
1661
|
+
for (const node of selection.getNodes()) {
|
|
1662
|
+
const topLevel = node.getTopLevelElement();
|
|
1663
|
+
if (topLevel) topLevelElements.add(topLevel);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
for (const element of topLevelElements) {
|
|
1667
|
+
if (!$isParagraphNode(element)) continue
|
|
1668
|
+
|
|
1669
|
+
const children = element.getChildren();
|
|
1670
|
+
if (!children.some($isLineBreakNode)) continue
|
|
1671
|
+
|
|
1672
|
+
const groups = [ [] ];
|
|
1673
|
+
for (const child of children) {
|
|
1674
|
+
if ($isLineBreakNode(child)) {
|
|
1675
|
+
groups.push([]);
|
|
1676
|
+
child.remove();
|
|
1677
|
+
} else {
|
|
1678
|
+
groups[groups.length - 1].push(child);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
for (const group of groups) {
|
|
1683
|
+
if (group.length === 0) continue
|
|
1684
|
+
const paragraph = $createParagraphNode();
|
|
1685
|
+
group.forEach(child => paragraph.append(child));
|
|
1686
|
+
element.insertBefore(paragraph);
|
|
1687
|
+
}
|
|
1688
|
+
if (groups.some(group => group.length > 0)) element.remove();
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function $expandSelectionToLineBreaksAndSplitAtEdges(selection) {
|
|
1642
1693
|
$ensureForwardRangeSelection(selection);
|
|
1643
1694
|
|
|
1644
|
-
|
|
1645
|
-
$
|
|
1646
|
-
|
|
1695
|
+
const focusCaret = $caretFromPoint(selection.focus, "next");
|
|
1696
|
+
const anchorCaret = $caretFromPoint(selection.anchor, "previous");
|
|
1697
|
+
|
|
1698
|
+
// A collapsed cursor adjacent to a <br> would claim it from both sides via
|
|
1699
|
+
// inward-edge; force outward-only walks so each side finds its own boundary.
|
|
1700
|
+
const skipInwardEdge = selection.isCollapsed();
|
|
1701
|
+
const focusBrCaret = $getCaretAtLineBreakBoundary(focusCaret, skipInwardEdge);
|
|
1702
|
+
let anchorBrCaret = $getCaretAtLineBreakBoundary(anchorCaret, skipInwardEdge);
|
|
1703
|
+
|
|
1704
|
+
if (focusBrCaret?.origin.is(anchorBrCaret?.origin)) {
|
|
1705
|
+
anchorBrCaret = null;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// Splitting focus first keeps the anchor <br>'s position stable.
|
|
1709
|
+
const focusOuter = focusBrCaret && $splitAroundLineBreak(focusBrCaret);
|
|
1710
|
+
const anchorOuter = anchorBrCaret && $splitAroundLineBreak(anchorBrCaret);
|
|
1711
|
+
|
|
1712
|
+
const innerStart = anchorOuter?.getNextSibling() ?? selection.anchor.getNode().getTopLevelElement();
|
|
1713
|
+
const innerEnd = focusOuter?.getPreviousSibling() ?? selection.focus.getNode().getTopLevelElement();
|
|
1714
|
+
if (!innerStart || !innerEnd) return
|
|
1715
|
+
|
|
1716
|
+
$setSelectionFromCaretRange($getCaretRange(
|
|
1717
|
+
$normalizeCaret($getChildCaret(innerStart, "next")),
|
|
1718
|
+
$getCaretInDirection(
|
|
1719
|
+
$normalizeCaret($getChildCaret(innerEnd, "previous")),
|
|
1720
|
+
"next",
|
|
1721
|
+
),
|
|
1722
|
+
));
|
|
1647
1723
|
}
|
|
1648
1724
|
|
|
1649
|
-
function $
|
|
1650
|
-
const paragraph =
|
|
1651
|
-
if (!paragraph || !$isParagraphNode(paragraph)) return
|
|
1725
|
+
function $getCaretAtLineBreakBoundary(caret, skipInwardEdge = false) {
|
|
1726
|
+
const paragraph = caret.origin.getTopLevelElement();
|
|
1727
|
+
if (!paragraph || !$isParagraphNode(paragraph)) return null
|
|
1652
1728
|
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
const lineBreakCaret = $caretAtNearestNodeOfType(selectionChild, LineBreakNode, direction);
|
|
1656
|
-
if (!lineBreakCaret) return
|
|
1729
|
+
const lineBreak = (skipInwardEdge ? null : $inwardEdgeLineBreak(caret, paragraph))
|
|
1730
|
+
?? $outwardLineBreak(caret, paragraph);
|
|
1657
1731
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1732
|
+
return lineBreak ? $getSiblingCaret(lineBreak, caret.direction) : null
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// Prefer a <br> the cursor is sitting flush against, except when a further <br>
|
|
1736
|
+
// also exists outward — that one is the real paragraph break for this side.
|
|
1737
|
+
function $inwardEdgeLineBreak(caret, paragraph) {
|
|
1738
|
+
let candidateCaret;
|
|
1660
1739
|
|
|
1661
|
-
if (
|
|
1662
|
-
$
|
|
1740
|
+
if (
|
|
1741
|
+
($isChildCaret(caret) && caret.origin.is(paragraph)) ||
|
|
1742
|
+
($isTextPointCaret(caret) && $isExtendableTextPointCaret(caret.getFlipped()))
|
|
1743
|
+
) {
|
|
1744
|
+
candidateCaret = null;
|
|
1745
|
+
} else if ($isSiblingCaret(caret) && caret.getParentAtCaret().is(paragraph)) {
|
|
1746
|
+
candidateCaret = caret;
|
|
1747
|
+
} else {
|
|
1748
|
+
const childCaret = $paragraphChildCaretAtInwardEdge(caret, paragraph);
|
|
1749
|
+
candidateCaret = childCaret ? $rewindSiblingCaret(childCaret) : null;
|
|
1663
1750
|
}
|
|
1664
1751
|
|
|
1665
|
-
|
|
1752
|
+
if (candidateCaret && $isLineBreakNode(candidateCaret.origin)) {
|
|
1753
|
+
return $candidateUnlessShadowed(candidateCaret)
|
|
1754
|
+
} else {
|
|
1755
|
+
return null
|
|
1756
|
+
}
|
|
1666
1757
|
}
|
|
1667
1758
|
|
|
1668
|
-
function $
|
|
1669
|
-
|
|
1670
|
-
|
|
1759
|
+
function $candidateUnlessShadowed(candidateCaret) {
|
|
1760
|
+
const outward = candidateCaret.getNodeAtCaret();
|
|
1761
|
+
return $isLineBreakNode(outward) ? null : candidateCaret.origin
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function $outwardLineBreak(caret, paragraph) {
|
|
1765
|
+
const startCaret = $outwardWalkStartCaret(caret, paragraph);
|
|
1766
|
+
if (!startCaret) return null
|
|
1767
|
+
|
|
1768
|
+
for (const { origin } of startCaret) {
|
|
1769
|
+
if (!origin.getParent().is(paragraph)) break
|
|
1770
|
+
if ($isLineBreakNode(origin)) return origin
|
|
1671
1771
|
}
|
|
1672
1772
|
return null
|
|
1673
1773
|
}
|
|
1674
1774
|
|
|
1775
|
+
function $outwardWalkStartCaret(caret, paragraph) {
|
|
1776
|
+
if (caret.getParentAtCaret().is(paragraph)) {
|
|
1777
|
+
return caret
|
|
1778
|
+
} else {
|
|
1779
|
+
return $paragraphChildCaretContaining(caret, paragraph)
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function $paragraphChildCaretContaining(caret, paragraph) {
|
|
1784
|
+
let cursor = caret.getSiblingCaret();
|
|
1785
|
+
while (cursor && !cursor.origin.getParent()?.is(paragraph)) {
|
|
1786
|
+
cursor = cursor.getParentCaret();
|
|
1787
|
+
}
|
|
1788
|
+
return cursor?.origin.getParent()?.is(paragraph) ? cursor : null
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// Only succeeds when the cursor is flush against the inward edge of every
|
|
1792
|
+
// ancestor between itself and the paragraph child.
|
|
1793
|
+
function $paragraphChildCaretAtInwardEdge(caret, paragraph) {
|
|
1794
|
+
let cursor = caret.getSiblingCaret();
|
|
1795
|
+
while (cursor && !cursor.origin.getParent()?.is(paragraph)) {
|
|
1796
|
+
if (cursor.getNodeAtCaret()) return null
|
|
1797
|
+
cursor = cursor.getParentCaret();
|
|
1798
|
+
}
|
|
1799
|
+
return cursor?.origin.getParent()?.is(paragraph) ? cursor : null
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function $splitAroundLineBreak(lineBreakCaret) {
|
|
1803
|
+
let outer = null;
|
|
1804
|
+
|
|
1805
|
+
if (lineBreakCaret.getNodeAtCaret() === null) {
|
|
1806
|
+
lineBreakCaret.origin.remove();
|
|
1807
|
+
} else {
|
|
1808
|
+
const lineBreak = lineBreakCaret.origin;
|
|
1809
|
+
const splitCaret = $getCaretInDirection($rewindSiblingCaret(lineBreakCaret), "next");
|
|
1810
|
+
|
|
1811
|
+
$splitAtPointCaretNext(splitCaret);
|
|
1812
|
+
outer = lineBreak.getTopLevelElement();
|
|
1813
|
+
lineBreak.remove();
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
return outer
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1675
1819
|
// Payload: Record<nodeKey, { patch?, replace? }>
|
|
1676
1820
|
// - patch: plain object, shallow-merged into the existing node's properties
|
|
1677
1821
|
// - replace: a LexicalNode instance that replaces the node
|
|
@@ -3548,12 +3692,18 @@ class CommandDispatcher {
|
|
|
3548
3692
|
#registerDragAndDropHandlers() {
|
|
3549
3693
|
if (this.editorElement.supportsAttachments) {
|
|
3550
3694
|
this.dragCounter = 0;
|
|
3551
|
-
const root = this.editor.getRootElement();
|
|
3552
3695
|
this.#listeners.track(
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3696
|
+
this.editor.registerRootListener((rootElement) => {
|
|
3697
|
+
if (rootElement) {
|
|
3698
|
+
const teardowns = [
|
|
3699
|
+
registerEventListener(rootElement, "dragover", this.#handleDragOver.bind(this)),
|
|
3700
|
+
registerEventListener(rootElement, "drop", this.#handleDrop.bind(this)),
|
|
3701
|
+
registerEventListener(rootElement, "dragenter", this.#handleDragEnter.bind(this)),
|
|
3702
|
+
registerEventListener(rootElement, "dragleave", this.#handleDragLeave.bind(this))
|
|
3703
|
+
];
|
|
3704
|
+
return () => teardowns.forEach((teardown) => teardown())
|
|
3705
|
+
}
|
|
3706
|
+
})
|
|
3557
3707
|
);
|
|
3558
3708
|
}
|
|
3559
3709
|
}
|
|
@@ -3723,32 +3873,6 @@ class Selection {
|
|
|
3723
3873
|
return { node: null, offset: 0 }
|
|
3724
3874
|
}
|
|
3725
3875
|
|
|
3726
|
-
preservingSelection(fn) {
|
|
3727
|
-
let selectionState = null;
|
|
3728
|
-
|
|
3729
|
-
this.editor.getEditorState().read(() => {
|
|
3730
|
-
const selection = $getSelection();
|
|
3731
|
-
if (selection && $isRangeSelection(selection)) {
|
|
3732
|
-
selectionState = {
|
|
3733
|
-
anchor: { key: selection.anchor.key, offset: selection.anchor.offset },
|
|
3734
|
-
focus: { key: selection.focus.key, offset: selection.focus.offset }
|
|
3735
|
-
};
|
|
3736
|
-
}
|
|
3737
|
-
});
|
|
3738
|
-
|
|
3739
|
-
fn();
|
|
3740
|
-
|
|
3741
|
-
if (selectionState) {
|
|
3742
|
-
this.editor.update(() => {
|
|
3743
|
-
const selection = $getSelection();
|
|
3744
|
-
if (selection && $isRangeSelection(selection)) {
|
|
3745
|
-
selection.anchor.set(selectionState.anchor.key, selectionState.anchor.offset, "text");
|
|
3746
|
-
selection.focus.set(selectionState.focus.key, selectionState.focus.offset, "text");
|
|
3747
|
-
}
|
|
3748
|
-
});
|
|
3749
|
-
}
|
|
3750
|
-
}
|
|
3751
|
-
|
|
3752
3876
|
getFormat() {
|
|
3753
3877
|
const selection = $getSelection();
|
|
3754
3878
|
if (!$isRangeSelection(selection)) return {}
|
|
@@ -4013,46 +4137,59 @@ class Selection {
|
|
|
4013
4137
|
return $isDecoratorNode(targetNode) && this.#selectInLexical(targetNode)
|
|
4014
4138
|
}, COMMAND_PRIORITY_LOW));
|
|
4015
4139
|
|
|
4016
|
-
const rootElement = this.editor.getRootElement();
|
|
4017
4140
|
this.#listeners.track(
|
|
4018
|
-
|
|
4141
|
+
this.editor.registerRootListener((rootElement) => {
|
|
4142
|
+
if (rootElement) {
|
|
4143
|
+
return registerEventListener(rootElement, "lexxy:internal:move-to-next-line", () => this.#selectOrAppendNextLine())
|
|
4144
|
+
}
|
|
4145
|
+
})
|
|
4019
4146
|
);
|
|
4020
4147
|
}
|
|
4021
4148
|
|
|
4022
4149
|
#containEditorFocus() {
|
|
4023
4150
|
// Workaround for a bizarre Chrome bug where the cursor abandons the editor to focus on not-focusable elements
|
|
4024
4151
|
// above when navigating UP/DOWN when Lexical shows its fake cursor on custom decorator nodes.
|
|
4025
|
-
this.
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4152
|
+
this.#listeners.track(
|
|
4153
|
+
this.editor.registerRootListener((rootElement) => {
|
|
4154
|
+
if (rootElement) {
|
|
4155
|
+
const handler = (event) => this.#handleArrowKeyOnLexicalCursor(event);
|
|
4156
|
+
rootElement.addEventListener("keydown", handler, true);
|
|
4157
|
+
return () => rootElement.removeEventListener("keydown", handler, true)
|
|
4158
|
+
}
|
|
4159
|
+
})
|
|
4160
|
+
);
|
|
4161
|
+
}
|
|
4034
4162
|
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4163
|
+
#handleArrowKeyOnLexicalCursor(event) {
|
|
4164
|
+
if (event.key === "ArrowUp") {
|
|
4165
|
+
const lexicalCursor = this.editor.getRootElement().querySelector("[data-lexical-cursor]");
|
|
4166
|
+
|
|
4167
|
+
if (lexicalCursor) {
|
|
4168
|
+
let currentElement = lexicalCursor.previousElementSibling;
|
|
4169
|
+
while (currentElement && currentElement.hasAttribute("data-lexical-cursor")) {
|
|
4170
|
+
currentElement = currentElement.previousElementSibling;
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
if (!currentElement) {
|
|
4174
|
+
event.preventDefault();
|
|
4038
4175
|
}
|
|
4039
4176
|
}
|
|
4177
|
+
}
|
|
4040
4178
|
|
|
4041
|
-
|
|
4042
|
-
|
|
4179
|
+
if (event.key === "ArrowDown") {
|
|
4180
|
+
const lexicalCursor = this.editor.getRootElement().querySelector("[data-lexical-cursor]");
|
|
4043
4181
|
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4182
|
+
if (lexicalCursor) {
|
|
4183
|
+
let currentElement = lexicalCursor.nextElementSibling;
|
|
4184
|
+
while (currentElement && currentElement.hasAttribute("data-lexical-cursor")) {
|
|
4185
|
+
currentElement = currentElement.nextElementSibling;
|
|
4186
|
+
}
|
|
4049
4187
|
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
}
|
|
4188
|
+
if (!currentElement) {
|
|
4189
|
+
event.preventDefault();
|
|
4053
4190
|
}
|
|
4054
4191
|
}
|
|
4055
|
-
}
|
|
4192
|
+
}
|
|
4056
4193
|
}
|
|
4057
4194
|
|
|
4058
4195
|
#syncSelectedClasses() {
|
|
@@ -5263,6 +5400,7 @@ class Contents {
|
|
|
5263
5400
|
const selection = $getSelection();
|
|
5264
5401
|
if (!$isRangeSelection(selection)) return
|
|
5265
5402
|
|
|
5403
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5266
5404
|
$setBlocksType(selection, () => $createParagraphNode());
|
|
5267
5405
|
}
|
|
5268
5406
|
|
|
@@ -5270,6 +5408,7 @@ class Contents {
|
|
|
5270
5408
|
const selection = $getSelection();
|
|
5271
5409
|
if (!$isRangeSelection(selection)) return
|
|
5272
5410
|
|
|
5411
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5273
5412
|
$setBlocksType(selection, () => $createHeadingNode(tag));
|
|
5274
5413
|
}
|
|
5275
5414
|
|
|
@@ -5311,10 +5450,14 @@ class Contents {
|
|
|
5311
5450
|
if (allCode) {
|
|
5312
5451
|
blockElements.forEach(node => this.#unwrapCodeBlock(node));
|
|
5313
5452
|
} else {
|
|
5453
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5454
|
+
const elements = this.#blockLevelElementsInSelection(selection);
|
|
5455
|
+
if (elements.length === 0) return
|
|
5456
|
+
|
|
5314
5457
|
const codeNode = $createCodeNode("plain");
|
|
5315
|
-
|
|
5458
|
+
elements.at(-1).insertAfter(codeNode);
|
|
5316
5459
|
codeNode.selectEnd();
|
|
5317
|
-
this.insertAtCursor(...
|
|
5460
|
+
this.insertAtCursor(...elements);
|
|
5318
5461
|
}
|
|
5319
5462
|
}
|
|
5320
5463
|
|
|
@@ -5333,8 +5476,7 @@ class Contents {
|
|
|
5333
5476
|
} else {
|
|
5334
5477
|
topLevelElements.filter($isQuoteNode).forEach(node => this.#unwrap(node));
|
|
5335
5478
|
|
|
5336
|
-
|
|
5337
|
-
|
|
5479
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5338
5480
|
const elements = this.#topLevelElementsInSelection(selection);
|
|
5339
5481
|
if (elements.length === 0) return
|
|
5340
5482
|
|
|
@@ -5602,45 +5744,8 @@ class Contents {
|
|
|
5602
5744
|
const selection = $getSelection();
|
|
5603
5745
|
if (!$isRangeSelection(selection)) return
|
|
5604
5746
|
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
#splitParagraphsAtLineBreaks(selection) {
|
|
5609
|
-
const anchorTopLevel = selection.anchor.getNode().getTopLevelElement();
|
|
5610
|
-
const focusTopLevel = selection.focus.getNode().getTopLevelElement();
|
|
5611
|
-
const topLevelElements = this.#topLevelElementsInSelection(selection);
|
|
5612
|
-
|
|
5613
|
-
for (const element of topLevelElements) {
|
|
5614
|
-
if (!$isParagraphNode(element)) continue
|
|
5615
|
-
|
|
5616
|
-
const children = element.getChildren();
|
|
5617
|
-
if (!children.some($isLineBreakNode)) continue
|
|
5618
|
-
|
|
5619
|
-
// Check whether this paragraph needs splitting: skip only if neither
|
|
5620
|
-
// selection endpoint is inside it (meaning it's a middle paragraph
|
|
5621
|
-
// fully between anchor and focus with no partial lines to split off).
|
|
5622
|
-
// Compare top-level elements so endpoints inside nested inline nodes
|
|
5623
|
-
// (e.g. text inside a LinkNode) are still recognized.
|
|
5624
|
-
if (element !== anchorTopLevel && element !== focusTopLevel) continue
|
|
5625
|
-
|
|
5626
|
-
const groups = [ [] ];
|
|
5627
|
-
for (const child of children) {
|
|
5628
|
-
if ($isLineBreakNode(child)) {
|
|
5629
|
-
groups.push([]);
|
|
5630
|
-
child.remove();
|
|
5631
|
-
} else {
|
|
5632
|
-
groups[groups.length - 1].push(child);
|
|
5633
|
-
}
|
|
5634
|
-
}
|
|
5635
|
-
|
|
5636
|
-
for (const group of groups) {
|
|
5637
|
-
if (group.length === 0) continue
|
|
5638
|
-
const paragraph = $createParagraphNode();
|
|
5639
|
-
group.forEach(child => paragraph.append(child));
|
|
5640
|
-
element.insertBefore(paragraph);
|
|
5641
|
-
}
|
|
5642
|
-
if (groups.some(group => group.length > 0)) element.remove();
|
|
5643
|
-
}
|
|
5747
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5748
|
+
$splitSelectedParagraphsAtInnerLineBreaks(selection);
|
|
5644
5749
|
}
|
|
5645
5750
|
|
|
5646
5751
|
#blockLevelElementsInSelection(selection) {
|
|
@@ -5905,8 +6010,12 @@ class Clipboard {
|
|
|
5905
6010
|
|
|
5906
6011
|
#isOnlyURLPasted(clipboardData) {
|
|
5907
6012
|
// Safari URLs are copied as a text/plain + text/uri-list object
|
|
6013
|
+
// App ShareSheet URLs are copied as solo text/uri-list object
|
|
5908
6014
|
const types = Array.from(clipboardData.types);
|
|
5909
|
-
return types.length
|
|
6015
|
+
return types.length
|
|
6016
|
+
&& types.length <= 2
|
|
6017
|
+
&& types.includes("text/uri-list")
|
|
6018
|
+
&& (types.length < 2 || types.includes("text/plain"))
|
|
5910
6019
|
}
|
|
5911
6020
|
|
|
5912
6021
|
#isPastingIntoCodeBlock() {
|
|
@@ -6964,7 +7073,11 @@ class AttachmentDragAndDrop {
|
|
|
6964
7073
|
const ATTACHMENT_ATTRIBUTES = [ "alt", "caption", "content", "content-type", "data-direct-upload-id",
|
|
6965
7074
|
"data-sgid", "filename", "filesize", "height", "presentation", "previewable", "sgid", "url", "width" ];
|
|
6966
7075
|
|
|
7076
|
+
const UPLOADS_BUSY_MESSAGE = "Please wait for all files to upload";
|
|
7077
|
+
|
|
6967
7078
|
class AttachmentsExtension extends LexxyExtension {
|
|
7079
|
+
#uploadsCount = 0
|
|
7080
|
+
|
|
6968
7081
|
get enabled() {
|
|
6969
7082
|
return this.editorElement.supportsAttachments
|
|
6970
7083
|
}
|
|
@@ -6981,17 +7094,41 @@ class AttachmentsExtension extends LexxyExtension {
|
|
|
6981
7094
|
ActionTextAttachmentUploadNode,
|
|
6982
7095
|
ImageGalleryNode
|
|
6983
7096
|
],
|
|
6984
|
-
register(editor) {
|
|
7097
|
+
register: (editor) => {
|
|
6985
7098
|
const dragAndDrop = new AttachmentDragAndDrop(editor);
|
|
6986
7099
|
|
|
6987
7100
|
return mergeRegister(
|
|
6988
7101
|
editor.registerNodeTransform(ActionTextAttachmentNode, $extractAttachmentFromParagraph),
|
|
6989
7102
|
editor.registerCommand(DELETE_CHARACTER_COMMAND, $collapseIntoGallery, COMMAND_PRIORITY_NORMAL),
|
|
7103
|
+
editor.registerMutationListener(ActionTextAttachmentUploadNode, this.#handleUploadMutations.bind(this)),
|
|
6990
7104
|
() => dragAndDrop.destroy()
|
|
6991
7105
|
)
|
|
6992
7106
|
}
|
|
6993
7107
|
})
|
|
6994
7108
|
}
|
|
7109
|
+
|
|
7110
|
+
#handleUploadMutations(mutations) {
|
|
7111
|
+
const previousUploadsCount = this.#uploadsCount;
|
|
7112
|
+
for (const [ , mutation ] of mutations) {
|
|
7113
|
+
if (mutation === "created") {
|
|
7114
|
+
this.#uploadsCount++;
|
|
7115
|
+
} else if (mutation === "destroyed") {
|
|
7116
|
+
this.#uploadsCount--;
|
|
7117
|
+
}
|
|
7118
|
+
}
|
|
7119
|
+
|
|
7120
|
+
if (this.#uploadsCount !== previousUploadsCount) {
|
|
7121
|
+
this.#setUploadsValidity();
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
|
|
7125
|
+
#setUploadsValidity() {
|
|
7126
|
+
if (this.#uploadsCount) {
|
|
7127
|
+
this.setEditorValidity({ customError: true }, UPLOADS_BUSY_MESSAGE);
|
|
7128
|
+
} else {
|
|
7129
|
+
this.setEditorValidity({});
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
6995
7132
|
}
|
|
6996
7133
|
|
|
6997
7134
|
// Decorator nodes can be wrapped in a Paragraph Node by Lexical when contained in a <div>
|
|
@@ -7399,12 +7536,16 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7399
7536
|
static observedAttributes = [ "connected", "required" ]
|
|
7400
7537
|
|
|
7401
7538
|
#initialValue = ""
|
|
7402
|
-
#
|
|
7403
|
-
#
|
|
7539
|
+
#initializeEventDispatched = false
|
|
7540
|
+
#editorInitializedDispatched = false
|
|
7541
|
+
#valueLoaded = false
|
|
7404
7542
|
#listeners = new ListenerBin()
|
|
7405
7543
|
#disposables = []
|
|
7406
7544
|
#historyState = { undo: false, redo: false }
|
|
7407
7545
|
|
|
7546
|
+
#validity = new Map()
|
|
7547
|
+
#validationTextArea = document.createElement("textarea")
|
|
7548
|
+
|
|
7408
7549
|
constructor() {
|
|
7409
7550
|
super();
|
|
7410
7551
|
this.internals = this.attachInternals();
|
|
@@ -7437,29 +7578,40 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7437
7578
|
|
|
7438
7579
|
this.#initialize();
|
|
7439
7580
|
|
|
7440
|
-
this.#scheduleEditorInitializedDispatch();
|
|
7441
7581
|
this.toggleAttribute("connected", true);
|
|
7442
7582
|
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7583
|
+
requestAnimationFrame(() => {
|
|
7584
|
+
this.#mountRoot();
|
|
7585
|
+
this.#handleAutofocus();
|
|
7586
|
+
this.#dispatchInitialize();
|
|
7587
|
+
});
|
|
7446
7588
|
}
|
|
7447
7589
|
|
|
7448
7590
|
disconnectedCallback() {
|
|
7449
|
-
this.#
|
|
7450
|
-
this
|
|
7591
|
+
this.#initializeEventDispatched = false;
|
|
7592
|
+
this.#editorInitializedDispatched = false;
|
|
7593
|
+
if (this.#valueLoaded) {
|
|
7594
|
+
this.valueBeforeDisconnect = this.value;
|
|
7595
|
+
} else {
|
|
7596
|
+
this.valueBeforeDisconnect = null;
|
|
7597
|
+
}
|
|
7598
|
+
this.#valueLoaded = false;
|
|
7451
7599
|
this.#reset(); // Prevent hangs with Safari when morphing
|
|
7452
7600
|
}
|
|
7453
7601
|
|
|
7454
7602
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
7455
|
-
if (name === "connected"
|
|
7603
|
+
if (name === "connected") this.connectedChangedCallback(oldValue, newValue);
|
|
7604
|
+
if (name === "required") this.requiredChangedCallback(oldValue, newValue);
|
|
7605
|
+
}
|
|
7606
|
+
|
|
7607
|
+
connectedChangedCallback(oldValue, newValue) {
|
|
7608
|
+
if (this.isConnected && oldValue != null && oldValue !== newValue) {
|
|
7456
7609
|
requestAnimationFrame(() => this.#reconnect());
|
|
7457
7610
|
}
|
|
7611
|
+
}
|
|
7458
7612
|
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
this.#setValidity();
|
|
7462
|
-
}
|
|
7613
|
+
requiredChangedCallback() {
|
|
7614
|
+
if (this.isConnected) this.#requestValidityRefresh();
|
|
7463
7615
|
}
|
|
7464
7616
|
|
|
7465
7617
|
formResetCallback() {
|
|
@@ -7485,6 +7637,27 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7485
7637
|
return this.getAttribute("name")
|
|
7486
7638
|
}
|
|
7487
7639
|
|
|
7640
|
+
get required() {
|
|
7641
|
+
return this.hasAttribute("required")
|
|
7642
|
+
}
|
|
7643
|
+
|
|
7644
|
+
get validity() {
|
|
7645
|
+
return this.internals.validity
|
|
7646
|
+
}
|
|
7647
|
+
|
|
7648
|
+
checkValidity() {
|
|
7649
|
+
return this.internals.checkValidity()
|
|
7650
|
+
}
|
|
7651
|
+
|
|
7652
|
+
reportValidity() {
|
|
7653
|
+
return this.internals.reportValidity()
|
|
7654
|
+
}
|
|
7655
|
+
|
|
7656
|
+
setElementValidity(key, flags, message) {
|
|
7657
|
+
this.#validity.set(key, { flags, message });
|
|
7658
|
+
this.#requestValidityRefresh();
|
|
7659
|
+
}
|
|
7660
|
+
|
|
7488
7661
|
get toolbarElement() {
|
|
7489
7662
|
if (!this.#hasToolbar) return null
|
|
7490
7663
|
|
|
@@ -7580,7 +7753,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7580
7753
|
|
|
7581
7754
|
if (!this.editor) return
|
|
7582
7755
|
|
|
7583
|
-
this.#
|
|
7756
|
+
this.#editorInitializedDispatched = true;
|
|
7584
7757
|
this.#dispatchEditorInitialized();
|
|
7585
7758
|
this.#dispatchAttributesChange();
|
|
7586
7759
|
}
|
|
@@ -7635,6 +7808,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7635
7808
|
}
|
|
7636
7809
|
|
|
7637
7810
|
set value(html) {
|
|
7811
|
+
this.#valueLoaded = true;
|
|
7638
7812
|
const editorHasFocus = this.#isContentFocused;
|
|
7639
7813
|
|
|
7640
7814
|
this.editor.update(() => {
|
|
@@ -7731,11 +7905,17 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7731
7905
|
...this.extensions.lexicalExtensions
|
|
7732
7906
|
);
|
|
7733
7907
|
|
|
7734
|
-
editor.setRootElement(this.editorContentElement);
|
|
7735
|
-
|
|
7736
7908
|
return editor
|
|
7737
7909
|
}
|
|
7738
7910
|
|
|
7911
|
+
// Toggling editable around setRootElement skips Lexical's DOM-selection sync,
|
|
7912
|
+
// which would otherwise steal focus from elsewhere on the page.
|
|
7913
|
+
#mountRoot() {
|
|
7914
|
+
this.editor.setEditable(false);
|
|
7915
|
+
this.editor.setRootElement(this.editorContentElement);
|
|
7916
|
+
this.editor.setEditable(true);
|
|
7917
|
+
}
|
|
7918
|
+
|
|
7739
7919
|
get #lexicalNodes() {
|
|
7740
7920
|
const nodes = [ CustomActionTextAttachmentNode ];
|
|
7741
7921
|
|
|
@@ -7792,7 +7972,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7792
7972
|
|
|
7793
7973
|
this.internals.setFormValue(html);
|
|
7794
7974
|
this._internalFormValue = html;
|
|
7795
|
-
this.#validationTextArea.value = this.isEmpty ? "" : html;
|
|
7796
7975
|
|
|
7797
7976
|
if (changed) {
|
|
7798
7977
|
dispatch(this, "lexxy:change");
|
|
@@ -7804,10 +7983,12 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7804
7983
|
}
|
|
7805
7984
|
|
|
7806
7985
|
#loadInitialValue() {
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
this.
|
|
7810
|
-
|
|
7986
|
+
if (!this.#valueLoaded) {
|
|
7987
|
+
const initialHtml = this.valueBeforeDisconnect || this.getAttribute("value") || "<p><br></p>";
|
|
7988
|
+
this.editor.update(() => {
|
|
7989
|
+
this.value = this.#initialValue = initialHtml;
|
|
7990
|
+
}, { tag: HISTORY_MERGE_TAG });
|
|
7991
|
+
}
|
|
7811
7992
|
}
|
|
7812
7993
|
|
|
7813
7994
|
#resetBeforeTurboCaches() {
|
|
@@ -7827,11 +8008,50 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7827
8008
|
this.#clearCachedValues();
|
|
7828
8009
|
this.#internalFormValue = this.value;
|
|
7829
8010
|
this.#toggleEmptyStatus();
|
|
7830
|
-
this.#
|
|
8011
|
+
this.#requestValidityRefresh();
|
|
7831
8012
|
this.#dispatchAttributesChange();
|
|
7832
8013
|
}));
|
|
7833
8014
|
}
|
|
7834
8015
|
|
|
8016
|
+
async #requestValidityRefresh() {
|
|
8017
|
+
await nextFrame();
|
|
8018
|
+
|
|
8019
|
+
if (this.isConnected) this.#refreshValidity();
|
|
8020
|
+
}
|
|
8021
|
+
|
|
8022
|
+
#refreshValidity() {
|
|
8023
|
+
this.#refreshInternalValidity();
|
|
8024
|
+
const { validity, message } = this.#calculateValidity();
|
|
8025
|
+
this.internals.setValidity(validity, message, this.editorContentElement);
|
|
8026
|
+
}
|
|
8027
|
+
|
|
8028
|
+
#refreshInternalValidity() {
|
|
8029
|
+
this.#validationTextArea.required = this.required && this.isBlank;
|
|
8030
|
+
const flags = this.#validationTextArea.validity;
|
|
8031
|
+
const message = this.#validationTextArea.validationMessage;
|
|
8032
|
+
|
|
8033
|
+
this.#validity.set(this, { flags, message });
|
|
8034
|
+
}
|
|
8035
|
+
|
|
8036
|
+
#calculateValidity() {
|
|
8037
|
+
const validity = {};
|
|
8038
|
+
const messages = [];
|
|
8039
|
+
|
|
8040
|
+
for (const { flags, message } of this.#validity.values()) {
|
|
8041
|
+
// internal TextArea's ValidityState can contain `valid: true`
|
|
8042
|
+
if (flags.valid === true) continue
|
|
8043
|
+
|
|
8044
|
+
for (const flag in flags) {
|
|
8045
|
+
if (flags[flag]) {
|
|
8046
|
+
validity[flag] = true;
|
|
8047
|
+
messages.push(message);
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
}
|
|
8051
|
+
|
|
8052
|
+
return { validity, message: messages.join("\n") }
|
|
8053
|
+
}
|
|
8054
|
+
|
|
7835
8055
|
#clearCachedValues() {
|
|
7836
8056
|
this.cachedValue = null;
|
|
7837
8057
|
this.cachedStringValue = null;
|
|
@@ -7987,14 +8207,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7987
8207
|
this.classList.toggle("lexxy-editor--empty", this.isEmpty);
|
|
7988
8208
|
}
|
|
7989
8209
|
|
|
7990
|
-
#setValidity() {
|
|
7991
|
-
if (this.#validationTextArea.validity.valid) {
|
|
7992
|
-
this.internals.setValidity({});
|
|
7993
|
-
} else {
|
|
7994
|
-
this.internals.setValidity(this.#validationTextArea.validity, this.#validationTextArea.validationMessage, this.editorContentElement);
|
|
7995
|
-
}
|
|
7996
|
-
}
|
|
7997
|
-
|
|
7998
8210
|
#configureSanitizer() {
|
|
7999
8211
|
setSanitizerConfig(this.#allowedElements);
|
|
8000
8212
|
}
|
|
@@ -8058,22 +8270,18 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8058
8270
|
});
|
|
8059
8271
|
}
|
|
8060
8272
|
|
|
8061
|
-
#
|
|
8062
|
-
this
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
dispatch(this, "lexxy:initialize");
|
|
8068
|
-
this.#dispatchEditorInitialized();
|
|
8069
|
-
});
|
|
8070
|
-
}
|
|
8071
|
-
|
|
8072
|
-
#cancelEditorInitializedDispatch() {
|
|
8073
|
-
if (this.#editorInitializedRafId == null) return
|
|
8273
|
+
#dispatchInitialize() {
|
|
8274
|
+
if (this.isConnected && this.adapter) {
|
|
8275
|
+
if (!this.#initializeEventDispatched) {
|
|
8276
|
+
this.#initializeEventDispatched = true;
|
|
8277
|
+
dispatch(this, "lexxy:initialize");
|
|
8278
|
+
}
|
|
8074
8279
|
|
|
8075
|
-
|
|
8076
|
-
|
|
8280
|
+
if (!this.#editorInitializedDispatched) {
|
|
8281
|
+
this.#editorInitializedDispatched = true;
|
|
8282
|
+
this.#dispatchEditorInitialized();
|
|
8283
|
+
}
|
|
8284
|
+
}
|
|
8077
8285
|
}
|
|
8078
8286
|
|
|
8079
8287
|
get #resolvedHighlightColors() {
|
|
@@ -8124,8 +8332,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8124
8332
|
}
|
|
8125
8333
|
|
|
8126
8334
|
#reset() {
|
|
8127
|
-
this.#cancelEditorInitializedDispatch();
|
|
8128
8335
|
this.#dispose();
|
|
8336
|
+
this.#resetValidity();
|
|
8129
8337
|
this.editorContentElement?.remove();
|
|
8130
8338
|
this.editorContentElement = null;
|
|
8131
8339
|
|
|
@@ -8145,6 +8353,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8145
8353
|
this.valueBeforeDisconnect = null;
|
|
8146
8354
|
this.connectedCallback();
|
|
8147
8355
|
}
|
|
8356
|
+
|
|
8357
|
+
#resetValidity() {
|
|
8358
|
+
this.#validity = new Map();
|
|
8359
|
+
}
|
|
8148
8360
|
}
|
|
8149
8361
|
|
|
8150
8362
|
// Like $getRoot().getTextContent() but uses readable text for custom attachment nodes
|
|
@@ -8532,6 +8744,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8532
8744
|
|
|
8533
8745
|
if (this.#doesSpaceSelect) {
|
|
8534
8746
|
this.#popoverListeners.track(this.#editor.registerCommand(KEY_SPACE_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_CRITICAL));
|
|
8747
|
+
this.#popoverListeners.track(this.#editor.registerCommand(INPUT_COMMAND, this.#handleInputCommand.bind(this), COMMAND_PRIORITY_CRITICAL));
|
|
8535
8748
|
}
|
|
8536
8749
|
|
|
8537
8750
|
// Register arrow keys with CRITICAL priority to prevent Lexical's selection handlers from running
|
|
@@ -8565,16 +8778,12 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8565
8778
|
return Array.from(this.popoverElement.querySelectorAll(".lexxy-prompt-menu__item"))
|
|
8566
8779
|
}
|
|
8567
8780
|
|
|
8568
|
-
#selectOption(listItem) {
|
|
8781
|
+
#selectOption(listItem, { scrollIntoView = false } = {}) {
|
|
8569
8782
|
this.#clearListItemSelection();
|
|
8570
8783
|
listItem.toggleAttribute("aria-selected", true);
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
// Preserve selection to prevent cursor jump
|
|
8575
|
-
this.#selection.preservingSelection(() => {
|
|
8576
|
-
this.#editorElement.focus();
|
|
8577
|
-
});
|
|
8784
|
+
if (scrollIntoView) {
|
|
8785
|
+
listItem.scrollIntoView({ block: "nearest", container: "nearest", behavior: "smooth" });
|
|
8786
|
+
}
|
|
8578
8787
|
|
|
8579
8788
|
this.#setEditorAssociationAttribute("aria-controls", this.popoverElement.id);
|
|
8580
8789
|
this.#setEditorAssociationAttribute("aria-activedescendant", listItem.id);
|
|
@@ -8718,17 +8927,22 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8718
8927
|
}
|
|
8719
8928
|
});
|
|
8720
8929
|
}
|
|
8721
|
-
// Arrow keys are
|
|
8930
|
+
// Arrow keys are handled via Lexical commands
|
|
8931
|
+
}
|
|
8932
|
+
|
|
8933
|
+
// Android Mobile keyboard doesn't trigger KEY_SPACE_COMMAND
|
|
8934
|
+
#handleInputCommand(event) {
|
|
8935
|
+
if (event.inputType === "insertText" && event.data === " ") return this.#handleSelectedOption(event)
|
|
8722
8936
|
}
|
|
8723
8937
|
|
|
8724
8938
|
#moveSelectionDown() {
|
|
8725
8939
|
const nextIndex = this.#selectedIndex + 1;
|
|
8726
|
-
if (nextIndex < this.#listItemElements.length) this.#selectOption(this.#listItemElements[nextIndex]);
|
|
8940
|
+
if (nextIndex < this.#listItemElements.length) this.#selectOption(this.#listItemElements[nextIndex], { scrollIntoView: true });
|
|
8727
8941
|
}
|
|
8728
8942
|
|
|
8729
8943
|
#moveSelectionUp() {
|
|
8730
8944
|
const previousIndex = this.#selectedIndex - 1;
|
|
8731
|
-
if (previousIndex >= 0) this.#selectOption(this.#listItemElements[previousIndex]);
|
|
8945
|
+
if (previousIndex >= 0) this.#selectOption(this.#listItemElements[previousIndex], { scrollIntoView: true });
|
|
8732
8946
|
}
|
|
8733
8947
|
|
|
8734
8948
|
get #selectedIndex() {
|
|
@@ -9147,6 +9361,8 @@ class TableController {
|
|
|
9147
9361
|
|
|
9148
9362
|
this.currentCellKey = cellNode?.getKey() ?? null;
|
|
9149
9363
|
this.currentTableNodeKey = tableNode?.getKey() ?? null;
|
|
9364
|
+
|
|
9365
|
+
return tableNode
|
|
9150
9366
|
}
|
|
9151
9367
|
|
|
9152
9368
|
executeTableCommand(command, customIndex = null) {
|
|
@@ -9649,9 +9865,8 @@ class TableTools extends HTMLElement {
|
|
|
9649
9865
|
|
|
9650
9866
|
#monitorForTableSelection() {
|
|
9651
9867
|
this.#listeners.track(this.#editor.registerUpdateListener(() => {
|
|
9652
|
-
this.tableController.updateSelectedTable();
|
|
9868
|
+
const tableNode = this.#editor.getRootElement() && this.tableController.updateSelectedTable();
|
|
9653
9869
|
|
|
9654
|
-
const tableNode = this.tableController.currentTableNode;
|
|
9655
9870
|
if (tableNode) {
|
|
9656
9871
|
this.#show();
|
|
9657
9872
|
} else {
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
hyphens: auto;
|
|
13
13
|
margin-block: 0 var(--lexxy-content-margin);
|
|
14
14
|
overflow-wrap: break-word;
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
@supports (text-wrap: balance) {
|
|
17
|
+
text-wrap: balance;
|
|
18
|
+
}
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
h1 { font-size: 2rem; }
|
|
@@ -35,7 +38,10 @@
|
|
|
35
38
|
|
|
36
39
|
&:not(lexxy-editor &) {
|
|
37
40
|
overflow-wrap: break-word;
|
|
38
|
-
|
|
41
|
+
|
|
42
|
+
@supports (text-wrap: pretty) {
|
|
43
|
+
text-wrap: pretty;
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -114,11 +120,14 @@
|
|
|
114
120
|
hyphens: none;
|
|
115
121
|
margin-block: 0 var(--lexxy-content-margin);
|
|
116
122
|
overflow-x: auto;
|
|
123
|
+
overflow-wrap: break-word;
|
|
117
124
|
padding: 1ch;
|
|
118
125
|
tab-size: 2;
|
|
119
|
-
text-wrap: nowrap;
|
|
120
126
|
white-space: pre;
|
|
121
|
-
|
|
127
|
+
|
|
128
|
+
@supports (text-wrap: nowrap) {
|
|
129
|
+
text-wrap: nowrap;
|
|
130
|
+
}
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
|
|
@@ -275,8 +284,11 @@
|
|
|
275
284
|
|
|
276
285
|
*:is(code, pre) {
|
|
277
286
|
hyphens: auto;
|
|
278
|
-
text-wrap: wrap;
|
|
279
287
|
white-space: pre-wrap;
|
|
288
|
+
|
|
289
|
+
@supports (text-wrap: wrap) {
|
|
290
|
+
text-wrap: wrap;
|
|
291
|
+
}
|
|
280
292
|
}
|
|
281
293
|
}
|
|
282
294
|
}
|
|
@@ -338,7 +350,11 @@
|
|
|
338
350
|
display: block;
|
|
339
351
|
margin-inline: auto;
|
|
340
352
|
max-inline-size: 100%;
|
|
341
|
-
user-select: none;
|
|
353
|
+
-webkit-user-select: none;
|
|
354
|
+
|
|
355
|
+
@supports (user-select: none) {
|
|
356
|
+
user-select: none;
|
|
357
|
+
}
|
|
342
358
|
}
|
|
343
359
|
|
|
344
360
|
> a {
|
|
@@ -166,6 +166,7 @@
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
&.lexxy-content__table--selection {
|
|
169
|
+
/* eslint-disable-next-line css/use-baseline */
|
|
169
170
|
::selection {
|
|
170
171
|
background: transparent;
|
|
171
172
|
}
|
|
@@ -366,9 +367,12 @@
|
|
|
366
367
|
inline-size: 100%;
|
|
367
368
|
max-inline-size: 100%;
|
|
368
369
|
padding: 0;
|
|
369
|
-
resize: none;
|
|
370
370
|
text-align: center;
|
|
371
371
|
|
|
372
|
+
@supports (resize: none) {
|
|
373
|
+
resize: none;
|
|
374
|
+
}
|
|
375
|
+
|
|
372
376
|
&:focus {
|
|
373
377
|
background: var(--lexxy-color-canvas);
|
|
374
378
|
outline: 0;
|
|
@@ -497,7 +501,11 @@
|
|
|
497
501
|
fill: currentColor;
|
|
498
502
|
grid-area: 1/1;
|
|
499
503
|
inline-size: var(--lexxy-toolbar-icon-size);
|
|
500
|
-
user-select: none;
|
|
504
|
+
-webkit-user-select: none;
|
|
505
|
+
|
|
506
|
+
@supports (user-select: none) {
|
|
507
|
+
user-select: none;
|
|
508
|
+
}
|
|
501
509
|
}
|
|
502
510
|
|
|
503
511
|
&.lexxy-editor__toolbar-group-end {
|
|
@@ -528,9 +536,12 @@
|
|
|
528
536
|
/* Dropdowns */
|
|
529
537
|
|
|
530
538
|
:where(.lexxy-editor__toolbar-dropdown) {
|
|
531
|
-
user-select: none;
|
|
532
539
|
-webkit-user-select: none;
|
|
533
540
|
|
|
541
|
+
@supports (user-select: none) {
|
|
542
|
+
user-select: none;
|
|
543
|
+
}
|
|
544
|
+
|
|
534
545
|
.lexxy-editor__toolbar-button {
|
|
535
546
|
color: currentColor;
|
|
536
547
|
|
|
@@ -806,9 +817,12 @@
|
|
|
806
817
|
line-height: inherit;
|
|
807
818
|
min-block-size: var(--button-size);
|
|
808
819
|
min-inline-size: var(--button-size);
|
|
809
|
-
user-select: none;
|
|
810
820
|
-webkit-user-select: none;
|
|
811
821
|
|
|
822
|
+
@supports (user-select: none) {
|
|
823
|
+
user-select: none;
|
|
824
|
+
}
|
|
825
|
+
|
|
812
826
|
@media(any-hover: hover) {
|
|
813
827
|
&:hover:not([aria-disabled="true"]),
|
|
814
828
|
[open] &:is(summary) {
|