@37signals/lexxy 0.9.19-alpha.1 → 0.9.19-alpha.3
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 +504 -173
- package/package.json +4 -3
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export { highlightCode, highlightElement } 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, $getRoot, $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, 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,
|
|
4
|
+
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createTextNode, $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isTextNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $normalizeSelection__EXPERIMENTAL, $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, INSERT_LINE_BREAK_COMMAND, COMMAND_PRIORITY_HIGH, INSERT_PARAGRAPH_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $isNodeSelection, 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, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, PASTE_COMMAND, $onUpdate, ParagraphNode, RootNode, DRAGSTART_COMMAND, DROP_COMMAND, mergeRegister as mergeRegister$1, CLEAR_HISTORY_COMMAND, 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
|
-
import { ListNode, ListItemNode, $getListDepth, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $
|
|
7
|
+
import { ListNode, ListItemNode, $getListDepth, $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $createListNode, $createListItemNode, registerList } from '@lexical/list';
|
|
8
8
|
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, $descendantsMatching, mergeRegister, $insertNodeToNearestRoot, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, IS_APPLE } from '@lexical/utils';
|
|
9
9
|
import { registerPlainText } from '@lexical/plain-text';
|
|
10
|
-
import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode,
|
|
10
|
+
import { RichTextExtension, $isQuoteNode, QuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
11
11
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
12
12
|
import { HistoryExtension } from '@lexical/history';
|
|
13
13
|
import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, normalizeCodeLang, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
@@ -125,6 +125,14 @@ class ListenerBin {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function handlingDefault(handler) {
|
|
129
|
+
return event => {
|
|
130
|
+
const handled = handler(event);
|
|
131
|
+
if (handled) event.preventDefault();
|
|
132
|
+
return handled
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
128
136
|
function createElement(name, properties, content = "") {
|
|
129
137
|
const element = document.createElement(name);
|
|
130
138
|
for (const [ key, value ] of Object.entries(properties || {})) {
|
|
@@ -1534,8 +1542,12 @@ function $isSafeForRoot(node) {
|
|
|
1534
1542
|
function $makeSafeForRoot(node) {
|
|
1535
1543
|
if ($isSafeForRoot(node)) {
|
|
1536
1544
|
return node
|
|
1537
|
-
} else {
|
|
1545
|
+
} else if (node.getParent()) {
|
|
1538
1546
|
return $wrapNodeInElement(node, () => node.createParentElementNode())
|
|
1547
|
+
} else {
|
|
1548
|
+
// Detached nodes (e.g. clipboard nodes being inserted) can't be `replace`d in place,
|
|
1549
|
+
// so append them into a fresh required parent instead.
|
|
1550
|
+
return node.createParentElementNode().append(node)
|
|
1539
1551
|
}
|
|
1540
1552
|
}
|
|
1541
1553
|
|
|
@@ -1723,7 +1735,7 @@ function $splitSelectedParagraphsAtInnerLineBreaks(selection) {
|
|
|
1723
1735
|
}
|
|
1724
1736
|
}
|
|
1725
1737
|
|
|
1726
|
-
function $expandSelectionToLineBreaksAndSplitAtEdges(selection) {
|
|
1738
|
+
function $expandSelectionToLineBreaksAndSplitAtEdges(selection, fallbackAncestor = (node) => node.getTopLevelElement()) {
|
|
1727
1739
|
$ensureForwardRangeSelection(selection);
|
|
1728
1740
|
|
|
1729
1741
|
const focusCaret = $caretFromPoint(selection.focus, "next");
|
|
@@ -1743,8 +1755,8 @@ function $expandSelectionToLineBreaksAndSplitAtEdges(selection) {
|
|
|
1743
1755
|
const focusOuter = focusBrCaret && $splitAroundLineBreak(focusBrCaret);
|
|
1744
1756
|
const anchorOuter = anchorBrCaret && $splitAroundLineBreak(anchorBrCaret);
|
|
1745
1757
|
|
|
1746
|
-
const innerStart = anchorOuter?.getNextSibling() ?? selection.anchor.getNode()
|
|
1747
|
-
const innerEnd = focusOuter?.getPreviousSibling() ?? selection.focus.getNode()
|
|
1758
|
+
const innerStart = anchorOuter?.getNextSibling() ?? fallbackAncestor(selection.anchor.getNode());
|
|
1759
|
+
const innerEnd = focusOuter?.getPreviousSibling() ?? fallbackAncestor(selection.focus.getNode());
|
|
1748
1760
|
if (!innerStart || !innerEnd) return
|
|
1749
1761
|
|
|
1750
1762
|
$setSelectionFromCaretRange($getCaretRange(
|
|
@@ -1850,6 +1862,49 @@ function $splitAroundLineBreak(lineBreakCaret) {
|
|
|
1850
1862
|
return outer
|
|
1851
1863
|
}
|
|
1852
1864
|
|
|
1865
|
+
// Lexical's RangeSelection.insertNodes/insertLineBreak require every selection point to have a
|
|
1866
|
+
// block ancestor with inline children. An element point on a container of block nodes — e.g. a
|
|
1867
|
+
// quote holding paragraphs — has none, so Lexical throws invariant #211 or #212. This detects
|
|
1868
|
+
// such a point so callers can descend it to a leaf before inserting.
|
|
1869
|
+
function $isPointOnBlockContainer(point) {
|
|
1870
|
+
if (point.type !== "element") return false
|
|
1871
|
+
|
|
1872
|
+
const firstChild = point.getNode().getFirstChild();
|
|
1873
|
+
return ($isElementNode(firstChild) || $isDecoratorNode(firstChild)) && !firstChild.isInline()
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function $hasPointOnBlockContainer(selection) {
|
|
1877
|
+
return $isRangeSelection(selection) &&
|
|
1878
|
+
[ selection.anchor, selection.focus ].some($isPointOnBlockContainer)
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// Descend any block-container element point in the selection to a leaf position, so a subsequent
|
|
1882
|
+
// Lexical insert (insertNodes, insertLineBreak, INSERT_PARAGRAPH) doesn't throw invariant #211/#212.
|
|
1883
|
+
function $normalizeBlockContainerSelection(selection = $getSelection()) {
|
|
1884
|
+
if (!$hasPointOnBlockContainer(selection)) return false
|
|
1885
|
+
|
|
1886
|
+
$normalizeSelection__EXPERIMENTAL(selection);
|
|
1887
|
+
return true
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function $consecutiveSiblingGroups(blocks) {
|
|
1891
|
+
const ordered = [ ...blocks ].sort((a, b) => a.getIndexWithinParent() - b.getIndexWithinParent());
|
|
1892
|
+
const groups = [];
|
|
1893
|
+
|
|
1894
|
+
for (const block of ordered) {
|
|
1895
|
+
const lastGroup = groups.at(-1);
|
|
1896
|
+
const previous = lastGroup?.at(-1);
|
|
1897
|
+
|
|
1898
|
+
if (previous && previous.getParent().is(block.getParent()) && previous.getNextSibling()?.is(block)) {
|
|
1899
|
+
lastGroup.push(block);
|
|
1900
|
+
} else {
|
|
1901
|
+
groups.push([ block ]);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
return groups
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1853
1908
|
// Payload: Record<nodeKey, { patch?, replace? }>
|
|
1854
1909
|
// - patch: plain object, shallow-merged into the existing node's properties
|
|
1855
1910
|
// - replace: a LexicalNode instance that replaces the node
|
|
@@ -2826,6 +2881,30 @@ var theme = {
|
|
|
2826
2881
|
}
|
|
2827
2882
|
};
|
|
2828
2883
|
|
|
2884
|
+
class UploadRequests {
|
|
2885
|
+
#requestsByKey = new Map()
|
|
2886
|
+
|
|
2887
|
+
track(key, request) {
|
|
2888
|
+
this.#requestsByKey.set(key, request);
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
forget(key) {
|
|
2892
|
+
this.#requestsByKey.delete(key);
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
abort(key) {
|
|
2896
|
+
const request = this.#requestsByKey.get(key);
|
|
2897
|
+
if (request) {
|
|
2898
|
+
this.#requestsByKey.delete(key);
|
|
2899
|
+
request.abort();
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
clear() {
|
|
2904
|
+
this.#requestsByKey.clear();
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2829
2908
|
// Shared, strictly-contained element used to attach ephemeral nodes when we
|
|
2830
2909
|
// need to read computed styles (e.g. canonicalizing style values, resolving
|
|
2831
2910
|
// CSS custom properties). The container is created once and attached to
|
|
@@ -3741,6 +3820,16 @@ class CommandDispatcher {
|
|
|
3741
3820
|
#registerKeyboardCommands() {
|
|
3742
3821
|
this.#registerCommandHandler(KEY_ARROW_RIGHT_COMMAND, COMMAND_PRIORITY_NORMAL, this.#handleArrowRightKey.bind(this));
|
|
3743
3822
|
this.#registerCommandHandler(KEY_TAB_COMMAND, COMMAND_PRIORITY_NORMAL, this.#handleTabKey.bind(this));
|
|
3823
|
+
|
|
3824
|
+
// Run before Lexical's built-in insert handlers to descend an element point on a
|
|
3825
|
+
// block container to a leaf, avoiding error #211 on Enter / Shift+Enter in a quote.
|
|
3826
|
+
this.#registerCommandHandler(INSERT_LINE_BREAK_COMMAND, COMMAND_PRIORITY_HIGH, this.#normalizeBlockContainerSelection.bind(this));
|
|
3827
|
+
this.#registerCommandHandler(INSERT_PARAGRAPH_COMMAND, COMMAND_PRIORITY_HIGH, this.#normalizeBlockContainerSelection.bind(this));
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
#normalizeBlockContainerSelection() {
|
|
3831
|
+
$normalizeBlockContainerSelection();
|
|
3832
|
+
return false
|
|
3744
3833
|
}
|
|
3745
3834
|
|
|
3746
3835
|
#handleArrowRightKey(event) {
|
|
@@ -4013,6 +4102,10 @@ class Selection {
|
|
|
4013
4102
|
return this.nearestNodeOfType(ListItemNode)
|
|
4014
4103
|
}
|
|
4015
4104
|
|
|
4105
|
+
get isInsideBlockQuote() {
|
|
4106
|
+
return this.nearestNodeOfType(QuoteNode)
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4016
4109
|
get isIndentedList() {
|
|
4017
4110
|
const closestListNode = this.nearestNodeOfType(ListNode);
|
|
4018
4111
|
return closestListNode && ($getListDepth(closestListNode) > 1)
|
|
@@ -4187,10 +4280,10 @@ class Selection {
|
|
|
4187
4280
|
|
|
4188
4281
|
#processSelectionChangeCommands() {
|
|
4189
4282
|
this.#listeners.track(
|
|
4190
|
-
this.editor.registerCommand(KEY_ARROW_LEFT_COMMAND, this.#selectPreviousNode.bind(this), COMMAND_PRIORITY_LOW),
|
|
4191
|
-
this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, this.#selectNextNode.bind(this), COMMAND_PRIORITY_LOW),
|
|
4192
|
-
this.editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#selectPreviousTopLevelNode.bind(this), COMMAND_PRIORITY_LOW),
|
|
4193
|
-
this.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, this.#selectNextTopLevelNode.bind(this), COMMAND_PRIORITY_LOW),
|
|
4283
|
+
this.editor.registerCommand(KEY_ARROW_LEFT_COMMAND, handlingDefault(this.#selectPreviousNode.bind(this)), COMMAND_PRIORITY_LOW),
|
|
4284
|
+
this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, handlingDefault(this.#selectNextNode.bind(this)), COMMAND_PRIORITY_LOW),
|
|
4285
|
+
this.editor.registerCommand(KEY_ARROW_UP_COMMAND, handlingDefault(this.#selectPreviousTopLevelNode.bind(this)), COMMAND_PRIORITY_LOW),
|
|
4286
|
+
this.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, handlingDefault(this.#selectNextTopLevelNode.bind(this)), COMMAND_PRIORITY_LOW),
|
|
4194
4287
|
|
|
4195
4288
|
this.editor.registerCommand(DELETE_CHARACTER_COMMAND, this.#selectDecoratorNodeBeforeDeletion.bind(this), COMMAND_PRIORITY_LOW),
|
|
4196
4289
|
|
|
@@ -4289,49 +4382,58 @@ class Selection {
|
|
|
4289
4382
|
}
|
|
4290
4383
|
}
|
|
4291
4384
|
|
|
4292
|
-
|
|
4293
|
-
if (event
|
|
4385
|
+
#selectPreviousNode(event) {
|
|
4386
|
+
if (event.shiftKey) {
|
|
4387
|
+
return this.#withCurrentNodeSelectionNode((currentNode) => {
|
|
4388
|
+
const selection = this.#rangeSelectDecorator(currentNode, "forward");
|
|
4294
4389
|
|
|
4295
|
-
|
|
4296
|
-
|
|
4390
|
+
// Can't rely on native pass-through with Playwright on firefox
|
|
4391
|
+
selection.modify("extend", true, "character");
|
|
4392
|
+
return true
|
|
4393
|
+
})
|
|
4297
4394
|
} else {
|
|
4298
|
-
return this.#
|
|
4395
|
+
return this.#withCurrentNodeSelectionNode((currentNode) => currentNode.selectPrevious())
|
|
4396
|
+
|| this.#selectInLexical(this.nodeBeforeCursor)
|
|
4299
4397
|
}
|
|
4300
4398
|
}
|
|
4301
4399
|
|
|
4302
|
-
|
|
4303
|
-
if (event
|
|
4400
|
+
#selectNextNode(event) {
|
|
4401
|
+
if (event.shiftKey) {
|
|
4402
|
+
return this.#withCurrentNodeSelectionNode((currentNode) => {
|
|
4403
|
+
const selection = this.#rangeSelectDecorator(currentNode, "forward");
|
|
4304
4404
|
|
|
4305
|
-
|
|
4306
|
-
|
|
4405
|
+
// Can't rely on native pass-through with Playwright on firefox
|
|
4406
|
+
selection.modify("extend", false, "character");
|
|
4407
|
+
return true
|
|
4408
|
+
})
|
|
4307
4409
|
} else {
|
|
4308
|
-
return this.#
|
|
4410
|
+
return this.#withCurrentNodeSelectionNode((currentNode) => currentNode.selectNext(0, 0))
|
|
4411
|
+
|| this.#selectInLexical(this.nodeAfterCursor)
|
|
4309
4412
|
}
|
|
4310
4413
|
}
|
|
4311
4414
|
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4415
|
+
#selectPreviousTopLevelNode() {
|
|
4416
|
+
return this.#withCurrentNodeSelectionNode((currentNode) => currentNode.getTopLevelElement().selectPrevious())
|
|
4417
|
+
|| this.#selectInLexical(this.topLevelNodeBeforeCursor)
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
#selectNextTopLevelNode() {
|
|
4421
|
+
return this.#withCurrentNodeSelectionNode((currentNode) => currentNode.getTopLevelElement().selectNext(0, 0))
|
|
4422
|
+
|| this.#selectInLexical(this.topLevelNodeAfterCursor)
|
|
4318
4423
|
}
|
|
4319
4424
|
|
|
4320
|
-
|
|
4425
|
+
#withCurrentNodeSelectionNode(fn) {
|
|
4321
4426
|
if (this.hasNodeSelection) {
|
|
4322
|
-
return
|
|
4323
|
-
} else {
|
|
4324
|
-
return this.#selectInLexical(this.topLevelNodeAfterCursor)
|
|
4427
|
+
return fn($getSelection().getNodes()[0])
|
|
4325
4428
|
}
|
|
4326
4429
|
}
|
|
4327
4430
|
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
});
|
|
4431
|
+
#rangeSelectDecorator(node, direction = "forward") {
|
|
4432
|
+
if ($isDecoratorNode(node)) {
|
|
4433
|
+
const [ anchorOffset, focusOffset ] = direction === "forward" ? [ 0, 1 ] : [ 1, 0 ];
|
|
4434
|
+
const indexAtNode = node.getIndexWithinParent();
|
|
4435
|
+
|
|
4436
|
+
return node.getParent().select(indexAtNode + anchorOffset, indexAtNode + focusOffset)
|
|
4335
4437
|
}
|
|
4336
4438
|
}
|
|
4337
4439
|
|
|
@@ -4440,6 +4542,9 @@ class Selection {
|
|
|
4440
4542
|
// - First item (no previous sibling): convert to a paragraph above the
|
|
4441
4543
|
// list, matching the standard "unwrap list formatting" behavior that
|
|
4442
4544
|
// users expect from pressing backspace at the start of a list item.
|
|
4545
|
+
// Inside a blockquote we instead just remove the empty item and move
|
|
4546
|
+
// the cursor into the next one — stranding a paragraph there would
|
|
4547
|
+
// leave the blank line the user is trying to close.
|
|
4443
4548
|
//
|
|
4444
4549
|
// When the empty item is the last/only one in the list, we return false
|
|
4445
4550
|
// and let Lexical's default (convert to paragraph) provide the standard
|
|
@@ -4458,19 +4563,22 @@ class Selection {
|
|
|
4458
4563
|
if (!nextSibling) return false
|
|
4459
4564
|
|
|
4460
4565
|
const previousSibling = listItem.getPreviousSibling();
|
|
4566
|
+
const listNode = $getNearestNodeOfType(listItem, ListNode);
|
|
4567
|
+
if (!listNode) return false
|
|
4568
|
+
|
|
4461
4569
|
if (previousSibling) {
|
|
4462
4570
|
previousSibling.selectEnd();
|
|
4463
4571
|
listItem.remove();
|
|
4464
|
-
|
|
4572
|
+
} else if ($isQuoteNode(listNode.getParent())) {
|
|
4573
|
+
nextSibling.selectStart();
|
|
4574
|
+
listItem.remove();
|
|
4575
|
+
} else {
|
|
4576
|
+
const paragraph = $createParagraphNode();
|
|
4577
|
+
listNode.insertBefore(paragraph);
|
|
4578
|
+
listItem.remove();
|
|
4579
|
+
paragraph.selectStart();
|
|
4465
4580
|
}
|
|
4466
4581
|
|
|
4467
|
-
const listNode = $getNearestNodeOfType(listItem, ListNode);
|
|
4468
|
-
if (!listNode) return false
|
|
4469
|
-
|
|
4470
|
-
const paragraph = $createParagraphNode();
|
|
4471
|
-
listNode.insertBefore(paragraph);
|
|
4472
|
-
listItem.remove();
|
|
4473
|
-
paragraph.selectStart();
|
|
4474
4582
|
return true
|
|
4475
4583
|
}
|
|
4476
4584
|
|
|
@@ -4947,9 +5055,9 @@ function $findOrCreateGalleryForImage(node) {
|
|
|
4947
5055
|
class Uploader {
|
|
4948
5056
|
#files
|
|
4949
5057
|
|
|
4950
|
-
static for(editorElement, files) {
|
|
5058
|
+
static for(editorElement, files, options = {}) {
|
|
4951
5059
|
const UploaderKlass = GalleryUploader.handle(editorElement, files) ? GalleryUploader : Uploader;
|
|
4952
|
-
return new UploaderKlass(editorElement, files)
|
|
5060
|
+
return new UploaderKlass(editorElement, files, options)
|
|
4953
5061
|
}
|
|
4954
5062
|
|
|
4955
5063
|
constructor(editorElement, files, options = {}) {
|
|
@@ -4971,7 +5079,13 @@ class Uploader {
|
|
|
4971
5079
|
}
|
|
4972
5080
|
|
|
4973
5081
|
$createUploadNodes() {
|
|
4974
|
-
this.nodes = this.files.map(file => this
|
|
5082
|
+
this.nodes = this.files.map(file => this.#createUploadNode(file));
|
|
5083
|
+
}
|
|
5084
|
+
|
|
5085
|
+
#createUploadNode(file) {
|
|
5086
|
+
return this.options.pending
|
|
5087
|
+
? this.contents.$createPendingUploadNode(file)
|
|
5088
|
+
: this.contents.$createUploadNode(file)
|
|
4975
5089
|
}
|
|
4976
5090
|
|
|
4977
5091
|
$insertUploadNodes() {
|
|
@@ -5229,6 +5343,8 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
5229
5343
|
this.#dispatchEvent("lexxy:upload-start", { file: this.file });
|
|
5230
5344
|
|
|
5231
5345
|
upload.create((error, blob) => {
|
|
5346
|
+
this.#forgetUploadRequest();
|
|
5347
|
+
|
|
5232
5348
|
if (error) {
|
|
5233
5349
|
this.#dispatchEvent("lexxy:upload-end", { file: this.file, error });
|
|
5234
5350
|
this.#handleUploadError(error);
|
|
@@ -5251,12 +5367,26 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
5251
5367
|
directUploadWillStoreFileWithXHR: (request) => {
|
|
5252
5368
|
if (shouldAuthenticateUploads) request.withCredentials = true;
|
|
5253
5369
|
|
|
5370
|
+
this.#rememberUploadRequest(request);
|
|
5371
|
+
|
|
5254
5372
|
const uploadProgressHandler = (event) => this.#handleUploadProgress(event, request);
|
|
5255
5373
|
request.upload.addEventListener("progress", uploadProgressHandler);
|
|
5256
5374
|
}
|
|
5257
5375
|
}
|
|
5258
5376
|
}
|
|
5259
5377
|
|
|
5378
|
+
#forgetUploadRequest() {
|
|
5379
|
+
this.#editorElement.uploadRequests.forget(this.getKey());
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5382
|
+
#rememberUploadRequest(request) {
|
|
5383
|
+
this.#editorElement.uploadRequests.track(this.getKey(), request);
|
|
5384
|
+
}
|
|
5385
|
+
|
|
5386
|
+
get #editorElement() {
|
|
5387
|
+
return this.editor.getRootElement()?.closest("lexxy-editor")
|
|
5388
|
+
}
|
|
5389
|
+
|
|
5260
5390
|
#setUploadStarted() {
|
|
5261
5391
|
this.#setProgress(1);
|
|
5262
5392
|
}
|
|
@@ -5346,24 +5476,13 @@ function $createActionTextAttachmentUploadNode(...args) {
|
|
|
5346
5476
|
return new ActionTextAttachmentUploadNode(...args)
|
|
5347
5477
|
}
|
|
5348
5478
|
|
|
5349
|
-
class
|
|
5350
|
-
static for(selection) {
|
|
5351
|
-
const INSERTERS = [
|
|
5352
|
-
CodeNodeInserter,
|
|
5353
|
-
ShadowRootNodeInserter,
|
|
5354
|
-
NodeSelectionNodeInserter,
|
|
5355
|
-
BlockContainerNodeInserter
|
|
5356
|
-
];
|
|
5357
|
-
const Inserter = INSERTERS.find(inserter => inserter.handles(selection));
|
|
5358
|
-
return Inserter ? new Inserter(selection) : selection
|
|
5359
|
-
}
|
|
5360
|
-
|
|
5479
|
+
class BaseNodeInserter {
|
|
5361
5480
|
constructor(selection) {
|
|
5362
5481
|
this.selection = selection;
|
|
5363
5482
|
}
|
|
5364
5483
|
}
|
|
5365
5484
|
|
|
5366
|
-
class CodeNodeInserter extends
|
|
5485
|
+
class CodeNodeInserter extends BaseNodeInserter {
|
|
5367
5486
|
static handles(selection) {
|
|
5368
5487
|
return $getNearestNodeOfType(selection.anchor?.getNode(), CodeNode)
|
|
5369
5488
|
}
|
|
@@ -5422,10 +5541,9 @@ class CodeNodeInserter extends NodeInserter {
|
|
|
5422
5541
|
return $createTextNode(node.getTextContent())
|
|
5423
5542
|
}
|
|
5424
5543
|
}
|
|
5425
|
-
|
|
5426
5544
|
}
|
|
5427
5545
|
|
|
5428
|
-
class ShadowRootNodeInserter extends
|
|
5546
|
+
class ShadowRootNodeInserter extends BaseNodeInserter {
|
|
5429
5547
|
static handles(selection) {
|
|
5430
5548
|
return $isShadowRoot(selection?.anchor?.getNode())
|
|
5431
5549
|
}
|
|
@@ -5439,20 +5557,96 @@ class ShadowRootNodeInserter extends NodeInserter {
|
|
|
5439
5557
|
}
|
|
5440
5558
|
}
|
|
5441
5559
|
|
|
5442
|
-
class NodeSelectionNodeInserter extends
|
|
5560
|
+
class NodeSelectionNodeInserter extends BaseNodeInserter {
|
|
5443
5561
|
static handles(selection) {
|
|
5444
5562
|
return $isNodeSelection(selection)
|
|
5445
5563
|
}
|
|
5446
5564
|
|
|
5447
5565
|
insertNodes(nodes) {
|
|
5448
|
-
const selectedNodes = this.selection.getNodes();
|
|
5449
|
-
|
|
5450
5566
|
// Overrides Lexical's default behavior of _removing_ the currently selected nodes
|
|
5451
5567
|
// https://github.com/facebook/lexical/blob/v0.38.2/packages/lexical/src/LexicalSelection.ts#L412
|
|
5452
|
-
let lastNode =
|
|
5568
|
+
let lastNode = this.selection.getNodes().at(-1);
|
|
5569
|
+
|
|
5570
|
+
for (const node of nodes) {
|
|
5571
|
+
// Inserting after a top-level node would make this one a root child. Inline nodes
|
|
5572
|
+
// can't live there (Lexical error #99), so wrap them in their required parent first.
|
|
5573
|
+
const nodeToInsert = this.#insertsIntoRoot(lastNode) ? $makeSafeForRoot(node) : node;
|
|
5574
|
+
lastNode = lastNode.insertAfter(nodeToInsert);
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
|
|
5578
|
+
#insertsIntoRoot(node) {
|
|
5579
|
+
return node.is(node.getTopLevelElement())
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
|
|
5583
|
+
// A list item can only hold inline content, so a block node (such as an image
|
|
5584
|
+
// attachment) dropped into one corrupts the list: Lexical lifts it into the
|
|
5585
|
+
// wrong list item and orphans an empty bullet. Block nodes belong at the top
|
|
5586
|
+
// level instead, splitting the list around the cursor. Inline content keeps
|
|
5587
|
+
// Lexical's default behavior and stays within the list item.
|
|
5588
|
+
class ListItemNodeInserter extends BaseNodeInserter {
|
|
5589
|
+
static handles(selection) {
|
|
5590
|
+
return $isRangeSelection(selection) &&
|
|
5591
|
+
$getNearestNodeOfType(selection.anchor.getNode(), ListItemNode)
|
|
5592
|
+
}
|
|
5593
|
+
|
|
5594
|
+
insertNodes(nodes) {
|
|
5595
|
+
if (nodes.some(node => this.#isBlockDecorator(node))) {
|
|
5596
|
+
this.#insertAroundList(nodes);
|
|
5597
|
+
} else {
|
|
5598
|
+
this.selection.insertNodes(nodes);
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
|
|
5602
|
+
#insertAroundList(nodes) {
|
|
5603
|
+
if (!this.selection.isCollapsed()) { this.selection.removeText(); }
|
|
5604
|
+
|
|
5605
|
+
// Break out of any nesting to the outermost list. Splitting an inner list
|
|
5606
|
+
// would leave the block stranded inside an ancestor list item, which is the
|
|
5607
|
+
// very corruption we are avoiding. The block must land at the list's level.
|
|
5608
|
+
const anchorNode = this.selection.anchor.getNode();
|
|
5609
|
+
const outerList = this.#outermostList(anchorNode);
|
|
5610
|
+
const topItem = this.#topLevelItemFor(anchorNode, outerList);
|
|
5611
|
+
|
|
5612
|
+
// A blank top-level bullet is just the insertion point (e.g. the user pressed
|
|
5613
|
+
// Enter to leave the list); break out of it entirely. A bullet with content —
|
|
5614
|
+
// including one wrapping a nested list — splits so its content stays in the list.
|
|
5615
|
+
const splitAfterItem = $isBlankNode(topItem) ? topItem.getPreviousSibling() : topItem;
|
|
5616
|
+
const splitIndex = splitAfterItem ? splitAfterItem.getIndexWithinParent() + 1 : 0;
|
|
5617
|
+
const [ listBefore, listAfter ] = $splitNode(outerList, splitIndex);
|
|
5618
|
+
if ($isBlankNode(topItem)) { topItem.remove(); }
|
|
5619
|
+
|
|
5620
|
+
let anchor = listBefore ?? listAfter;
|
|
5453
5621
|
for (const node of nodes) {
|
|
5454
|
-
|
|
5622
|
+
anchor.insertAfter(node);
|
|
5623
|
+
anchor = node;
|
|
5455
5624
|
}
|
|
5625
|
+
|
|
5626
|
+
this.#removeEmptyList(listBefore);
|
|
5627
|
+
this.#removeEmptyList(listAfter);
|
|
5628
|
+
nodes.at(-1).selectNext();
|
|
5629
|
+
}
|
|
5630
|
+
|
|
5631
|
+
#outermostList(node) {
|
|
5632
|
+
return [ node, ...node.getParents() ].reverse().find($isListNode)
|
|
5633
|
+
}
|
|
5634
|
+
|
|
5635
|
+
#topLevelItemFor(node, outerList) {
|
|
5636
|
+
return [ node, ...node.getParents() ].find(ancestor =>
|
|
5637
|
+
$isListItemNode(ancestor) && ancestor.getParent()?.is(outerList)
|
|
5638
|
+
)
|
|
5639
|
+
}
|
|
5640
|
+
|
|
5641
|
+
#removeEmptyList(list) {
|
|
5642
|
+
if ($isListNode(list) && list.isEmpty()) list.remove();
|
|
5643
|
+
}
|
|
5644
|
+
|
|
5645
|
+
// Only block decorator nodes (image/file attachments) are intercepted. A list
|
|
5646
|
+
// item cannot hold them, so they must break out. Pasted element blocks
|
|
5647
|
+
// (paragraphs, quotes) keep Lexical's own list-escape semantics.
|
|
5648
|
+
#isBlockDecorator(node) {
|
|
5649
|
+
return $isDecoratorNode(node) && !node.isInline()
|
|
5456
5650
|
}
|
|
5457
5651
|
}
|
|
5458
5652
|
|
|
@@ -5460,10 +5654,9 @@ class NodeSelectionNodeInserter extends NodeInserter {
|
|
|
5460
5654
|
// ancestor with inline children. An element point on a container of block nodes — e.g.
|
|
5461
5655
|
// a quote holding paragraphs — has none, so Lexical throws invariant #211 or #212.
|
|
5462
5656
|
// Descend such points to a leaf position before inserting.
|
|
5463
|
-
class BlockContainerNodeInserter extends
|
|
5657
|
+
class BlockContainerNodeInserter extends BaseNodeInserter {
|
|
5464
5658
|
static handles(selection) {
|
|
5465
|
-
return $
|
|
5466
|
-
[ selection.anchor, selection.focus ].some($isPointOnBlockContainer)
|
|
5659
|
+
return $hasPointOnBlockContainer(selection)
|
|
5467
5660
|
}
|
|
5468
5661
|
|
|
5469
5662
|
insertNodes(nodes) {
|
|
@@ -5472,12 +5665,66 @@ class BlockContainerNodeInserter extends NodeInserter {
|
|
|
5472
5665
|
}
|
|
5473
5666
|
}
|
|
5474
5667
|
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5668
|
+
const INSERTERS = [
|
|
5669
|
+
CodeNodeInserter,
|
|
5670
|
+
ShadowRootNodeInserter,
|
|
5671
|
+
NodeSelectionNodeInserter,
|
|
5672
|
+
ListItemNodeInserter,
|
|
5673
|
+
BlockContainerNodeInserter
|
|
5674
|
+
];
|
|
5675
|
+
|
|
5676
|
+
// Defined here rather than on the base class so the base can stay free of any
|
|
5677
|
+
// dependency on its subclasses (they import the base), avoiding an import cycle.
|
|
5678
|
+
BaseNodeInserter.for = (selection) => {
|
|
5679
|
+
const inserterClass = INSERTERS.find(inserter => inserter.handles(selection));
|
|
5680
|
+
return inserterClass ? new inserterClass(selection) : selection
|
|
5681
|
+
};
|
|
5682
|
+
|
|
5683
|
+
class PastedContentFormatter {
|
|
5684
|
+
constructor(doc) {
|
|
5685
|
+
this.doc = doc;
|
|
5686
|
+
}
|
|
5687
|
+
|
|
5688
|
+
format() {
|
|
5689
|
+
this.#unwrapPlaceholderAnchors();
|
|
5690
|
+
this.#stripTableCellColorStyles();
|
|
5691
|
+
this.#stripStrayListChildren();
|
|
5692
|
+
return this.doc
|
|
5693
|
+
}
|
|
5694
|
+
|
|
5695
|
+
// Anchors with non-meaningful hrefs (e.g. "#", "") appear in content copied
|
|
5696
|
+
// from rendered views where mentions and interactive elements are wrapped in
|
|
5697
|
+
// <a href="#"> tags. Unwrap them so their text content pastes as plain text
|
|
5698
|
+
// and real links are preserved.
|
|
5699
|
+
#unwrapPlaceholderAnchors() {
|
|
5700
|
+
for (const anchor of this.doc.querySelectorAll("a")) {
|
|
5701
|
+
const href = anchor.getAttribute("href") || "";
|
|
5702
|
+
if (href === "" || href === "#") {
|
|
5703
|
+
anchor.replaceWith(...anchor.childNodes);
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
|
|
5708
|
+
// Table cells copied from a page inherit the source theme's inline color
|
|
5709
|
+
// styles (e.g. dark-mode backgrounds). Strip them so pasted tables adopt
|
|
5710
|
+
// the current theme instead of carrying stale colors.
|
|
5711
|
+
#stripTableCellColorStyles() {
|
|
5712
|
+
for (const cell of this.doc.querySelectorAll("td, th")) {
|
|
5713
|
+
cell.style.removeProperty("background-color");
|
|
5714
|
+
cell.style.removeProperty("background");
|
|
5715
|
+
cell.style.removeProperty("color");
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5718
|
+
|
|
5719
|
+
// Only <li> is a valid child of a list; drop stray <br>/whitespace so the
|
|
5720
|
+
// import doesn't wrap them into an empty leading item.
|
|
5721
|
+
#stripStrayListChildren() {
|
|
5722
|
+
for (const list of this.doc.querySelectorAll("ol, ul")) {
|
|
5723
|
+
for (const child of Array.from(list.childNodes)) {
|
|
5724
|
+
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "LI") continue
|
|
5725
|
+
list.removeChild(child);
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5481
5728
|
}
|
|
5482
5729
|
}
|
|
5483
5730
|
|
|
@@ -5512,21 +5759,16 @@ class Contents {
|
|
|
5512
5759
|
|
|
5513
5760
|
insertAtCursor(...nodes) {
|
|
5514
5761
|
const selection = $getSelection() ?? $getRoot().selectEnd();
|
|
5515
|
-
const inserter =
|
|
5762
|
+
const inserter = BaseNodeInserter.for(selection);
|
|
5516
5763
|
|
|
5517
5764
|
inserter.insertNodes(nodes);
|
|
5518
5765
|
}
|
|
5519
5766
|
|
|
5520
|
-
insertAtCursorEnsuringLineBelow(node) {
|
|
5521
|
-
this.insertAtCursor(node);
|
|
5522
|
-
this.#insertLineBelowIfLastNode(node);
|
|
5523
|
-
}
|
|
5524
|
-
|
|
5525
5767
|
applyParagraphFormat() {
|
|
5526
5768
|
const selection = $getSelection();
|
|
5527
5769
|
if (!$isRangeSelection(selection)) return
|
|
5528
5770
|
|
|
5529
|
-
$expandSelectionToLineBreaksAndSplitAtEdges(selection);
|
|
5771
|
+
$expandSelectionToLineBreaksAndSplitAtEdges(selection, (node) => $getNearestBlockElementAncestorOrThrow(node));
|
|
5530
5772
|
$setBlocksType(selection, () => $createParagraphNode());
|
|
5531
5773
|
}
|
|
5532
5774
|
|
|
@@ -5539,13 +5781,11 @@ class Contents {
|
|
|
5539
5781
|
}
|
|
5540
5782
|
|
|
5541
5783
|
applyUnorderedListFormat() {
|
|
5542
|
-
this.#
|
|
5543
|
-
this.editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
5784
|
+
this.#applyListFormat("bullet", INSERT_UNORDERED_LIST_COMMAND);
|
|
5544
5785
|
}
|
|
5545
5786
|
|
|
5546
5787
|
applyOrderedListFormat() {
|
|
5547
|
-
this.#
|
|
5548
|
-
this.editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
5788
|
+
this.#applyListFormat("number", INSERT_ORDERED_LIST_COMMAND);
|
|
5549
5789
|
}
|
|
5550
5790
|
|
|
5551
5791
|
clearFormatting() {
|
|
@@ -5633,7 +5873,7 @@ class Contents {
|
|
|
5633
5873
|
|
|
5634
5874
|
const selection = $getSelection();
|
|
5635
5875
|
if ($isRangeSelection(selection)) {
|
|
5636
|
-
selection.insertNodes([ linkNode ]);
|
|
5876
|
+
BaseNodeInserter.for(selection).insertNodes([ linkNode ]);
|
|
5637
5877
|
linkNodeKey = linkNode.getKey();
|
|
5638
5878
|
}
|
|
5639
5879
|
});
|
|
@@ -5665,17 +5905,27 @@ class Contents {
|
|
|
5665
5905
|
const fullText = anchorNode.getTextContent();
|
|
5666
5906
|
const offset = anchor.offset;
|
|
5667
5907
|
|
|
5668
|
-
const
|
|
5669
|
-
|
|
5670
|
-
const lastIndex = textBeforeCursor.lastIndexOf(string);
|
|
5908
|
+
const lastIndex = fullText.slice(0, offset).lastIndexOf(string);
|
|
5671
5909
|
if (lastIndex !== -1) {
|
|
5672
|
-
result =
|
|
5910
|
+
result = fullText.slice(lastIndex + string.length, this.#endOffsetAt(fullText, offset));
|
|
5673
5911
|
}
|
|
5674
5912
|
});
|
|
5675
5913
|
|
|
5676
5914
|
return result
|
|
5677
5915
|
}
|
|
5678
5916
|
|
|
5917
|
+
// The query runs from the trigger up to the next whitespace, even when the
|
|
5918
|
+
// cursor sits inside an existing word — inserting "@" before "Jack" must
|
|
5919
|
+
// filter by "Jack" rather than treating the prompt as empty.
|
|
5920
|
+
#endOffsetAt(fullText, cursorOffset) {
|
|
5921
|
+
const whitespaceOffset = fullText.slice(cursorOffset).search(/\s/);
|
|
5922
|
+
if (whitespaceOffset === -1) {
|
|
5923
|
+
return fullText.length
|
|
5924
|
+
} else {
|
|
5925
|
+
return cursorOffset + whitespaceOffset
|
|
5926
|
+
}
|
|
5927
|
+
}
|
|
5928
|
+
|
|
5679
5929
|
containsTextBackUntil(string) {
|
|
5680
5930
|
let result = false;
|
|
5681
5931
|
|
|
@@ -5706,10 +5956,10 @@ class Contents {
|
|
|
5706
5956
|
const { anchorNode, offset } = this.#getTextAnchorData();
|
|
5707
5957
|
if (!anchorNode) return
|
|
5708
5958
|
|
|
5709
|
-
const lastIndex = this.#
|
|
5959
|
+
const lastIndex = this.#findReplacementStart(anchorNode, offset, stringToReplace);
|
|
5710
5960
|
if (lastIndex === -1) return
|
|
5711
5961
|
|
|
5712
|
-
this.#performTextReplacement(anchorNode, selection,
|
|
5962
|
+
this.#performTextReplacement(anchorNode, selection, lastIndex, stringToReplace, replacementNodes);
|
|
5713
5963
|
}
|
|
5714
5964
|
|
|
5715
5965
|
uploadFiles(files, { selectLast } = {}) {
|
|
@@ -5742,24 +5992,46 @@ class Contents {
|
|
|
5742
5992
|
})
|
|
5743
5993
|
}
|
|
5744
5994
|
|
|
5995
|
+
$createPendingUploadNode(file) {
|
|
5996
|
+
return $createActionTextAttachmentUploadNode({
|
|
5997
|
+
file,
|
|
5998
|
+
uploadUrl: null,
|
|
5999
|
+
blobUrlTemplate: this.editorElement.blobUrlTemplate,
|
|
6000
|
+
contentType: file.type,
|
|
6001
|
+
})
|
|
6002
|
+
}
|
|
6003
|
+
|
|
5745
6004
|
insertPendingAttachment(file) {
|
|
5746
6005
|
if (!this.editorElement.supportsAttachments) return null
|
|
5747
6006
|
|
|
5748
6007
|
let nodeKey = null;
|
|
5749
6008
|
this.editor.update(() => {
|
|
5750
|
-
const uploadNode =
|
|
5751
|
-
file,
|
|
5752
|
-
uploadUrl: null,
|
|
5753
|
-
blobUrlTemplate: this.editorElement.blobUrlTemplate,
|
|
5754
|
-
editor: this.editor
|
|
5755
|
-
});
|
|
6009
|
+
const uploadNode = this.$createPendingUploadNode(file);
|
|
5756
6010
|
this.insertAtCursor(uploadNode);
|
|
5757
6011
|
nodeKey = uploadNode.getKey();
|
|
5758
|
-
}
|
|
6012
|
+
});
|
|
6013
|
+
|
|
6014
|
+
return nodeKey ? this.#pendingAttachmentHandle(nodeKey) : null
|
|
6015
|
+
}
|
|
6016
|
+
|
|
6017
|
+
insertPendingAttachments(files) {
|
|
6018
|
+
const fileList = Array.from(files);
|
|
6019
|
+
if (!this.editorElement.supportsAttachments || fileList.length === 0) return []
|
|
6020
|
+
|
|
6021
|
+
let nodeKeys = [];
|
|
6022
|
+
this.editor.update(() => {
|
|
6023
|
+
const uploader = Uploader.for(this.editorElement, fileList, { pending: true });
|
|
6024
|
+
uploader.$uploadFiles();
|
|
6025
|
+
nodeKeys = (uploader.nodes ?? []).map(node => node.getKey());
|
|
6026
|
+
});
|
|
5759
6027
|
|
|
5760
|
-
|
|
6028
|
+
return nodeKeys.map(nodeKey => this.#pendingAttachmentHandle(nodeKey))
|
|
6029
|
+
}
|
|
5761
6030
|
|
|
6031
|
+
#pendingAttachmentHandle(initialNodeKey) {
|
|
5762
6032
|
const editor = this.editor;
|
|
6033
|
+
let nodeKey = initialNodeKey;
|
|
6034
|
+
|
|
5763
6035
|
return {
|
|
5764
6036
|
setAttributes(blob) {
|
|
5765
6037
|
editor.update(() => {
|
|
@@ -5828,8 +6100,7 @@ class Contents {
|
|
|
5828
6100
|
}
|
|
5829
6101
|
|
|
5830
6102
|
#formatPastedDOM(doc) {
|
|
5831
|
-
|
|
5832
|
-
this.#stripTableCellColorStyles(doc);
|
|
6103
|
+
new PastedContentFormatter(doc).format();
|
|
5833
6104
|
}
|
|
5834
6105
|
|
|
5835
6106
|
#dispatchPastedNodesCommand(nodes) {
|
|
@@ -5877,6 +6148,45 @@ class Contents {
|
|
|
5877
6148
|
codeNode.remove();
|
|
5878
6149
|
}
|
|
5879
6150
|
|
|
6151
|
+
#applyListFormat(listType, command) {
|
|
6152
|
+
if (this.selection.isInsideBlockQuote) {
|
|
6153
|
+
this.#insertListInsideQuote(listType);
|
|
6154
|
+
} else {
|
|
6155
|
+
this.#splitParagraphsAtLineBreaksUnlessInsideList();
|
|
6156
|
+
this.editor.dispatchCommand(command);
|
|
6157
|
+
}
|
|
6158
|
+
}
|
|
6159
|
+
|
|
6160
|
+
#insertListInsideQuote(listType) {
|
|
6161
|
+
for (const group of $consecutiveSiblingGroups(this.#quotedBlocksInSelection())) {
|
|
6162
|
+
this.#wrapBlocksInList(group, listType);
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
|
|
6166
|
+
#quotedBlocksInSelection() {
|
|
6167
|
+
const selection = $getSelection();
|
|
6168
|
+
if (!$isRangeSelection(selection)) return []
|
|
6169
|
+
|
|
6170
|
+
const blocks = this.#outermostElements(this.#blockLevelElementsInSelection(selection));
|
|
6171
|
+
return blocks.filter((block) => $isQuoteNode(block.getParent()))
|
|
6172
|
+
}
|
|
6173
|
+
|
|
6174
|
+
#wrapBlocksInList(blocks, listType) {
|
|
6175
|
+
const list = $createListNode(listType);
|
|
6176
|
+
blocks[0].insertBefore(list);
|
|
6177
|
+
|
|
6178
|
+
for (const block of blocks) {
|
|
6179
|
+
const listItem = $createListItemNode();
|
|
6180
|
+
if ($isListNode(block)) {
|
|
6181
|
+
listItem.append(...block.getChildren().flatMap((item) => item.getChildren()));
|
|
6182
|
+
} else {
|
|
6183
|
+
listItem.append(...block.getChildren());
|
|
6184
|
+
}
|
|
6185
|
+
list.append(listItem);
|
|
6186
|
+
block.remove();
|
|
6187
|
+
}
|
|
6188
|
+
}
|
|
6189
|
+
|
|
5880
6190
|
#splitParagraphsAtLineBreaksUnlessInsideList() {
|
|
5881
6191
|
if (this.selection.isInsideList) return
|
|
5882
6192
|
|
|
@@ -5926,17 +6236,6 @@ class Contents {
|
|
|
5926
6236
|
}
|
|
5927
6237
|
}
|
|
5928
6238
|
|
|
5929
|
-
#insertLineBelowIfLastNode(node) {
|
|
5930
|
-
this.editor.update(() => {
|
|
5931
|
-
const nextSibling = node.getNextSibling();
|
|
5932
|
-
if (!nextSibling) {
|
|
5933
|
-
const newParagraph = $createParagraphNode();
|
|
5934
|
-
node.insertAfter(newParagraph);
|
|
5935
|
-
newParagraph.selectStart();
|
|
5936
|
-
}
|
|
5937
|
-
});
|
|
5938
|
-
}
|
|
5939
|
-
|
|
5940
6239
|
#unwrap(node) {
|
|
5941
6240
|
const children = node.getChildren();
|
|
5942
6241
|
|
|
@@ -5957,30 +6256,6 @@ class Contents {
|
|
|
5957
6256
|
node.remove();
|
|
5958
6257
|
}
|
|
5959
6258
|
|
|
5960
|
-
// Anchors with non-meaningful hrefs (e.g. "#", "") appear in content copied
|
|
5961
|
-
// from rendered views where mentions and interactive elements are wrapped in
|
|
5962
|
-
// <a href="#"> tags. Unwrap them so their text content pastes as plain text
|
|
5963
|
-
// and real links are preserved.
|
|
5964
|
-
#unwrapPlaceholderAnchors(doc) {
|
|
5965
|
-
for (const anchor of doc.querySelectorAll("a")) {
|
|
5966
|
-
const href = anchor.getAttribute("href") || "";
|
|
5967
|
-
if (href === "" || href === "#") {
|
|
5968
|
-
anchor.replaceWith(...anchor.childNodes);
|
|
5969
|
-
}
|
|
5970
|
-
}
|
|
5971
|
-
}
|
|
5972
|
-
|
|
5973
|
-
// Table cells copied from a page inherit the source theme's inline color
|
|
5974
|
-
// styles (e.g. dark-mode backgrounds). Strip them so pasted tables adopt
|
|
5975
|
-
// the current theme instead of carrying stale colors.
|
|
5976
|
-
#stripTableCellColorStyles(doc) {
|
|
5977
|
-
for (const cell of doc.querySelectorAll("td, th")) {
|
|
5978
|
-
cell.style.removeProperty("background-color");
|
|
5979
|
-
cell.style.removeProperty("background");
|
|
5980
|
-
cell.style.removeProperty("color");
|
|
5981
|
-
}
|
|
5982
|
-
}
|
|
5983
|
-
|
|
5984
6259
|
#getTextAnchorData() {
|
|
5985
6260
|
const selection = $getSelection();
|
|
5986
6261
|
if (!selection || !selection.isCollapsed()) return { anchorNode: null, offset: 0 }
|
|
@@ -5993,19 +6268,27 @@ class Contents {
|
|
|
5993
6268
|
return { anchorNode, offset: anchor.offset }
|
|
5994
6269
|
}
|
|
5995
6270
|
|
|
5996
|
-
|
|
6271
|
+
// The replaced span can straddle the cursor (e.g. "@Jack" when "@" was just
|
|
6272
|
+
// inserted before "Jack"), so we anchor on the trigger before the cursor and
|
|
6273
|
+
// verify the whole string matches there rather than searching text up to it.
|
|
6274
|
+
#findReplacementStart(anchorNode, offset, stringToReplace) {
|
|
5997
6275
|
const fullText = anchorNode.getTextContent();
|
|
5998
|
-
const
|
|
5999
|
-
|
|
6276
|
+
const triggerIndex = fullText.slice(0, offset).lastIndexOf(stringToReplace[0]);
|
|
6277
|
+
|
|
6278
|
+
if (triggerIndex !== -1 && fullText.startsWith(stringToReplace, triggerIndex)) {
|
|
6279
|
+
return triggerIndex
|
|
6280
|
+
} else {
|
|
6281
|
+
return -1
|
|
6282
|
+
}
|
|
6000
6283
|
}
|
|
6001
6284
|
|
|
6002
|
-
#performTextReplacement(anchorNode, selection,
|
|
6285
|
+
#performTextReplacement(anchorNode, selection, startIndex, stringToReplace, replacementNodes) {
|
|
6003
6286
|
const fullText = anchorNode.getTextContent();
|
|
6004
|
-
const textBeforeString = fullText.slice(0,
|
|
6005
|
-
const
|
|
6287
|
+
const textBeforeString = fullText.slice(0, startIndex);
|
|
6288
|
+
const textAfterString = fullText.slice(startIndex + stringToReplace.length);
|
|
6006
6289
|
|
|
6007
6290
|
const textNodeBefore = this.#cloneTextNodeFormatting(anchorNode, selection, textBeforeString);
|
|
6008
|
-
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection,
|
|
6291
|
+
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterString || " ");
|
|
6009
6292
|
|
|
6010
6293
|
anchorNode.replace(textNodeBefore);
|
|
6011
6294
|
|
|
@@ -6013,7 +6296,7 @@ class Contents {
|
|
|
6013
6296
|
lastInsertedNode.insertAfter(textNodeAfter);
|
|
6014
6297
|
|
|
6015
6298
|
this.#appendLineBreakIfNeeded(textNodeAfter.getParentOrThrow());
|
|
6016
|
-
const cursorOffset =
|
|
6299
|
+
const cursorOffset = textAfterString ? 0 : 1;
|
|
6017
6300
|
textNodeAfter.select(cursorOffset, cursorOffset);
|
|
6018
6301
|
}
|
|
6019
6302
|
|
|
@@ -6226,7 +6509,7 @@ class Clipboard {
|
|
|
6226
6509
|
}
|
|
6227
6510
|
|
|
6228
6511
|
const linkNode = $createLinkNode(url).append($createTextNode(url));
|
|
6229
|
-
selection.insertNodes([ linkNode ]);
|
|
6512
|
+
BaseNodeInserter.for(selection).insertNodes([ linkNode ]);
|
|
6230
6513
|
|
|
6231
6514
|
$onUpdate(() => this.#dispatchLinkInsertEvent(linkNode.getKey(), { url }));
|
|
6232
6515
|
}
|
|
@@ -7257,11 +7540,12 @@ class AttachmentsExtension extends LexxyExtension {
|
|
|
7257
7540
|
|
|
7258
7541
|
#handleUploadMutations(mutations) {
|
|
7259
7542
|
const previousUploadsCount = this.#uploadsCount;
|
|
7260
|
-
for (const [ , mutation ] of mutations) {
|
|
7543
|
+
for (const [ key, mutation ] of mutations) {
|
|
7261
7544
|
if (mutation === "created") {
|
|
7262
7545
|
this.#uploadsCount++;
|
|
7263
7546
|
} else if (mutation === "destroyed") {
|
|
7264
7547
|
this.#uploadsCount--;
|
|
7548
|
+
this.editorElement.uploadRequests.abort(key);
|
|
7265
7549
|
}
|
|
7266
7550
|
}
|
|
7267
7551
|
|
|
@@ -7692,7 +7976,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7692
7976
|
static debug = false
|
|
7693
7977
|
static commands = [ "bold", "italic", "strikethrough" ]
|
|
7694
7978
|
|
|
7695
|
-
static observedAttributes = [ "connected", "required" ]
|
|
7979
|
+
static observedAttributes = [ "autocapitalize", "connected", "required" ]
|
|
7696
7980
|
|
|
7697
7981
|
#initialValue = ""
|
|
7698
7982
|
#previousInternalFormValue = null
|
|
@@ -7705,6 +7989,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7705
7989
|
|
|
7706
7990
|
#validity = new Map()
|
|
7707
7991
|
#validationTextArea = document.createElement("textarea")
|
|
7992
|
+
#uploadRequests
|
|
7708
7993
|
|
|
7709
7994
|
constructor() {
|
|
7710
7995
|
super();
|
|
@@ -7712,6 +7997,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7712
7997
|
this.internals.role = "presentation";
|
|
7713
7998
|
}
|
|
7714
7999
|
|
|
8000
|
+
get uploadRequests() {
|
|
8001
|
+
return this.#uploadRequests
|
|
8002
|
+
}
|
|
8003
|
+
|
|
7715
8004
|
connectedCallback() {
|
|
7716
8005
|
this.id ||= generateDomId("lexxy-editor");
|
|
7717
8006
|
this.config = new EditorConfiguration(this);
|
|
@@ -7732,6 +8021,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7732
8021
|
this.#disposables.push(this.clipboard);
|
|
7733
8022
|
|
|
7734
8023
|
this.adapter = new BrowserAdapter();
|
|
8024
|
+
this.#uploadRequests = new UploadRequests();
|
|
7735
8025
|
|
|
7736
8026
|
const commandDispatcher = CommandDispatcher.configureFor(this);
|
|
7737
8027
|
this.#disposables.push(commandDispatcher);
|
|
@@ -7759,8 +8049,15 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7759
8049
|
}
|
|
7760
8050
|
|
|
7761
8051
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
7762
|
-
if (name === "
|
|
7763
|
-
|
|
8052
|
+
if (typeof this[`${name}ChangedCallback`] === "function") {
|
|
8053
|
+
this[`${name}ChangedCallback`](oldValue, newValue);
|
|
8054
|
+
}
|
|
8055
|
+
}
|
|
8056
|
+
|
|
8057
|
+
autocapitalizeChangedCallback() {
|
|
8058
|
+
if (this.editorContentElement) {
|
|
8059
|
+
this.#transferAttributeToContentEditable(this.editorContentElement, "autocapitalize");
|
|
8060
|
+
}
|
|
7764
8061
|
}
|
|
7765
8062
|
|
|
7766
8063
|
connectedChangedCallback(oldValue, newValue) {
|
|
@@ -8092,25 +8389,33 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8092
8389
|
|
|
8093
8390
|
#createEditorContentElement() {
|
|
8094
8391
|
const editorContentElement = createElement("div", {
|
|
8392
|
+
id: `${this.id}-content`,
|
|
8095
8393
|
classList: "lexxy-editor__content",
|
|
8096
8394
|
contenteditable: true,
|
|
8097
|
-
autocapitalize: "none",
|
|
8098
8395
|
role: "textbox",
|
|
8099
8396
|
"aria-multiline": true,
|
|
8100
8397
|
"aria-label": this.#labelText,
|
|
8101
8398
|
placeholder: this.getAttribute("placeholder")
|
|
8102
8399
|
});
|
|
8103
|
-
|
|
8400
|
+
|
|
8104
8401
|
this.#ariaAttributes.forEach(attribute => editorContentElement.setAttribute(attribute.name, attribute.value));
|
|
8105
8402
|
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8403
|
+
this.#transferAttributeToContentEditable(editorContentElement, "autocapitalize");
|
|
8404
|
+
this.#transferAttributeToContentEditable(editorContentElement, "tabindex", { defaultValue: 0, removeSource: true });
|
|
8405
|
+
|
|
8406
|
+
return editorContentElement
|
|
8407
|
+
}
|
|
8408
|
+
|
|
8409
|
+
#transferAttributeToContentEditable(element, qualifiedName, { defaultValue = null, removeSource = false } = {}) {
|
|
8410
|
+
if (this.hasAttribute(qualifiedName)) {
|
|
8411
|
+
element.setAttribute(qualifiedName, this.getAttribute(qualifiedName));
|
|
8412
|
+
} else if (defaultValue !== null) {
|
|
8413
|
+
element.setAttribute(qualifiedName, defaultValue);
|
|
8109
8414
|
} else {
|
|
8110
|
-
|
|
8415
|
+
element.removeAttribute(qualifiedName);
|
|
8111
8416
|
}
|
|
8112
8417
|
|
|
8113
|
-
|
|
8418
|
+
if (removeSource) this.removeAttribute(qualifiedName);
|
|
8114
8419
|
}
|
|
8115
8420
|
|
|
8116
8421
|
get #labelText() {
|
|
@@ -8490,6 +8795,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
8490
8795
|
#reset() {
|
|
8491
8796
|
this.#dispose();
|
|
8492
8797
|
this.#resetValidity();
|
|
8798
|
+
this.#uploadRequests?.clear();
|
|
8493
8799
|
this.editorContentElement?.remove();
|
|
8494
8800
|
this.editorContentElement = null;
|
|
8495
8801
|
|
|
@@ -8727,6 +9033,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8727
9033
|
this.source = this.#createSource();
|
|
8728
9034
|
|
|
8729
9035
|
this.#addTriggerListener();
|
|
9036
|
+
this.#removePopoverBeforeTurboCaches();
|
|
8730
9037
|
this.toggleAttribute("connected", true);
|
|
8731
9038
|
}
|
|
8732
9039
|
|
|
@@ -8734,7 +9041,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8734
9041
|
this.#popoverListeners.dispose();
|
|
8735
9042
|
this.#globalListeners.dispose();
|
|
8736
9043
|
this.source = null;
|
|
8737
|
-
this
|
|
9044
|
+
this.#removePopover();
|
|
8738
9045
|
}
|
|
8739
9046
|
|
|
8740
9047
|
|
|
@@ -8855,24 +9162,33 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
8855
9162
|
const { node, offset } = this.#selection.selectedNodeWithOffset();
|
|
8856
9163
|
if (!node) return
|
|
8857
9164
|
|
|
8858
|
-
if (
|
|
8859
|
-
|
|
8860
|
-
const textBeforeCursor = fullText.slice(0, offset);
|
|
8861
|
-
const lastTriggerIndex = textBeforeCursor.lastIndexOf(this.trigger);
|
|
8862
|
-
const triggerEndIndex = lastTriggerIndex + this.trigger.length - 1;
|
|
8863
|
-
|
|
8864
|
-
// If trigger is not found, or cursor is at or before the trigger end position, hide popover
|
|
8865
|
-
if (lastTriggerIndex === -1 || offset <= triggerEndIndex) {
|
|
8866
|
-
this.#hidePopover();
|
|
8867
|
-
}
|
|
9165
|
+
if (this.#cursorIsTypingSearchTerm(node, offset)) {
|
|
9166
|
+
return
|
|
8868
9167
|
} else {
|
|
8869
|
-
// Cursor is not in a text node or at offset 0, hide popover
|
|
8870
9168
|
this.#hidePopover();
|
|
8871
9169
|
}
|
|
8872
9170
|
});
|
|
8873
9171
|
}));
|
|
8874
9172
|
}
|
|
8875
9173
|
|
|
9174
|
+
// The popover should stay open only while the cursor sits at the end of the
|
|
9175
|
+
// trigger and its search term. When the cursor moves away — before the
|
|
9176
|
+
// trigger, or past the token into later text — the text between the trigger
|
|
9177
|
+
// and the cursor breaks that run and we dismiss. A newline always breaks the
|
|
9178
|
+
// run; a space breaks it only for triggers that don't support spaces in
|
|
9179
|
+
// searches, since those that do (e.g. `person:`) expect multi-word terms.
|
|
9180
|
+
#cursorIsTypingSearchTerm(node, offset) {
|
|
9181
|
+
if (!$isTextNode(node) || offset === 0) return false
|
|
9182
|
+
|
|
9183
|
+
const textBeforeCursor = node.getTextContent().slice(0, offset);
|
|
9184
|
+
const lastTriggerIndex = textBeforeCursor.lastIndexOf(this.trigger);
|
|
9185
|
+
if (lastTriggerIndex === -1) return false
|
|
9186
|
+
|
|
9187
|
+
const searchTerm = textBeforeCursor.slice(lastTriggerIndex + this.trigger.length);
|
|
9188
|
+
const breakPattern = this.supportsSpaceInSearches ? /\n/ : /[ \n]/;
|
|
9189
|
+
return !breakPattern.test(searchTerm)
|
|
9190
|
+
}
|
|
9191
|
+
|
|
8876
9192
|
get #editor() {
|
|
8877
9193
|
return this.#editorElement.editor
|
|
8878
9194
|
}
|
|
@@ -9030,6 +9346,21 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
9030
9346
|
this.#addTriggerListener();
|
|
9031
9347
|
}
|
|
9032
9348
|
|
|
9349
|
+
// The popover is appended to the <lexxy-editor> subtree, so Turbo serializes it
|
|
9350
|
+
// into the page cache. Removing it before caching prevents an orphaned, unmanaged
|
|
9351
|
+
// popover from being restored on history back/forward.
|
|
9352
|
+
#removePopoverBeforeTurboCaches() {
|
|
9353
|
+
this.#globalListeners.track(
|
|
9354
|
+
registerEventListener(document, "turbo:before-cache", () => this.#removePopover())
|
|
9355
|
+
);
|
|
9356
|
+
}
|
|
9357
|
+
|
|
9358
|
+
#removePopover() {
|
|
9359
|
+
this.#popoverListeners.dispose();
|
|
9360
|
+
this.popoverElement?.remove();
|
|
9361
|
+
this.popoverElement = null;
|
|
9362
|
+
}
|
|
9363
|
+
|
|
9033
9364
|
#filterOptions = async () => {
|
|
9034
9365
|
if (this.initialPrompt) {
|
|
9035
9366
|
this.initialPrompt = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@37signals/lexxy",
|
|
3
|
-
"version": "0.9.19-alpha.
|
|
3
|
+
"version": "0.9.19-alpha.3",
|
|
4
4
|
"description": "Lexxy - A modern rich text editor for Rails.",
|
|
5
5
|
"module": "dist/lexxy.esm.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
29
29
|
"@rollup/plugin-terser": "^0.4.4",
|
|
30
30
|
"eslint": "^10.3.0",
|
|
31
|
-
"globals": "^17.6.0",
|
|
32
31
|
"eslint-plugin-compat": "^7.0.2",
|
|
32
|
+
"globals": "^17.6.0",
|
|
33
33
|
"jsdom": "^27.3.0",
|
|
34
34
|
"rollup": "^4.44.1",
|
|
35
35
|
"rollup-plugin-copy": "^3.5.0",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"test:browser:headed": "npx playwright test --config test/browser/playwright.config.js --headed",
|
|
53
53
|
"test:browser:debug": "npx playwright test --config test/browser/playwright.config.js --debug",
|
|
54
54
|
"prerelease": "yarn build:npm",
|
|
55
|
-
"release": "yarn build:npm && yarn publish"
|
|
55
|
+
"release": "yarn build:npm && yarn publish",
|
|
56
|
+
"release:alpha": "yarn build:npm && yarn publish --tag alpha"
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
59
|
"@lexical/clipboard": "^0.44.0",
|