@37signals/lexxy 0.9.12-beta → 0.9.13-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lexxy.esm.js +443 -230
- package/dist/stylesheets/lexxy-content.css +22 -6
- package/dist/stylesheets/lexxy-editor.css +18 -4
- package/package.json +1 -1
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();
|
|
@@ -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() {
|
|
@@ -1486,6 +1499,10 @@ class LexxyExtension {
|
|
|
1486
1499
|
|
|
1487
1500
|
}
|
|
1488
1501
|
|
|
1502
|
+
setEditorValidity(flags, message) {
|
|
1503
|
+
this.editorElement.setElementValidity(this, flags, message);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1489
1506
|
dispose() {
|
|
1490
1507
|
}
|
|
1491
1508
|
}
|
|
@@ -1638,40 +1655,166 @@ function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
|
|
|
1638
1655
|
&& previousNode instanceof CustomActionTextAttachmentNode
|
|
1639
1656
|
}
|
|
1640
1657
|
|
|
1641
|
-
function $
|
|
1658
|
+
function $splitSelectedParagraphsAtInnerLineBreaks(selection) {
|
|
1659
|
+
const topLevelElements = new Set();
|
|
1660
|
+
for (const node of selection.getNodes()) {
|
|
1661
|
+
const topLevel = node.getTopLevelElement();
|
|
1662
|
+
if (topLevel) topLevelElements.add(topLevel);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
for (const element of topLevelElements) {
|
|
1666
|
+
if (!$isParagraphNode(element)) continue
|
|
1667
|
+
|
|
1668
|
+
const children = element.getChildren();
|
|
1669
|
+
if (!children.some($isLineBreakNode)) continue
|
|
1670
|
+
|
|
1671
|
+
const groups = [ [] ];
|
|
1672
|
+
for (const child of children) {
|
|
1673
|
+
if ($isLineBreakNode(child)) {
|
|
1674
|
+
groups.push([]);
|
|
1675
|
+
child.remove();
|
|
1676
|
+
} else {
|
|
1677
|
+
groups[groups.length - 1].push(child);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
for (const group of groups) {
|
|
1682
|
+
if (group.length === 0) continue
|
|
1683
|
+
const paragraph = $createParagraphNode();
|
|
1684
|
+
group.forEach(child => paragraph.append(child));
|
|
1685
|
+
element.insertBefore(paragraph);
|
|
1686
|
+
}
|
|
1687
|
+
if (groups.some(group => group.length > 0)) element.remove();
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function $expandSelectionToLineBreaksAndSplitAtEdges(selection) {
|
|
1642
1692
|
$ensureForwardRangeSelection(selection);
|
|
1643
1693
|
|
|
1644
|
-
|
|
1645
|
-
$
|
|
1646
|
-
|
|
1694
|
+
const focusCaret = $caretFromPoint(selection.focus, "next");
|
|
1695
|
+
const anchorCaret = $caretFromPoint(selection.anchor, "previous");
|
|
1696
|
+
|
|
1697
|
+
// A collapsed cursor adjacent to a <br> would claim it from both sides via
|
|
1698
|
+
// inward-edge; force outward-only walks so each side finds its own boundary.
|
|
1699
|
+
const skipInwardEdge = selection.isCollapsed();
|
|
1700
|
+
const focusBrCaret = $getCaretAtLineBreakBoundary(focusCaret, skipInwardEdge);
|
|
1701
|
+
let anchorBrCaret = $getCaretAtLineBreakBoundary(anchorCaret, skipInwardEdge);
|
|
1702
|
+
|
|
1703
|
+
if (focusBrCaret?.origin.is(anchorBrCaret?.origin)) {
|
|
1704
|
+
anchorBrCaret = null;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// Splitting focus first keeps the anchor <br>'s position stable.
|
|
1708
|
+
const focusOuter = focusBrCaret && $splitAroundLineBreak(focusBrCaret);
|
|
1709
|
+
const anchorOuter = anchorBrCaret && $splitAroundLineBreak(anchorBrCaret);
|
|
1710
|
+
|
|
1711
|
+
const innerStart = anchorOuter?.getNextSibling() ?? selection.anchor.getNode().getTopLevelElement();
|
|
1712
|
+
const innerEnd = focusOuter?.getPreviousSibling() ?? selection.focus.getNode().getTopLevelElement();
|
|
1713
|
+
if (!innerStart || !innerEnd) return
|
|
1714
|
+
|
|
1715
|
+
$setSelectionFromCaretRange($getCaretRange(
|
|
1716
|
+
$normalizeCaret($getChildCaret(innerStart, "next")),
|
|
1717
|
+
$getCaretInDirection(
|
|
1718
|
+
$normalizeCaret($getChildCaret(innerEnd, "previous")),
|
|
1719
|
+
"next",
|
|
1720
|
+
),
|
|
1721
|
+
));
|
|
1647
1722
|
}
|
|
1648
1723
|
|
|
1649
|
-
function $
|
|
1650
|
-
const paragraph =
|
|
1651
|
-
if (!paragraph || !$isParagraphNode(paragraph)) return
|
|
1724
|
+
function $getCaretAtLineBreakBoundary(caret, skipInwardEdge = false) {
|
|
1725
|
+
const paragraph = caret.origin.getTopLevelElement();
|
|
1726
|
+
if (!paragraph || !$isParagraphNode(paragraph)) return null
|
|
1727
|
+
|
|
1728
|
+
const lineBreak = (skipInwardEdge ? null : $inwardEdgeLineBreak(caret, paragraph))
|
|
1729
|
+
?? $outwardLineBreak(caret, paragraph);
|
|
1652
1730
|
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
const lineBreakCaret = $caretAtNearestNodeOfType(selectionChild, LineBreakNode, direction);
|
|
1656
|
-
if (!lineBreakCaret) return
|
|
1731
|
+
return lineBreak ? $getSiblingCaret(lineBreak, caret.direction) : null
|
|
1732
|
+
}
|
|
1657
1733
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1734
|
+
// Prefer a <br> the cursor is sitting flush against, except when a further <br>
|
|
1735
|
+
// also exists outward — that one is the real paragraph break for this side.
|
|
1736
|
+
function $inwardEdgeLineBreak(caret, paragraph) {
|
|
1737
|
+
let candidateCaret;
|
|
1660
1738
|
|
|
1661
|
-
if (
|
|
1662
|
-
$
|
|
1739
|
+
if (
|
|
1740
|
+
($isChildCaret(caret) && caret.origin.is(paragraph)) ||
|
|
1741
|
+
($isTextPointCaret(caret) && $isExtendableTextPointCaret(caret.getFlipped()))
|
|
1742
|
+
) {
|
|
1743
|
+
candidateCaret = null;
|
|
1744
|
+
} else if ($isSiblingCaret(caret) && caret.getParentAtCaret().is(paragraph)) {
|
|
1745
|
+
candidateCaret = caret;
|
|
1746
|
+
} else {
|
|
1747
|
+
const childCaret = $paragraphChildCaretAtInwardEdge(caret, paragraph);
|
|
1748
|
+
candidateCaret = childCaret ? $rewindSiblingCaret(childCaret) : null;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (candidateCaret && $isLineBreakNode(candidateCaret.origin)) {
|
|
1752
|
+
return $candidateUnlessShadowed(candidateCaret)
|
|
1753
|
+
} else {
|
|
1754
|
+
return null
|
|
1663
1755
|
}
|
|
1756
|
+
}
|
|
1664
1757
|
|
|
1665
|
-
|
|
1758
|
+
function $candidateUnlessShadowed(candidateCaret) {
|
|
1759
|
+
const outward = candidateCaret.getNodeAtCaret();
|
|
1760
|
+
return $isLineBreakNode(outward) ? null : candidateCaret.origin
|
|
1666
1761
|
}
|
|
1667
1762
|
|
|
1668
|
-
function $
|
|
1669
|
-
|
|
1670
|
-
|
|
1763
|
+
function $outwardLineBreak(caret, paragraph) {
|
|
1764
|
+
const startCaret = $outwardWalkStartCaret(caret, paragraph);
|
|
1765
|
+
if (!startCaret) return null
|
|
1766
|
+
|
|
1767
|
+
for (const { origin } of startCaret) {
|
|
1768
|
+
if (!origin.getParent().is(paragraph)) break
|
|
1769
|
+
if ($isLineBreakNode(origin)) return origin
|
|
1671
1770
|
}
|
|
1672
1771
|
return null
|
|
1673
1772
|
}
|
|
1674
1773
|
|
|
1774
|
+
function $outwardWalkStartCaret(caret, paragraph) {
|
|
1775
|
+
if (caret.getParentAtCaret().is(paragraph)) {
|
|
1776
|
+
return caret
|
|
1777
|
+
} else {
|
|
1778
|
+
return $paragraphChildCaretContaining(caret, paragraph)
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function $paragraphChildCaretContaining(caret, paragraph) {
|
|
1783
|
+
let cursor = caret.getSiblingCaret();
|
|
1784
|
+
while (cursor && !cursor.origin.getParent()?.is(paragraph)) {
|
|
1785
|
+
cursor = cursor.getParentCaret();
|
|
1786
|
+
}
|
|
1787
|
+
return cursor?.origin.getParent()?.is(paragraph) ? cursor : null
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// Only succeeds when the cursor is flush against the inward edge of every
|
|
1791
|
+
// ancestor between itself and the paragraph child.
|
|
1792
|
+
function $paragraphChildCaretAtInwardEdge(caret, paragraph) {
|
|
1793
|
+
let cursor = caret.getSiblingCaret();
|
|
1794
|
+
while (cursor && !cursor.origin.getParent()?.is(paragraph)) {
|
|
1795
|
+
if (cursor.getNodeAtCaret()) return null
|
|
1796
|
+
cursor = cursor.getParentCaret();
|
|
1797
|
+
}
|
|
1798
|
+
return cursor?.origin.getParent()?.is(paragraph) ? cursor : null
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function $splitAroundLineBreak(lineBreakCaret) {
|
|
1802
|
+
let outer = null;
|
|
1803
|
+
|
|
1804
|
+
if (lineBreakCaret.getNodeAtCaret() === null) {
|
|
1805
|
+
lineBreakCaret.origin.remove();
|
|
1806
|
+
} else {
|
|
1807
|
+
const lineBreak = lineBreakCaret.origin;
|
|
1808
|
+
const splitCaret = $getCaretInDirection($rewindSiblingCaret(lineBreakCaret), "next");
|
|
1809
|
+
|
|
1810
|
+
$splitAtPointCaretNext(splitCaret);
|
|
1811
|
+
outer = lineBreak.getTopLevelElement();
|
|
1812
|
+
lineBreak.remove();
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
return outer
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1675
1818
|
// Payload: Record<nodeKey, { patch?, replace? }>
|
|
1676
1819
|
// - patch: plain object, shallow-merged into the existing node's properties
|
|
1677
1820
|
// - replace: a LexicalNode instance that replaces the node
|
|
@@ -3548,12 +3691,18 @@ class CommandDispatcher {
|
|
|
3548
3691
|
#registerDragAndDropHandlers() {
|
|
3549
3692
|
if (this.editorElement.supportsAttachments) {
|
|
3550
3693
|
this.dragCounter = 0;
|
|
3551
|
-
const root = this.editor.getRootElement();
|
|
3552
3694
|
this.#listeners.track(
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3695
|
+
this.editor.registerRootListener((rootElement) => {
|
|
3696
|
+
if (rootElement) {
|
|
3697
|
+
const teardowns = [
|
|
3698
|
+
registerEventListener(rootElement, "dragover", this.#handleDragOver.bind(this)),
|
|
3699
|
+
registerEventListener(rootElement, "drop", this.#handleDrop.bind(this)),
|
|
3700
|
+
registerEventListener(rootElement, "dragenter", this.#handleDragEnter.bind(this)),
|
|
3701
|
+
registerEventListener(rootElement, "dragleave", this.#handleDragLeave.bind(this))
|
|
3702
|
+
];
|
|
3703
|
+
return () => teardowns.forEach((teardown) => teardown())
|
|
3704
|
+
}
|
|
3705
|
+
})
|
|
3557
3706
|
);
|
|
3558
3707
|
}
|
|
3559
3708
|
}
|
|
@@ -3723,32 +3872,6 @@ class Selection {
|
|
|
3723
3872
|
return { node: null, offset: 0 }
|
|
3724
3873
|
}
|
|
3725
3874
|
|
|
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
3875
|
getFormat() {
|
|
3753
3876
|
const selection = $getSelection();
|
|
3754
3877
|
if (!$isRangeSelection(selection)) return {}
|
|
@@ -4013,46 +4136,59 @@ class Selection {
|
|
|
4013
4136
|
return $isDecoratorNode(targetNode) && this.#selectInLexical(targetNode)
|
|
4014
4137
|
}, COMMAND_PRIORITY_LOW));
|
|
4015
4138
|
|
|
4016
|
-
const rootElement = this.editor.getRootElement();
|
|
4017
4139
|
this.#listeners.track(
|
|
4018
|
-
|
|
4140
|
+
this.editor.registerRootListener((rootElement) => {
|
|
4141
|
+
if (rootElement) {
|
|
4142
|
+
return registerEventListener(rootElement, "lexxy:internal:move-to-next-line", () => this.#selectOrAppendNextLine())
|
|
4143
|
+
}
|
|
4144
|
+
})
|
|
4019
4145
|
);
|
|
4020
4146
|
}
|
|
4021
4147
|
|
|
4022
4148
|
#containEditorFocus() {
|
|
4023
4149
|
// Workaround for a bizarre Chrome bug where the cursor abandons the editor to focus on not-focusable elements
|
|
4024
4150
|
// 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
|
-
|
|
4151
|
+
this.#listeners.track(
|
|
4152
|
+
this.editor.registerRootListener((rootElement) => {
|
|
4153
|
+
if (rootElement) {
|
|
4154
|
+
const handler = (event) => this.#handleArrowKeyOnLexicalCursor(event);
|
|
4155
|
+
rootElement.addEventListener("keydown", handler, true);
|
|
4156
|
+
return () => rootElement.removeEventListener("keydown", handler, true)
|
|
4157
|
+
}
|
|
4158
|
+
})
|
|
4159
|
+
);
|
|
4160
|
+
}
|
|
4034
4161
|
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4162
|
+
#handleArrowKeyOnLexicalCursor(event) {
|
|
4163
|
+
if (event.key === "ArrowUp") {
|
|
4164
|
+
const lexicalCursor = this.editor.getRootElement().querySelector("[data-lexical-cursor]");
|
|
4165
|
+
|
|
4166
|
+
if (lexicalCursor) {
|
|
4167
|
+
let currentElement = lexicalCursor.previousElementSibling;
|
|
4168
|
+
while (currentElement && currentElement.hasAttribute("data-lexical-cursor")) {
|
|
4169
|
+
currentElement = currentElement.previousElementSibling;
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
if (!currentElement) {
|
|
4173
|
+
event.preventDefault();
|
|
4038
4174
|
}
|
|
4039
4175
|
}
|
|
4176
|
+
}
|
|
4040
4177
|
|
|
4041
|
-
|
|
4042
|
-
|
|
4178
|
+
if (event.key === "ArrowDown") {
|
|
4179
|
+
const lexicalCursor = this.editor.getRootElement().querySelector("[data-lexical-cursor]");
|
|
4043
4180
|
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4181
|
+
if (lexicalCursor) {
|
|
4182
|
+
let currentElement = lexicalCursor.nextElementSibling;
|
|
4183
|
+
while (currentElement && currentElement.hasAttribute("data-lexical-cursor")) {
|
|
4184
|
+
currentElement = currentElement.nextElementSibling;
|
|
4185
|
+
}
|
|
4049
4186
|
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
}
|
|
4187
|
+
if (!currentElement) {
|
|
4188
|
+
event.preventDefault();
|
|
4053
4189
|
}
|
|
4054
4190
|
}
|
|
4055
|
-
}
|
|
4191
|
+
}
|
|
4056
4192
|
}
|
|
4057
4193
|
|
|
4058
4194
|
#syncSelectedClasses() {
|
|
@@ -5263,6 +5399,7 @@ class Contents {
|
|
|
5263
5399
|
const selection = $getSelection();
|
|
5264
5400
|
if (!$isRangeSelection(selection)) return
|
|
5265
5401
|
|
|
5402
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5266
5403
|
$setBlocksType(selection, () => $createParagraphNode());
|
|
5267
5404
|
}
|
|
5268
5405
|
|
|
@@ -5270,6 +5407,7 @@ class Contents {
|
|
|
5270
5407
|
const selection = $getSelection();
|
|
5271
5408
|
if (!$isRangeSelection(selection)) return
|
|
5272
5409
|
|
|
5410
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5273
5411
|
$setBlocksType(selection, () => $createHeadingNode(tag));
|
|
5274
5412
|
}
|
|
5275
5413
|
|
|
@@ -5311,10 +5449,14 @@ class Contents {
|
|
|
5311
5449
|
if (allCode) {
|
|
5312
5450
|
blockElements.forEach(node => this.#unwrapCodeBlock(node));
|
|
5313
5451
|
} else {
|
|
5452
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5453
|
+
const elements = this.#blockLevelElementsInSelection(selection);
|
|
5454
|
+
if (elements.length === 0) return
|
|
5455
|
+
|
|
5314
5456
|
const codeNode = $createCodeNode("plain");
|
|
5315
|
-
|
|
5457
|
+
elements.at(-1).insertAfter(codeNode);
|
|
5316
5458
|
codeNode.selectEnd();
|
|
5317
|
-
this.insertAtCursor(...
|
|
5459
|
+
this.insertAtCursor(...elements);
|
|
5318
5460
|
}
|
|
5319
5461
|
}
|
|
5320
5462
|
|
|
@@ -5333,8 +5475,7 @@ class Contents {
|
|
|
5333
5475
|
} else {
|
|
5334
5476
|
topLevelElements.filter($isQuoteNode).forEach(node => this.#unwrap(node));
|
|
5335
5477
|
|
|
5336
|
-
|
|
5337
|
-
|
|
5478
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5338
5479
|
const elements = this.#topLevelElementsInSelection(selection);
|
|
5339
5480
|
if (elements.length === 0) return
|
|
5340
5481
|
|
|
@@ -5602,45 +5743,8 @@ class Contents {
|
|
|
5602
5743
|
const selection = $getSelection();
|
|
5603
5744
|
if (!$isRangeSelection(selection)) return
|
|
5604
5745
|
|
|
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
|
-
}
|
|
5746
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5747
|
+
$splitSelectedParagraphsAtInnerLineBreaks(selection);
|
|
5644
5748
|
}
|
|
5645
5749
|
|
|
5646
5750
|
#blockLevelElementsInSelection(selection) {
|
|
@@ -5905,8 +6009,12 @@ class Clipboard {
|
|
|
5905
6009
|
|
|
5906
6010
|
#isOnlyURLPasted(clipboardData) {
|
|
5907
6011
|
// Safari URLs are copied as a text/plain + text/uri-list object
|
|
6012
|
+
// App ShareSheet URLs are copied as solo text/uri-list object
|
|
5908
6013
|
const types = Array.from(clipboardData.types);
|
|
5909
|
-
return types.length
|
|
6014
|
+
return types.length
|
|
6015
|
+
&& types.length <= 2
|
|
6016
|
+
&& types.includes("text/uri-list")
|
|
6017
|
+
&& (types.length < 2 || types.includes("text/plain"))
|
|
5910
6018
|
}
|
|
5911
6019
|
|
|
5912
6020
|
#isPastingIntoCodeBlock() {
|
|
@@ -6964,7 +7072,11 @@ class AttachmentDragAndDrop {
|
|
|
6964
7072
|
const ATTACHMENT_ATTRIBUTES = [ "alt", "caption", "content", "content-type", "data-direct-upload-id",
|
|
6965
7073
|
"data-sgid", "filename", "filesize", "height", "presentation", "previewable", "sgid", "url", "width" ];
|
|
6966
7074
|
|
|
7075
|
+
const UPLOADS_BUSY_MESSAGE = "Please wait for all files to upload";
|
|
7076
|
+
|
|
6967
7077
|
class AttachmentsExtension extends LexxyExtension {
|
|
7078
|
+
#uploadsCount = 0
|
|
7079
|
+
|
|
6968
7080
|
get enabled() {
|
|
6969
7081
|
return this.editorElement.supportsAttachments
|
|
6970
7082
|
}
|
|
@@ -6981,17 +7093,41 @@ class AttachmentsExtension extends LexxyExtension {
|
|
|
6981
7093
|
ActionTextAttachmentUploadNode,
|
|
6982
7094
|
ImageGalleryNode
|
|
6983
7095
|
],
|
|
6984
|
-
register(editor) {
|
|
7096
|
+
register: (editor) => {
|
|
6985
7097
|
const dragAndDrop = new AttachmentDragAndDrop(editor);
|
|
6986
7098
|
|
|
6987
7099
|
return mergeRegister(
|
|
6988
7100
|
editor.registerNodeTransform(ActionTextAttachmentNode, $extractAttachmentFromParagraph),
|
|
6989
7101
|
editor.registerCommand(DELETE_CHARACTER_COMMAND, $collapseIntoGallery, COMMAND_PRIORITY_NORMAL),
|
|
7102
|
+
editor.registerMutationListener(ActionTextAttachmentUploadNode, this.#handleUploadMutations.bind(this)),
|
|
6990
7103
|
() => dragAndDrop.destroy()
|
|
6991
7104
|
)
|
|
6992
7105
|
}
|
|
6993
7106
|
})
|
|
6994
7107
|
}
|
|
7108
|
+
|
|
7109
|
+
#handleUploadMutations(mutations) {
|
|
7110
|
+
const previousUploadsCount = this.#uploadsCount;
|
|
7111
|
+
for (const [ , mutation ] of mutations) {
|
|
7112
|
+
if (mutation === "created") {
|
|
7113
|
+
this.#uploadsCount++;
|
|
7114
|
+
} else if (mutation === "destroyed") {
|
|
7115
|
+
this.#uploadsCount--;
|
|
7116
|
+
}
|
|
7117
|
+
}
|
|
7118
|
+
|
|
7119
|
+
if (this.#uploadsCount !== previousUploadsCount) {
|
|
7120
|
+
this.#setUploadsValidity();
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
|
|
7124
|
+
#setUploadsValidity() {
|
|
7125
|
+
if (this.#uploadsCount) {
|
|
7126
|
+
this.setEditorValidity({ customError: true }, UPLOADS_BUSY_MESSAGE);
|
|
7127
|
+
} else {
|
|
7128
|
+
this.setEditorValidity({});
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
6995
7131
|
}
|
|
6996
7132
|
|
|
6997
7133
|
// Decorator nodes can be wrapped in a Paragraph Node by Lexical when contained in a <div>
|
|
@@ -7399,12 +7535,16 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7399
7535
|
static observedAttributes = [ "connected", "required" ]
|
|
7400
7536
|
|
|
7401
7537
|
#initialValue = ""
|
|
7402
|
-
#
|
|
7403
|
-
#
|
|
7538
|
+
#initializeEventDispatched = false
|
|
7539
|
+
#editorInitializedDispatched = false
|
|
7540
|
+
#valueLoaded = false
|
|
7404
7541
|
#listeners = new ListenerBin()
|
|
7405
7542
|
#disposables = []
|
|
7406
7543
|
#historyState = { undo: false, redo: false }
|
|
7407
7544
|
|
|
7545
|
+
#validity = new Map()
|
|
7546
|
+
#validationTextArea = document.createElement("textarea")
|
|
7547
|
+
|
|
7408
7548
|
constructor() {
|
|
7409
7549
|
super();
|
|
7410
7550
|
this.internals = this.attachInternals();
|
|
@@ -7437,29 +7577,40 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7437
7577
|
|
|
7438
7578
|
this.#initialize();
|
|
7439
7579
|
|
|
7440
|
-
this.#scheduleEditorInitializedDispatch();
|
|
7441
7580
|
this.toggleAttribute("connected", true);
|
|
7442
7581
|
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7582
|
+
requestAnimationFrame(() => {
|
|
7583
|
+
this.#mountRoot();
|
|
7584
|
+
this.#handleAutofocus();
|
|
7585
|
+
this.#dispatchInitialize();
|
|
7586
|
+
});
|
|
7446
7587
|
}
|
|
7447
7588
|
|
|
7448
7589
|
disconnectedCallback() {
|
|
7449
|
-
this.#
|
|
7450
|
-
this
|
|
7590
|
+
this.#initializeEventDispatched = false;
|
|
7591
|
+
this.#editorInitializedDispatched = false;
|
|
7592
|
+
if (this.#valueLoaded) {
|
|
7593
|
+
this.valueBeforeDisconnect = this.value;
|
|
7594
|
+
} else {
|
|
7595
|
+
this.valueBeforeDisconnect = null;
|
|
7596
|
+
}
|
|
7597
|
+
this.#valueLoaded = false;
|
|
7451
7598
|
this.#reset(); // Prevent hangs with Safari when morphing
|
|
7452
7599
|
}
|
|
7453
7600
|
|
|
7454
7601
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
7455
|
-
if (name === "connected"
|
|
7602
|
+
if (name === "connected") this.connectedChangedCallback(oldValue, newValue);
|
|
7603
|
+
if (name === "required") this.requiredChangedCallback(oldValue, newValue);
|
|
7604
|
+
}
|
|
7605
|
+
|
|
7606
|
+
connectedChangedCallback(oldValue, newValue) {
|
|
7607
|
+
if (this.isConnected && oldValue != null && oldValue !== newValue) {
|
|
7456
7608
|
requestAnimationFrame(() => this.#reconnect());
|
|
7457
7609
|
}
|
|
7610
|
+
}
|
|
7458
7611
|
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
this.#setValidity();
|
|
7462
|
-
}
|
|
7612
|
+
requiredChangedCallback() {
|
|
7613
|
+
if (this.isConnected) this.#requestValidityRefresh();
|
|
7463
7614
|
}
|
|
7464
7615
|
|
|
7465
7616
|
formResetCallback() {
|
|
@@ -7485,6 +7636,27 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7485
7636
|
return this.getAttribute("name")
|
|
7486
7637
|
}
|
|
7487
7638
|
|
|
7639
|
+
get required() {
|
|
7640
|
+
return this.hasAttribute("required")
|
|
7641
|
+
}
|
|
7642
|
+
|
|
7643
|
+
get validity() {
|
|
7644
|
+
return this.internals.validity
|
|
7645
|
+
}
|
|
7646
|
+
|
|
7647
|
+
checkValidity() {
|
|
7648
|
+
return this.internals.checkValidity()
|
|
7649
|
+
}
|
|
7650
|
+
|
|
7651
|
+
reportValidity() {
|
|
7652
|
+
return this.internals.reportValidity()
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
setElementValidity(key, flags, message) {
|
|
7656
|
+
this.#validity.set(key, { flags, message });
|
|
7657
|
+
this.#requestValidityRefresh();
|
|
7658
|
+
}
|
|
7659
|
+
|
|
7488
7660
|
get toolbarElement() {
|
|
7489
7661
|
if (!this.#hasToolbar) return null
|
|
7490
7662
|
|
|
@@ -7580,7 +7752,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7580
7752
|
|
|
7581
7753
|
if (!this.editor) return
|
|
7582
7754
|
|
|
7583
|
-
this.#
|
|
7755
|
+
this.#editorInitializedDispatched = true;
|
|
7584
7756
|
this.#dispatchEditorInitialized();
|
|
7585
7757
|
this.#dispatchAttributesChange();
|
|
7586
7758
|
}
|
|
@@ -7635,6 +7807,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7635
7807
|
}
|
|
7636
7808
|
|
|
7637
7809
|
set value(html) {
|
|
7810
|
+
this.#valueLoaded = true;
|
|
7638
7811
|
const editorHasFocus = this.#isContentFocused;
|
|
7639
7812
|
|
|
7640
7813
|
this.editor.update(() => {
|
|
@@ -7731,11 +7904,17 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7731
7904
|
...this.extensions.lexicalExtensions
|
|
7732
7905
|
);
|
|
7733
7906
|
|
|
7734
|
-
editor.setRootElement(this.editorContentElement);
|
|
7735
|
-
|
|
7736
7907
|
return editor
|
|
7737
7908
|
}
|
|
7738
7909
|
|
|
7910
|
+
// Toggling editable around setRootElement skips Lexical's DOM-selection sync,
|
|
7911
|
+
// which would otherwise steal focus from elsewhere on the page.
|
|
7912
|
+
#mountRoot() {
|
|
7913
|
+
this.editor.setEditable(false);
|
|
7914
|
+
this.editor.setRootElement(this.editorContentElement);
|
|
7915
|
+
this.editor.setEditable(true);
|
|
7916
|
+
}
|
|
7917
|
+
|
|
7739
7918
|
get #lexicalNodes() {
|
|
7740
7919
|
const nodes = [ CustomActionTextAttachmentNode ];
|
|
7741
7920
|
|
|
@@ -7792,7 +7971,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7792
7971
|
|
|
7793
7972
|
this.internals.setFormValue(html);
|
|
7794
7973
|
this._internalFormValue = html;
|
|
7795
|
-
this.#validationTextArea.value = this.isEmpty ? "" : html;
|
|
7796
7974
|
|
|
7797
7975
|
if (changed) {
|
|
7798
7976
|
dispatch(this, "lexxy:change");
|
|
@@ -7804,10 +7982,12 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7804
7982
|
}
|
|
7805
7983
|
|
|
7806
7984
|
#loadInitialValue() {
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
this.
|
|
7810
|
-
|
|
7985
|
+
if (!this.#valueLoaded) {
|
|
7986
|
+
const initialHtml = this.valueBeforeDisconnect || this.getAttribute("value") || "<p><br></p>";
|
|
7987
|
+
this.editor.update(() => {
|
|
7988
|
+
this.value = this.#initialValue = initialHtml;
|
|
7989
|
+
}, { tag: HISTORY_MERGE_TAG });
|
|
7990
|
+
}
|
|
7811
7991
|
}
|
|
7812
7992
|
|
|
7813
7993
|
#resetBeforeTurboCaches() {
|
|
@@ -7827,11 +8007,50 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7827
8007
|
this.#clearCachedValues();
|
|
7828
8008
|
this.#internalFormValue = this.value;
|
|
7829
8009
|
this.#toggleEmptyStatus();
|
|
7830
|
-
this.#
|
|
8010
|
+
this.#requestValidityRefresh();
|
|
7831
8011
|
this.#dispatchAttributesChange();
|
|
7832
8012
|
}));
|
|
7833
8013
|
}
|
|
7834
8014
|
|
|
8015
|
+
async #requestValidityRefresh() {
|
|
8016
|
+
await nextFrame();
|
|
8017
|
+
|
|
8018
|
+
if (this.isConnected) this.#refreshValidity();
|
|
8019
|
+
}
|
|
8020
|
+
|
|
8021
|
+
#refreshValidity() {
|
|
8022
|
+
this.#refreshInternalValidity();
|
|
8023
|
+
const { validity, message } = this.#calculateValidity();
|
|
8024
|
+
this.internals.setValidity(validity, message, this.editorContentElement);
|
|
8025
|
+
}
|
|
8026
|
+
|
|
8027
|
+
#refreshInternalValidity() {
|
|
8028
|
+
this.#validationTextArea.required = this.required && this.isBlank;
|
|
8029
|
+
const flags = this.#validationTextArea.validity;
|
|
8030
|
+
const message = this.#validationTextArea.validationMessage;
|
|
8031
|
+
|
|
8032
|
+
this.#validity.set(this, { flags, message });
|
|
8033
|
+
}
|
|
8034
|
+
|
|
8035
|
+
#calculateValidity() {
|
|
8036
|
+
const validity = {};
|
|
8037
|
+
const messages = [];
|
|
8038
|
+
|
|
8039
|
+
for (const { flags, message } of this.#validity.values()) {
|
|
8040
|
+
// internal TextArea's ValidityState can contain `valid: true`
|
|
8041
|
+
if (flags.valid === true) continue
|
|
8042
|
+
|
|
8043
|
+
for (const flag in flags) {
|
|
8044
|
+
if (flags[flag]) {
|
|
8045
|
+
validity[flag] = true;
|
|
8046
|
+
messages.push(message);
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
|
|
8051
|
+
return { validity, message: messages.join("\n") }
|
|
8052
|
+
}
|
|
8053
|
+
|
|
7835
8054
|
#clearCachedValues() {
|
|
7836
8055
|
this.cachedValue = null;
|
|
7837
8056
|
this.cachedStringValue = null;
|
|
@@ -7987,14 +8206,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7987
8206
|
this.classList.toggle("lexxy-editor--empty", this.isEmpty);
|
|
7988
8207
|
}
|
|
7989
8208
|
|
|
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
8209
|
#configureSanitizer() {
|
|
7999
8210
|
setSanitizerConfig(this.#allowedElements);
|
|
8000
8211
|
}
|
|
@@ -8058,22 +8269,18 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8058
8269
|
});
|
|
8059
8270
|
}
|
|
8060
8271
|
|
|
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
|
|
8272
|
+
#dispatchInitialize() {
|
|
8273
|
+
if (this.isConnected && this.adapter) {
|
|
8274
|
+
if (!this.#initializeEventDispatched) {
|
|
8275
|
+
this.#initializeEventDispatched = true;
|
|
8276
|
+
dispatch(this, "lexxy:initialize");
|
|
8277
|
+
}
|
|
8074
8278
|
|
|
8075
|
-
|
|
8076
|
-
|
|
8279
|
+
if (!this.#editorInitializedDispatched) {
|
|
8280
|
+
this.#editorInitializedDispatched = true;
|
|
8281
|
+
this.#dispatchEditorInitialized();
|
|
8282
|
+
}
|
|
8283
|
+
}
|
|
8077
8284
|
}
|
|
8078
8285
|
|
|
8079
8286
|
get #resolvedHighlightColors() {
|
|
@@ -8124,8 +8331,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8124
8331
|
}
|
|
8125
8332
|
|
|
8126
8333
|
#reset() {
|
|
8127
|
-
this.#cancelEditorInitializedDispatch();
|
|
8128
8334
|
this.#dispose();
|
|
8335
|
+
this.#resetValidity();
|
|
8129
8336
|
this.editorContentElement?.remove();
|
|
8130
8337
|
this.editorContentElement = null;
|
|
8131
8338
|
|
|
@@ -8145,6 +8352,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8145
8352
|
this.valueBeforeDisconnect = null;
|
|
8146
8353
|
this.connectedCallback();
|
|
8147
8354
|
}
|
|
8355
|
+
|
|
8356
|
+
#resetValidity() {
|
|
8357
|
+
this.#validity = new Map();
|
|
8358
|
+
}
|
|
8148
8359
|
}
|
|
8149
8360
|
|
|
8150
8361
|
// Like $getRoot().getTextContent() but uses readable text for custom attachment nodes
|
|
@@ -8532,6 +8743,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8532
8743
|
|
|
8533
8744
|
if (this.#doesSpaceSelect) {
|
|
8534
8745
|
this.#popoverListeners.track(this.#editor.registerCommand(KEY_SPACE_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_CRITICAL));
|
|
8746
|
+
this.#popoverListeners.track(this.#editor.registerCommand(INPUT_COMMAND, this.#handleInputCommand.bind(this), COMMAND_PRIORITY_CRITICAL));
|
|
8535
8747
|
}
|
|
8536
8748
|
|
|
8537
8749
|
// Register arrow keys with CRITICAL priority to prevent Lexical's selection handlers from running
|
|
@@ -8565,16 +8777,12 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8565
8777
|
return Array.from(this.popoverElement.querySelectorAll(".lexxy-prompt-menu__item"))
|
|
8566
8778
|
}
|
|
8567
8779
|
|
|
8568
|
-
#selectOption(listItem) {
|
|
8780
|
+
#selectOption(listItem, { scrollIntoView = false } = {}) {
|
|
8569
8781
|
this.#clearListItemSelection();
|
|
8570
8782
|
listItem.toggleAttribute("aria-selected", true);
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
// Preserve selection to prevent cursor jump
|
|
8575
|
-
this.#selection.preservingSelection(() => {
|
|
8576
|
-
this.#editorElement.focus();
|
|
8577
|
-
});
|
|
8783
|
+
if (scrollIntoView) {
|
|
8784
|
+
listItem.scrollIntoView({ block: "nearest", container: "nearest", behavior: "smooth" });
|
|
8785
|
+
}
|
|
8578
8786
|
|
|
8579
8787
|
this.#setEditorAssociationAttribute("aria-controls", this.popoverElement.id);
|
|
8580
8788
|
this.#setEditorAssociationAttribute("aria-activedescendant", listItem.id);
|
|
@@ -8718,17 +8926,22 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8718
8926
|
}
|
|
8719
8927
|
});
|
|
8720
8928
|
}
|
|
8721
|
-
// Arrow keys are
|
|
8929
|
+
// Arrow keys are handled via Lexical commands
|
|
8930
|
+
}
|
|
8931
|
+
|
|
8932
|
+
// Android Mobile keyboard doesn't trigger KEY_SPACE_COMMAND
|
|
8933
|
+
#handleInputCommand(event) {
|
|
8934
|
+
if (event.inputType === "insertText" && event.data === " ") return this.#handleSelectedOption(event)
|
|
8722
8935
|
}
|
|
8723
8936
|
|
|
8724
8937
|
#moveSelectionDown() {
|
|
8725
8938
|
const nextIndex = this.#selectedIndex + 1;
|
|
8726
|
-
if (nextIndex < this.#listItemElements.length) this.#selectOption(this.#listItemElements[nextIndex]);
|
|
8939
|
+
if (nextIndex < this.#listItemElements.length) this.#selectOption(this.#listItemElements[nextIndex], { scrollIntoView: true });
|
|
8727
8940
|
}
|
|
8728
8941
|
|
|
8729
8942
|
#moveSelectionUp() {
|
|
8730
8943
|
const previousIndex = this.#selectedIndex - 1;
|
|
8731
|
-
if (previousIndex >= 0) this.#selectOption(this.#listItemElements[previousIndex]);
|
|
8944
|
+
if (previousIndex >= 0) this.#selectOption(this.#listItemElements[previousIndex], { scrollIntoView: true });
|
|
8732
8945
|
}
|
|
8733
8946
|
|
|
8734
8947
|
get #selectedIndex() {
|
|
@@ -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) {
|