@37signals/lexxy 0.9.9-beta → 0.9.9-beta.preview2
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 +520 -400
- package/dist/stylesheets/lexxy-editor.css +11 -0
- package/package.json +15 -15
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { isActiveAndVisible, createElement, extractPlainTextFromHtml, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
|
|
2
2
|
export { highlightCode } from './lexxy_helpers.esm.js';
|
|
3
3
|
import DOMPurify from 'dompurify';
|
|
4
|
-
import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $
|
|
5
|
-
import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, DecoratorNode, $createParagraphNode, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, $createTextNode,
|
|
4
|
+
import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $ensureForwardRangeSelection, $setBlocksType, $forEachSelectedTextNode } from '@lexical/selection';
|
|
5
|
+
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, DecoratorNode, $createParagraphNode, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, $createTextNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, COMMAND_PRIORITY_EDITOR, $getEditor, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $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, $isParagraphNode, ParagraphNode, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, mergeRegister as mergeRegister$1, $findMatchingParent, CLEAR_HISTORY_COMMAND, KEY_ENTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
6
6
|
import { buildEditorFromExtensions } from '@lexical/extension';
|
|
7
7
|
import { ListNode, ListItemNode, $getListDepth, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, $isListItemNode, $isListNode, registerList } from '@lexical/list';
|
|
8
|
-
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode,
|
|
8
|
+
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
|
|
9
9
|
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, $descendantsMatching, IS_APPLE } from '@lexical/utils';
|
|
10
10
|
import { registerPlainText } from '@lexical/plain-text';
|
|
11
|
-
import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode,
|
|
11
|
+
import { RichTextExtension, $isQuoteNode, $isHeadingNode, QuoteNode, $createHeadingNode, $createQuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
12
12
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
13
13
|
import { $isCodeNode, CodeHighlightNode, CodeNode, $createCodeNode, $isCodeHighlightNode, $createCodeHighlightNode, normalizeCodeLang, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP } from '@lexical/code';
|
|
14
14
|
import { TRANSFORMERS, registerMarkdownShortcuts } from '@lexical/markdown';
|
|
15
|
-
import { createEmptyHistoryState, registerHistory } from '@lexical/history';
|
|
16
15
|
import { INSERT_TABLE_COMMAND, $getTableCellNodeFromLexicalNode, TableCellNode, TableNode, TableRowNode, setScrollableTablesActive, registerTablePlugin, registerTableSelectionObserver, TableCellHeaderStates, $insertTableRowAtSelection, $insertTableColumnAtSelection, $deleteTableRowAtSelection, $deleteTableColumnAtSelection, $findTableNode, $getTableRowIndexFromTableCellNode, $getTableColumnIndexFromTableCellNode, $findCellNode, $getElementForTableNode } from '@lexical/table';
|
|
16
|
+
import { HistoryExtension } from '@lexical/history';
|
|
17
17
|
import { marked } from 'marked';
|
|
18
18
|
import { $insertDataTransferForRichText } from '@lexical/clipboard';
|
|
19
19
|
import 'prismjs';
|
|
@@ -511,19 +511,10 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
511
511
|
}
|
|
512
512
|
|
|
513
513
|
#monitorHistoryChanges() {
|
|
514
|
-
this.#listeners.track(
|
|
515
|
-
this.#
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
#updateUndoRedoButtonStates() {
|
|
520
|
-
this.editor.getEditorState().read(() => {
|
|
521
|
-
const historyState = this.editorElement.historyState;
|
|
522
|
-
if (historyState) {
|
|
523
|
-
this.#setButtonDisabled("undo", historyState.undoStack.length === 0);
|
|
524
|
-
this.#setButtonDisabled("redo", historyState.redoStack.length === 0);
|
|
525
|
-
}
|
|
526
|
-
});
|
|
514
|
+
this.#listeners.track(
|
|
515
|
+
this.editor.registerCommand(CAN_UNDO_COMMAND, (enabled) => { this.#setButtonDisabled("undo", !enabled); }, COMMAND_PRIORITY_LOW),
|
|
516
|
+
this.editor.registerCommand(CAN_REDO_COMMAND, (enabled) => { this.#setButtonDisabled("redo", !enabled); }, COMMAND_PRIORITY_LOW),
|
|
517
|
+
);
|
|
527
518
|
}
|
|
528
519
|
|
|
529
520
|
#updateButtonStates() {
|
|
@@ -557,8 +548,6 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
557
548
|
this.#setButtonPressed("code", isInCode);
|
|
558
549
|
|
|
559
550
|
this.#setButtonPressed("table", isInTable);
|
|
560
|
-
|
|
561
|
-
this.#updateUndoRedoButtonStates();
|
|
562
551
|
}
|
|
563
552
|
|
|
564
553
|
#setButtonPressed(name, isPressed) {
|
|
@@ -769,11 +758,11 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
769
758
|
|
|
770
759
|
<div class="lexxy-editor__toolbar-spacer" role="separator"></div>
|
|
771
760
|
|
|
772
|
-
<button class="lexxy-editor__toolbar-button" type="button" name="undo" data-command="undo" title="Undo">
|
|
761
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="undo" data-command="undo" title="Undo" disabled aria-disabled="true">
|
|
773
762
|
${ToolbarIcons.undo}
|
|
774
763
|
</button>
|
|
775
764
|
|
|
776
|
-
<button class="lexxy-editor__toolbar-button" type="button" name="redo" data-command="redo" title="Redo">
|
|
765
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="redo" data-command="redo" title="Redo" disabled aria-disabled="true">
|
|
777
766
|
${ToolbarIcons.redo}
|
|
778
767
|
</button>
|
|
779
768
|
|
|
@@ -1332,14 +1321,16 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
|
1332
1321
|
}
|
|
1333
1322
|
}
|
|
1334
1323
|
|
|
1335
|
-
const SILENT_UPDATE_TAGS = [ HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG ];
|
|
1336
|
-
|
|
1337
1324
|
function $createNodeSelectionWith(...nodes) {
|
|
1338
1325
|
const selection = $createNodeSelection();
|
|
1339
1326
|
nodes.forEach(node => selection.add(node.getKey()));
|
|
1340
1327
|
return selection
|
|
1341
1328
|
}
|
|
1342
1329
|
|
|
1330
|
+
function $isShadowRoot(node) {
|
|
1331
|
+
return $isElementNode(node) && $isRootOrShadowRoot(node) && !$isRootNode(node)
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1343
1334
|
function $makeSafeForRoot(node) {
|
|
1344
1335
|
if ($isTextNode(node)) {
|
|
1345
1336
|
return $wrapNodeInElement(node, $createParagraphNode)
|
|
@@ -1356,6 +1347,11 @@ function getListType(node) {
|
|
|
1356
1347
|
return list?.getListType() ?? null
|
|
1357
1348
|
}
|
|
1358
1349
|
|
|
1350
|
+
function isEditorFocused(editor) {
|
|
1351
|
+
const rootElement = editor.getRootElement();
|
|
1352
|
+
return rootElement !== null && rootElement.contains(document.activeElement)
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1359
1355
|
function $isAtNodeEdge(point, atStart = null) {
|
|
1360
1356
|
if (atStart === null) {
|
|
1361
1357
|
return $isAtNodeEdge(point, true) || $isAtNodeEdge(point, false)
|
|
@@ -1464,6 +1460,35 @@ function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
|
|
|
1464
1460
|
&& previousNode instanceof CustomActionTextAttachmentNode
|
|
1465
1461
|
}
|
|
1466
1462
|
|
|
1463
|
+
// Shared, strictly-contained element used to attach ephemeral nodes when we
|
|
1464
|
+
// need to read computed styles (e.g. canonicalizing style values, resolving
|
|
1465
|
+
// CSS custom properties). The container is created once and attached to
|
|
1466
|
+
// `document.body` once; subsequent child mutations happen *inside* the
|
|
1467
|
+
// contained subtree so they do not invalidate style on the rest of the page.
|
|
1468
|
+
//
|
|
1469
|
+
// Without this, `document.body.appendChild(...)` / `element.remove()` calls
|
|
1470
|
+
// forced the browser to re-evaluate every ancestor-dependent selector (`:has()`,
|
|
1471
|
+
// descendant combinators, universal sibling rules) across the document on each
|
|
1472
|
+
// invocation — a 13,000+ element style recalc per call on a typical Basecamp
|
|
1473
|
+
// page.
|
|
1474
|
+
|
|
1475
|
+
let resolverRoot = null;
|
|
1476
|
+
|
|
1477
|
+
function styleResolverRoot() {
|
|
1478
|
+
if (resolverRoot && resolverRoot.isConnected) return resolverRoot
|
|
1479
|
+
|
|
1480
|
+
resolverRoot = document.createElement("div");
|
|
1481
|
+
resolverRoot.setAttribute("aria-hidden", "true");
|
|
1482
|
+
resolverRoot.setAttribute("data-lexxy-style-resolver", "");
|
|
1483
|
+
// `contain: strict` (size, layout, paint, style) isolates everything.
|
|
1484
|
+
// The root itself paints nothing (visibility hidden), has zero
|
|
1485
|
+
// geometric impact (position fixed, intrinsic size via contain), and
|
|
1486
|
+
// never leaks style invalidation to its ancestors.
|
|
1487
|
+
resolverRoot.style.cssText = "contain: strict; position: fixed; top: 0; left: 0; visibility: hidden; pointer-events: none; width: 0; height: 0;";
|
|
1488
|
+
document.body.appendChild(resolverRoot);
|
|
1489
|
+
return resolverRoot
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1467
1492
|
function isSelectionHighlighted(selection) {
|
|
1468
1493
|
if (!$isRangeSelection(selection)) return false
|
|
1469
1494
|
|
|
@@ -1544,10 +1569,11 @@ class StyleCanonicalizer {
|
|
|
1544
1569
|
}
|
|
1545
1570
|
}
|
|
1546
1571
|
|
|
1547
|
-
// Separates DOM writes from layout reads to avoid forced reflows
|
|
1548
|
-
// elements
|
|
1549
|
-
//
|
|
1550
|
-
//
|
|
1572
|
+
// Separates DOM writes from layout reads to avoid forced reflows, and attaches
|
|
1573
|
+
// resolver elements to a strictly-contained root (outside the normal document
|
|
1574
|
+
// flow) so neither the attach nor the detach invalidate styles on the rest of
|
|
1575
|
+
// the page. Without containment, appending to `document.body` triggered a
|
|
1576
|
+
// page-wide style recalc on every canonicalization pass.
|
|
1551
1577
|
function computeStyleValues(property, values) {
|
|
1552
1578
|
const fragment = document.createDocumentFragment();
|
|
1553
1579
|
|
|
@@ -1557,7 +1583,7 @@ function computeStyleValues(property, values) {
|
|
|
1557
1583
|
return element
|
|
1558
1584
|
});
|
|
1559
1585
|
|
|
1560
|
-
|
|
1586
|
+
styleResolverRoot().appendChild(fragment);
|
|
1561
1587
|
|
|
1562
1588
|
const computed = elements.map(element =>
|
|
1563
1589
|
window.getComputedStyle(element).getPropertyValue(property)
|
|
@@ -2541,6 +2567,10 @@ function debounceAsync(fn, wait) {
|
|
|
2541
2567
|
}
|
|
2542
2568
|
}
|
|
2543
2569
|
|
|
2570
|
+
function delay(ms) {
|
|
2571
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2544
2574
|
function nextFrame() {
|
|
2545
2575
|
return new Promise(requestAnimationFrame)
|
|
2546
2576
|
}
|
|
@@ -2589,6 +2619,109 @@ function parseBoolean(value) {
|
|
|
2589
2619
|
return Boolean(value)
|
|
2590
2620
|
}
|
|
2591
2621
|
|
|
2622
|
+
// Payload: Record<nodeKey, { patch?, replace? }>
|
|
2623
|
+
// - patch: plain object, shallow-merged into the existing node's properties
|
|
2624
|
+
// - replace: a LexicalNode instance that replaces the node
|
|
2625
|
+
const REWRITE_HISTORY_COMMAND = createCommand("REWRITE_HISTORY_COMMAND");
|
|
2626
|
+
|
|
2627
|
+
class RewritableHistoryExtension extends LexxyExtension {
|
|
2628
|
+
#historyState = null
|
|
2629
|
+
|
|
2630
|
+
get lexicalExtension() {
|
|
2631
|
+
return defineExtension({
|
|
2632
|
+
name: "lexxy/rewritable-history",
|
|
2633
|
+
dependencies: [ HistoryExtension ],
|
|
2634
|
+
register: (editor, _config, state) => {
|
|
2635
|
+
const historyOutput = state.getDependency(HistoryExtension).output;
|
|
2636
|
+
this.#historyState = historyOutput.historyState.value;
|
|
2637
|
+
|
|
2638
|
+
return editor.registerCommand(
|
|
2639
|
+
REWRITE_HISTORY_COMMAND,
|
|
2640
|
+
(rewrites) => this.#rewriteHistory(rewrites),
|
|
2641
|
+
COMMAND_PRIORITY_EDITOR
|
|
2642
|
+
)
|
|
2643
|
+
}
|
|
2644
|
+
})
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
get historyState() {
|
|
2648
|
+
return this.#historyState
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
get #allHistoryEntries() {
|
|
2652
|
+
const entries = Array.from(this.#historyState.undoStack);
|
|
2653
|
+
if (this.#historyState.current) entries.push(this.#historyState.current);
|
|
2654
|
+
return entries.concat(this.#historyState.redoStack)
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
#rewriteHistory(rewrites) {
|
|
2658
|
+
$getEditor().update(() => {
|
|
2659
|
+
for (const [ nodeKey, { patch, replace } ] of Object.entries(rewrites)) {
|
|
2660
|
+
const node = $getNodeByKey(nodeKey);
|
|
2661
|
+
if (!node) continue
|
|
2662
|
+
|
|
2663
|
+
if (patch) Object.assign(node.getWritable(), patch);
|
|
2664
|
+
if (replace) node.replace(replace);
|
|
2665
|
+
}
|
|
2666
|
+
}, { discrete: true, tag: this.#getBackgroundUpdateTags() });
|
|
2667
|
+
|
|
2668
|
+
const nodeKeys = Object.keys(rewrites);
|
|
2669
|
+
|
|
2670
|
+
for (const entry of this.#allHistoryEntries) {
|
|
2671
|
+
if (!this.#entryHasSomeKeys(entry, nodeKeys)) continue
|
|
2672
|
+
|
|
2673
|
+
for (const [ nodeKey, { patch, replace } ] of Object.entries(rewrites)) {
|
|
2674
|
+
const node = entry.editorState._nodeMap.get(nodeKey);
|
|
2675
|
+
if (!node) continue
|
|
2676
|
+
|
|
2677
|
+
entry.editorState = safeCloneEditorState(entry.editorState);
|
|
2678
|
+
|
|
2679
|
+
if (patch) {
|
|
2680
|
+
entry.editorState._nodeMap.set(nodeKey, $cloneNodeWithPatch(node, patch));
|
|
2681
|
+
} else if (replace) {
|
|
2682
|
+
entry.editorState._nodeMap.set(nodeKey, $cloneNodeAdoptingKey(replace, node));
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
return true
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
#entryHasSomeKeys(entry, nodeKeys) {
|
|
2691
|
+
return nodeKeys.some(key => entry.editorState._nodeMap.has(key))
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
#getBackgroundUpdateTags() {
|
|
2695
|
+
const tags = [ HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG ];
|
|
2696
|
+
if (!isEditorFocused(this.editorElement.editor)) { tags.push(SKIP_DOM_SELECTION_TAG); }
|
|
2697
|
+
return tags
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
function $cloneNodeWithPatch(node, patch) {
|
|
2702
|
+
const clone = $cloneWithProperties(node);
|
|
2703
|
+
Object.assign(clone, patch);
|
|
2704
|
+
return clone
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
function $cloneNodeAdoptingKey(source, keyNode) {
|
|
2708
|
+
const clone = $cloneWithProperties(source);
|
|
2709
|
+
clone.__key = keyNode.__key;
|
|
2710
|
+
clone.__parent = keyNode.__parent;
|
|
2711
|
+
clone.__prev = keyNode.__prev;
|
|
2712
|
+
clone.__next = keyNode.__next;
|
|
2713
|
+
return clone
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// EditorState#clone() keeps the same map reference.
|
|
2717
|
+
// A new Map is needed to prevent editing Lexical's internal map
|
|
2718
|
+
// Warning: this bypasses DEV's safety map freezing
|
|
2719
|
+
function safeCloneEditorState(editorState) {
|
|
2720
|
+
const clone = editorState.clone();
|
|
2721
|
+
clone._nodeMap = new Map(editorState._nodeMap);
|
|
2722
|
+
return clone
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2592
2725
|
class ActionTextAttachmentNode extends DecoratorNode {
|
|
2593
2726
|
static getType() {
|
|
2594
2727
|
return "action_text_attachment"
|
|
@@ -2801,6 +2934,18 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2801
2934
|
return figure
|
|
2802
2935
|
}
|
|
2803
2936
|
|
|
2937
|
+
patchAndRewriteHistory(patch) {
|
|
2938
|
+
this.editor.dispatchCommand(REWRITE_HISTORY_COMMAND, {
|
|
2939
|
+
[this.getKey()]: { patch }
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
replaceAndRewriteHistory(node) {
|
|
2944
|
+
this.editor.dispatchCommand(REWRITE_HISTORY_COMMAND, {
|
|
2945
|
+
[this.getKey()]: { replace: node }
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2804
2949
|
#createDOMForImage(options = {}) {
|
|
2805
2950
|
const initialSrc = this.previewSrc || this.src;
|
|
2806
2951
|
const img = createElement("img", { src: initialSrc, draggable: false, alt: this.altText, ...this.#imageDimensions, ...options });
|
|
@@ -2829,33 +2974,18 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2829
2974
|
|
|
2830
2975
|
#handleImageLoaded(img, previewSrc) {
|
|
2831
2976
|
img.src = this.src;
|
|
2832
|
-
this.
|
|
2833
|
-
if (this.isAttached()) this.getWritable().previewSrc = null;
|
|
2834
|
-
}, { tag: this.#backgroundUpdateTags });
|
|
2977
|
+
this.patchAndRewriteHistory({ previewSrc: null });
|
|
2835
2978
|
this.#revokePreviewSrc(previewSrc);
|
|
2836
2979
|
}
|
|
2837
2980
|
|
|
2838
2981
|
#handleImageLoadError(previewSrc) {
|
|
2839
|
-
this.
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
}
|
|
2844
|
-
}, { tag: this.#backgroundUpdateTags });
|
|
2982
|
+
this.patchAndRewriteHistory({
|
|
2983
|
+
previewSrc: null,
|
|
2984
|
+
uploadError: true
|
|
2985
|
+
});
|
|
2845
2986
|
this.#revokePreviewSrc(previewSrc);
|
|
2846
2987
|
}
|
|
2847
2988
|
|
|
2848
|
-
get #backgroundUpdateTags() {
|
|
2849
|
-
const rootElement = this.editor.getRootElement();
|
|
2850
|
-
const editorHasFocus = rootElement !== null && rootElement.contains(document.activeElement);
|
|
2851
|
-
|
|
2852
|
-
if (editorHasFocus) {
|
|
2853
|
-
return SILENT_UPDATE_TAGS
|
|
2854
|
-
} else {
|
|
2855
|
-
return [ ...SILENT_UPDATE_TAGS, SKIP_DOM_SELECTION_TAG ]
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
2989
|
#revokePreviewSrc(previewSrc) {
|
|
2860
2990
|
if (previewSrc?.startsWith("blob:")) URL.revokeObjectURL(previewSrc);
|
|
2861
2991
|
}
|
|
@@ -2917,9 +3047,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2917
3047
|
figure.appendChild(this.#createEditableCaption());
|
|
2918
3048
|
});
|
|
2919
3049
|
|
|
2920
|
-
this.
|
|
2921
|
-
if (this.isAttached()) this.getWritable().pendingPreview = false;
|
|
2922
|
-
}, { tag: this.#backgroundUpdateTags });
|
|
3050
|
+
this.patchAndRewriteHistory({ pendingPreview: false });
|
|
2923
3051
|
}
|
|
2924
3052
|
|
|
2925
3053
|
#swapFigureContent(figure, fromClass, toClass, renderContent) {
|
|
@@ -3034,12 +3162,6 @@ class Selection {
|
|
|
3034
3162
|
this.#clearStaleInlineCodeFormat();
|
|
3035
3163
|
}
|
|
3036
3164
|
|
|
3037
|
-
set current(selection) {
|
|
3038
|
-
this.editor.update(() => {
|
|
3039
|
-
this.#syncSelectedClasses();
|
|
3040
|
-
});
|
|
3041
|
-
}
|
|
3042
|
-
|
|
3043
3165
|
get hasNodeSelection() {
|
|
3044
3166
|
return this.editor.getEditorState().read(() => {
|
|
3045
3167
|
const selection = $getSelection();
|
|
@@ -3368,7 +3490,7 @@ class Selection {
|
|
|
3368
3490
|
this.editor.registerCommand(DELETE_CHARACTER_COMMAND, this.#selectDecoratorNodeBeforeDeletion.bind(this), COMMAND_PRIORITY_LOW),
|
|
3369
3491
|
|
|
3370
3492
|
this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
3371
|
-
this
|
|
3493
|
+
this.#syncSelectedClasses();
|
|
3372
3494
|
}, COMMAND_PRIORITY_LOW)
|
|
3373
3495
|
);
|
|
3374
3496
|
}
|
|
@@ -3566,6 +3688,7 @@ class Selection {
|
|
|
3566
3688
|
|
|
3567
3689
|
#selectInLexical(node) {
|
|
3568
3690
|
if ($isDecoratorNode(node)) {
|
|
3691
|
+
$addUpdateTag(HISTORY_MERGE_TAG);
|
|
3569
3692
|
const selection = $createNodeSelectionWith(node);
|
|
3570
3693
|
$setSelection(selection);
|
|
3571
3694
|
return selection
|
|
@@ -3931,107 +4054,6 @@ class EditorConfiguration {
|
|
|
3931
4054
|
}
|
|
3932
4055
|
}
|
|
3933
4056
|
|
|
3934
|
-
class ProvisionalParagraphNode extends ParagraphNode {
|
|
3935
|
-
$config() {
|
|
3936
|
-
return this.config("provisonal_paragraph", {
|
|
3937
|
-
extends: ParagraphNode,
|
|
3938
|
-
importDOM: () => null,
|
|
3939
|
-
$transform: (node) => {
|
|
3940
|
-
node.concretizeIfEdited(node);
|
|
3941
|
-
node.removeUnlessRequired(node);
|
|
3942
|
-
}
|
|
3943
|
-
})
|
|
3944
|
-
}
|
|
3945
|
-
|
|
3946
|
-
static neededBetween(nodeBefore, nodeAfter) {
|
|
3947
|
-
return !$isSelectableElement(nodeBefore, "next")
|
|
3948
|
-
&& !$isSelectableElement(nodeAfter, "previous")
|
|
3949
|
-
}
|
|
3950
|
-
|
|
3951
|
-
createDOM(editor) {
|
|
3952
|
-
const p = super.createDOM(editor);
|
|
3953
|
-
const selected = this.isSelected($getSelection());
|
|
3954
|
-
p.classList.add("provisional-paragraph");
|
|
3955
|
-
p.classList.toggle("hidden", !selected);
|
|
3956
|
-
return p
|
|
3957
|
-
}
|
|
3958
|
-
|
|
3959
|
-
updateDOM(_prevNode, dom) {
|
|
3960
|
-
const selected = this.isSelected($getSelection());
|
|
3961
|
-
dom.classList.toggle("hidden", !selected);
|
|
3962
|
-
return false
|
|
3963
|
-
}
|
|
3964
|
-
|
|
3965
|
-
getTextContent() {
|
|
3966
|
-
return ""
|
|
3967
|
-
}
|
|
3968
|
-
|
|
3969
|
-
exportDOM() {
|
|
3970
|
-
return {
|
|
3971
|
-
element: null
|
|
3972
|
-
}
|
|
3973
|
-
}
|
|
3974
|
-
|
|
3975
|
-
// override as Lexical has an interesting view of collapsed selection in ElementNodes
|
|
3976
|
-
// https://github.com/facebook/lexical/blob/f1e4f66014377b1f2595aec2b0ee17f5b7ef4dfc/packages/lexical/src/LexicalNode.ts#L646
|
|
3977
|
-
isSelected(selection = null) {
|
|
3978
|
-
const targetSelection = selection || $getSelection();
|
|
3979
|
-
if (!targetSelection) return false
|
|
3980
|
-
|
|
3981
|
-
if (targetSelection.getNodes().some(node => node.is(this) || this.isParentOf(node))) return true
|
|
3982
|
-
|
|
3983
|
-
// A collapsed range selection on the parent element at an offset adjacent to
|
|
3984
|
-
// this node means the caret is visually at this paragraph's position. Treat it
|
|
3985
|
-
// as selected so the paragraph is visible and the caret renders correctly.
|
|
3986
|
-
//
|
|
3987
|
-
// Both the offset matching our index (cursor just before us) and index + 1
|
|
3988
|
-
// (cursor just after us) count, because the provisional paragraph is an
|
|
3989
|
-
// invisible spacer: the browser resolves both offsets to the same visual spot.
|
|
3990
|
-
if ($isRangeSelection(targetSelection) && targetSelection.isCollapsed()) {
|
|
3991
|
-
const { anchor } = targetSelection;
|
|
3992
|
-
const parent = this.getParent();
|
|
3993
|
-
if (parent && anchor.getNode().is(parent) && anchor.type === "element") {
|
|
3994
|
-
const index = this.getIndexWithinParent();
|
|
3995
|
-
return anchor.offset === index || anchor.offset === index + 1
|
|
3996
|
-
}
|
|
3997
|
-
}
|
|
3998
|
-
|
|
3999
|
-
return false
|
|
4000
|
-
}
|
|
4001
|
-
|
|
4002
|
-
removeUnlessRequired(self = this.getLatest()) {
|
|
4003
|
-
if (!self.required) self.remove();
|
|
4004
|
-
}
|
|
4005
|
-
|
|
4006
|
-
concretizeIfEdited(self = this.getLatest()) {
|
|
4007
|
-
if (self.getTextContentSize() > 0) {
|
|
4008
|
-
self.replace($createParagraphNode(), true);
|
|
4009
|
-
}
|
|
4010
|
-
}
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
get required() {
|
|
4014
|
-
return this.isDirectRootChild && ProvisionalParagraphNode.neededBetween(...this.immediateSiblings)
|
|
4015
|
-
}
|
|
4016
|
-
|
|
4017
|
-
get isDirectRootChild() {
|
|
4018
|
-
const parent = this.getParent();
|
|
4019
|
-
return $isRootOrShadowRoot(parent)
|
|
4020
|
-
}
|
|
4021
|
-
|
|
4022
|
-
get immediateSiblings() {
|
|
4023
|
-
return [ this.getPreviousSibling(), this.getNextSibling() ]
|
|
4024
|
-
}
|
|
4025
|
-
}
|
|
4026
|
-
|
|
4027
|
-
function $isProvisionalParagraphNode(node) {
|
|
4028
|
-
return node instanceof ProvisionalParagraphNode
|
|
4029
|
-
}
|
|
4030
|
-
|
|
4031
|
-
function $isSelectableElement(node, direction) {
|
|
4032
|
-
return $isElementNode(node) && (direction === "next" ? node.canInsertTextBefore() : node.canInsertTextAfter())
|
|
4033
|
-
}
|
|
4034
|
-
|
|
4035
4057
|
async function loadFileIntoImage(file, image) {
|
|
4036
4058
|
return new Promise((resolve) => {
|
|
4037
4059
|
const reader = new FileReader();
|
|
@@ -4067,10 +4089,10 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
4067
4089
|
}
|
|
4068
4090
|
|
|
4069
4091
|
constructor(node, key) {
|
|
4070
|
-
const { file, uploadUrl, blobUrlTemplate, progress, width, height, uploadError } = node;
|
|
4071
|
-
super({ ...node, contentType: file
|
|
4072
|
-
this.file = file;
|
|
4073
|
-
this.fileName = file
|
|
4092
|
+
const { file, uploadUrl, blobUrlTemplate, progress, width, height, uploadError, fileName, contentType } = node;
|
|
4093
|
+
super({ ...node, contentType: file?.type ?? contentType }, key);
|
|
4094
|
+
this.file = file ?? null;
|
|
4095
|
+
this.fileName = file?.name ?? fileName;
|
|
4074
4096
|
this.uploadUrl = uploadUrl;
|
|
4075
4097
|
this.blobUrlTemplate = blobUrlTemplate;
|
|
4076
4098
|
this.progress = progress ?? null;
|
|
@@ -4127,6 +4149,8 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
4127
4149
|
...super.exportJSON(),
|
|
4128
4150
|
type: "action_text_attachment_upload",
|
|
4129
4151
|
version: 1,
|
|
4152
|
+
fileName: this.fileName,
|
|
4153
|
+
contentType: this.contentType,
|
|
4130
4154
|
uploadUrl: this.uploadUrl,
|
|
4131
4155
|
blobUrlTemplate: this.blobUrlTemplate,
|
|
4132
4156
|
progress: this.progress,
|
|
@@ -4151,14 +4175,14 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
4151
4175
|
}
|
|
4152
4176
|
|
|
4153
4177
|
#getFileExtension() {
|
|
4154
|
-
return this.
|
|
4178
|
+
return (this.fileName || "").split(".").pop().toLowerCase()
|
|
4155
4179
|
}
|
|
4156
4180
|
|
|
4157
4181
|
#createCaption() {
|
|
4158
4182
|
const figcaption = createElement("figcaption", { className: "attachment__caption" });
|
|
4159
4183
|
|
|
4160
|
-
const nameSpan = createElement("span", { className: "attachment__name", textContent: this.caption || this.
|
|
4161
|
-
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.file
|
|
4184
|
+
const nameSpan = createElement("span", { className: "attachment__name", textContent: this.caption || this.fileName || "" });
|
|
4185
|
+
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.file?.size) });
|
|
4162
4186
|
figcaption.appendChild(nameSpan);
|
|
4163
4187
|
figcaption.appendChild(sizeSpan);
|
|
4164
4188
|
|
|
@@ -4172,11 +4196,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
4172
4196
|
#setDimensionsFromImage({ width, height }) {
|
|
4173
4197
|
if (this.#hasDimensions) return
|
|
4174
4198
|
|
|
4175
|
-
this.
|
|
4176
|
-
const writable = this.getWritable();
|
|
4177
|
-
writable.width = width;
|
|
4178
|
-
writable.height = height;
|
|
4179
|
-
}, { tag: this.#backgroundUpdateTags });
|
|
4199
|
+
this.patchAndRewriteHistory({ width, height });
|
|
4180
4200
|
}
|
|
4181
4201
|
|
|
4182
4202
|
get #hasDimensions() {
|
|
@@ -4203,8 +4223,8 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
4203
4223
|
} else {
|
|
4204
4224
|
this.#dispatchEvent("lexxy:upload-end", { file: this.file, error: null });
|
|
4205
4225
|
this.editor.update(() => {
|
|
4206
|
-
this
|
|
4207
|
-
}
|
|
4226
|
+
this.$showUploadedAttachment(blob);
|
|
4227
|
+
});
|
|
4208
4228
|
}
|
|
4209
4229
|
});
|
|
4210
4230
|
}
|
|
@@ -4219,7 +4239,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
4219
4239
|
directUploadWillStoreFileWithXHR: (request) => {
|
|
4220
4240
|
if (shouldAuthenticateUploads) request.withCredentials = true;
|
|
4221
4241
|
|
|
4222
|
-
const uploadProgressHandler = (event) => this.#handleUploadProgress(event);
|
|
4242
|
+
const uploadProgressHandler = (event) => this.#handleUploadProgress(event, request);
|
|
4223
4243
|
request.upload.addEventListener("progress", uploadProgressHandler);
|
|
4224
4244
|
}
|
|
4225
4245
|
}
|
|
@@ -4229,70 +4249,35 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
4229
4249
|
this.#setProgress(1);
|
|
4230
4250
|
}
|
|
4231
4251
|
|
|
4232
|
-
#handleUploadProgress(event) {
|
|
4252
|
+
#handleUploadProgress(event, request) {
|
|
4233
4253
|
const progress = Math.round(event.loaded / event.total * 100);
|
|
4234
|
-
|
|
4235
|
-
|
|
4254
|
+
try {
|
|
4255
|
+
this.#setProgress(progress);
|
|
4256
|
+
this.#dispatchEvent("lexxy:upload-progress", { file: this.file, progress });
|
|
4257
|
+
} catch {
|
|
4258
|
+
request.abort();
|
|
4259
|
+
}
|
|
4236
4260
|
}
|
|
4237
4261
|
|
|
4238
4262
|
#setProgress(progress) {
|
|
4239
|
-
this.
|
|
4240
|
-
this.getWritable().progress = progress;
|
|
4241
|
-
}, { tag: this.#backgroundUpdateTags });
|
|
4263
|
+
this.patchAndRewriteHistory({ progress });
|
|
4242
4264
|
}
|
|
4243
4265
|
|
|
4244
4266
|
#handleUploadError(error) {
|
|
4245
4267
|
console.warn(`Upload error for ${this.file?.name ?? "file"}: ${error}`);
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
}, { tag: this.#backgroundUpdateTags });
|
|
4268
|
+
|
|
4269
|
+
this.patchAndRewriteHistory({ uploadError: true });
|
|
4249
4270
|
}
|
|
4250
4271
|
|
|
4251
|
-
showUploadedAttachment(blob) {
|
|
4272
|
+
$showUploadedAttachment(blob) {
|
|
4252
4273
|
const previewSrc = this.isPreviewableImage && this.file ? URL.createObjectURL(this.file) : null;
|
|
4253
4274
|
|
|
4254
4275
|
const replacementNode = this.#toActionTextAttachmentNodeWith(blob, previewSrc);
|
|
4255
|
-
|
|
4256
|
-
this.replace(replacementNode);
|
|
4257
|
-
|
|
4258
|
-
if (shouldSelectAfterReplacement && $isRootOrShadowRoot(replacementNode.getParent())) {
|
|
4259
|
-
replacementNode.selectNext();
|
|
4260
|
-
}
|
|
4276
|
+
this.replaceAndRewriteHistory(replacementNode);
|
|
4261
4277
|
|
|
4262
4278
|
return replacementNode.getKey()
|
|
4263
4279
|
}
|
|
4264
4280
|
|
|
4265
|
-
// Upload lifecycle methods (progress, completion, errors) run asynchronously and may
|
|
4266
|
-
// fire while the user is focused on another element (e.g., a title field). Without
|
|
4267
|
-
// SKIP_DOM_SELECTION_TAG, Lexical's reconciler would move the DOM selection back into
|
|
4268
|
-
// the editor, stealing focus from wherever the user is currently typing.
|
|
4269
|
-
get #backgroundUpdateTags() {
|
|
4270
|
-
if (this.#editorHasFocus) {
|
|
4271
|
-
return SILENT_UPDATE_TAGS
|
|
4272
|
-
} else {
|
|
4273
|
-
return [ ...SILENT_UPDATE_TAGS, SKIP_DOM_SELECTION_TAG ]
|
|
4274
|
-
}
|
|
4275
|
-
}
|
|
4276
|
-
|
|
4277
|
-
get #editorHasFocus() {
|
|
4278
|
-
const rootElement = this.editor.getRootElement();
|
|
4279
|
-
return rootElement !== null && rootElement.contains(document.activeElement)
|
|
4280
|
-
}
|
|
4281
|
-
|
|
4282
|
-
get #selectionIncludesUploadNode() {
|
|
4283
|
-
const selection = $getSelection();
|
|
4284
|
-
if (selection === null) return false
|
|
4285
|
-
|
|
4286
|
-
if (selection.getNodes().some((node) => node.is(this))) return true
|
|
4287
|
-
if (!$isRangeSelection(selection) || !selection.isCollapsed()) return false
|
|
4288
|
-
|
|
4289
|
-
const anchorNode = selection.anchor.getNode();
|
|
4290
|
-
if (!$isProvisionalParagraphNode(anchorNode) || !anchorNode.isEmpty()) return false
|
|
4291
|
-
|
|
4292
|
-
const previousSibling = anchorNode.getPreviousSibling();
|
|
4293
|
-
return previousSibling !== null && previousSibling.is(this)
|
|
4294
|
-
}
|
|
4295
|
-
|
|
4296
4281
|
#toActionTextAttachmentNodeWith(blob, previewSrc) {
|
|
4297
4282
|
const conversion = new AttachmentNodeConversion(this, blob, previewSrc);
|
|
4298
4283
|
return conversion.toAttachmentNode()
|
|
@@ -4642,35 +4627,138 @@ class GalleryUploader extends Uploader {
|
|
|
4642
4627
|
}
|
|
4643
4628
|
}
|
|
4644
4629
|
|
|
4645
|
-
class
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4630
|
+
class NodeInserter {
|
|
4631
|
+
static for(selection) {
|
|
4632
|
+
const INSERTERS = [
|
|
4633
|
+
CodeNodeInserter,
|
|
4634
|
+
QuoteNodeInserter,
|
|
4635
|
+
ShadowRootNodeInserter,
|
|
4636
|
+
NodeSelectionNodeInserter
|
|
4637
|
+
];
|
|
4638
|
+
const Inserter = INSERTERS.find(inserter => inserter.handles(selection));
|
|
4639
|
+
return Inserter ? new Inserter(selection) : selection
|
|
4649
4640
|
}
|
|
4650
4641
|
|
|
4651
|
-
|
|
4652
|
-
this.
|
|
4653
|
-
this.editor = null;
|
|
4642
|
+
constructor(selection) {
|
|
4643
|
+
this.selection = selection;
|
|
4654
4644
|
}
|
|
4645
|
+
}
|
|
4655
4646
|
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
this.insertDOM(parseHtml(html), { tag });
|
|
4647
|
+
class CodeNodeInserter extends NodeInserter {
|
|
4648
|
+
static handles(selection) {
|
|
4649
|
+
return $getNearestNodeOfType(selection.anchor?.getNode(), CodeNode)
|
|
4660
4650
|
}
|
|
4661
4651
|
|
|
4662
|
-
|
|
4663
|
-
this
|
|
4652
|
+
insertNodes(nodes) {
|
|
4653
|
+
if (!this.selection.isCollapsed()) { this.selection.removeText(); }
|
|
4664
4654
|
|
|
4665
|
-
this.
|
|
4666
|
-
|
|
4655
|
+
$ensureForwardRangeSelection(this.selection);
|
|
4656
|
+
const focusNode = this.selection.focus.getNode();
|
|
4657
|
+
const codeNode = $getNearestNodeOfType(focusNode, CodeNode);
|
|
4658
|
+
const insertionIndex = focusNode.is(codeNode) ? 0 : focusNode.getIndexWithinParent();
|
|
4667
4659
|
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4660
|
+
const caret = $getChildCaretAtIndex(codeNode, insertionIndex + 1, "previous");
|
|
4661
|
+
|
|
4662
|
+
for (const node of nodes) {
|
|
4663
|
+
if (!node.isAttached()) continue
|
|
4664
|
+
if (caret.getNodeAtCaret() && $isElementNode(node)) { caret.insert($createLineBreakNode()); }
|
|
4665
|
+
|
|
4666
|
+
caret.insert(this.#convertNodeToCodeChild(node));
|
|
4667
|
+
}
|
|
4668
|
+
|
|
4669
|
+
caret.getNodeAtCaret().selectEnd();
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
#convertNodeToCodeChild(node) {
|
|
4673
|
+
if ($isLineBreakNode(node)) {
|
|
4674
|
+
return node
|
|
4675
|
+
} else {
|
|
4676
|
+
node.remove();
|
|
4677
|
+
return $createTextNode(node.getTextContent())
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
// Lexical will split a QuoteNode when inserting other Elements - we want them simply inserted as-is
|
|
4684
|
+
class QuoteNodeInserter extends NodeInserter {
|
|
4685
|
+
static handles(selection) {
|
|
4686
|
+
return $getNearestNodeOfType(selection.anchor?.getNode(), QuoteNode)
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
insertNodes(nodes) {
|
|
4690
|
+
if (!this.selection.isCollapsed()) { this.selection.removeText(); }
|
|
4691
|
+
|
|
4692
|
+
$ensureForwardRangeSelection(this.selection);
|
|
4693
|
+
let lastNode = this.selection.focus.getNode();
|
|
4694
|
+
for (const node of nodes) {
|
|
4695
|
+
lastNode = lastNode.insertAfter(node);
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
lastNode.selectEnd();
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4702
|
+
class ShadowRootNodeInserter extends NodeInserter {
|
|
4703
|
+
static handles(selection) {
|
|
4704
|
+
return $isShadowRoot(selection?.anchor.getNode())
|
|
4705
|
+
}
|
|
4706
|
+
|
|
4707
|
+
insertNodes(nodes) {
|
|
4708
|
+
const anchorNode = this.selection.anchor.getNode();
|
|
4709
|
+
const paragraph = $createParagraphNode();
|
|
4710
|
+
anchorNode.append(paragraph);
|
|
4711
|
+
|
|
4712
|
+
paragraph.selectStart().insertNodes(nodes);
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4716
|
+
class NodeSelectionNodeInserter extends NodeInserter {
|
|
4717
|
+
static handles(selection) {
|
|
4718
|
+
return $isNodeSelection(selection)
|
|
4719
|
+
}
|
|
4720
|
+
|
|
4721
|
+
insertNodes(nodes) {
|
|
4722
|
+
const selectedNodes = this.selection.getNodes();
|
|
4723
|
+
|
|
4724
|
+
// Overrides Lexical's default behavior of _removing_ the currently selected nodes
|
|
4725
|
+
// https://github.com/facebook/lexical/blob/v0.38.2/packages/lexical/src/LexicalSelection.ts#L412
|
|
4726
|
+
let lastNode = selectedNodes.at(-1);
|
|
4727
|
+
for (const node of nodes) {
|
|
4728
|
+
lastNode = lastNode.insertAfter(node);
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
class Contents {
|
|
4734
|
+
constructor(editorElement) {
|
|
4735
|
+
this.editorElement = editorElement;
|
|
4736
|
+
this.editor = editorElement.editor;
|
|
4737
|
+
}
|
|
4738
|
+
|
|
4739
|
+
dispose() {
|
|
4740
|
+
this.editorElement = null;
|
|
4741
|
+
this.editor = null;
|
|
4742
|
+
}
|
|
4743
|
+
|
|
4744
|
+
get selection() { return this.editorElement.selection }
|
|
4745
|
+
|
|
4746
|
+
insertHtml(html, { tag } = {}) {
|
|
4747
|
+
this.insertDOM(parseHtml(html), { tag });
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4750
|
+
insertDOM(doc, { tag } = {}) {
|
|
4751
|
+
this.#unwrapPlaceholderAnchors(doc);
|
|
4752
|
+
|
|
4753
|
+
this.editor.update(() => {
|
|
4754
|
+
if ($hasUpdateTag(PASTE_TAG)) this.#stripTableCellColorStyles(doc);
|
|
4755
|
+
|
|
4756
|
+
const nodes = $generateNodesFromDOM(this.editor, doc);
|
|
4757
|
+
if (!this.#insertUploadNodes(nodes)) {
|
|
4758
|
+
this.insertAtCursor(...nodes);
|
|
4759
|
+
}
|
|
4760
|
+
}, { tag });
|
|
4761
|
+
}
|
|
4674
4762
|
|
|
4675
4763
|
insertAtCursor(...nodes) {
|
|
4676
4764
|
const selection = $getSelection() ?? $getRoot().selectEnd();
|
|
@@ -4912,7 +5000,7 @@ class Contents {
|
|
|
4912
5000
|
const node = $getNodeByKey(nodeKey);
|
|
4913
5001
|
if (!(node instanceof ActionTextAttachmentUploadNode)) return
|
|
4914
5002
|
|
|
4915
|
-
const replacementNodeKey = node
|
|
5003
|
+
const replacementNodeKey = node.$showUploadedAttachment(blob);
|
|
4916
5004
|
if (replacementNodeKey) {
|
|
4917
5005
|
nodeKey = replacementNodeKey;
|
|
4918
5006
|
}
|
|
@@ -5251,113 +5339,6 @@ class Contents {
|
|
|
5251
5339
|
}
|
|
5252
5340
|
}
|
|
5253
5341
|
|
|
5254
|
-
function $isShadowRoot(node) {
|
|
5255
|
-
return $isElementNode(node) && $isRootOrShadowRoot(node) && !$isRootNode(node)
|
|
5256
|
-
}
|
|
5257
|
-
|
|
5258
|
-
class NodeInserter {
|
|
5259
|
-
static for(selection) {
|
|
5260
|
-
const INSERTERS = [
|
|
5261
|
-
CodeNodeInserter,
|
|
5262
|
-
QuoteNodeInserter,
|
|
5263
|
-
ShadowRootNodeInserter,
|
|
5264
|
-
NodeSelectionNodeInserter
|
|
5265
|
-
];
|
|
5266
|
-
const Inserter = INSERTERS.find(inserter => inserter.handles(selection));
|
|
5267
|
-
return Inserter ? new Inserter(selection) : selection
|
|
5268
|
-
}
|
|
5269
|
-
|
|
5270
|
-
constructor(selection) {
|
|
5271
|
-
this.selection = selection;
|
|
5272
|
-
}
|
|
5273
|
-
}
|
|
5274
|
-
|
|
5275
|
-
class CodeNodeInserter extends NodeInserter {
|
|
5276
|
-
static handles(selection) {
|
|
5277
|
-
return $getNearestNodeOfType(selection.anchor?.getNode(), CodeNode)
|
|
5278
|
-
}
|
|
5279
|
-
|
|
5280
|
-
insertNodes(nodes) {
|
|
5281
|
-
if (!this.selection.isCollapsed()) { this.selection.removeText(); }
|
|
5282
|
-
|
|
5283
|
-
$ensureForwardRangeSelection(this.selection);
|
|
5284
|
-
const focusNode = this.selection.focus.getNode();
|
|
5285
|
-
const codeNode = $getNearestNodeOfType(focusNode, CodeNode);
|
|
5286
|
-
const insertionIndex = focusNode.is(codeNode) ? 0 : focusNode.getIndexWithinParent();
|
|
5287
|
-
|
|
5288
|
-
const caret = $getChildCaretAtIndex(codeNode, insertionIndex + 1, "previous");
|
|
5289
|
-
|
|
5290
|
-
for (const node of nodes) {
|
|
5291
|
-
if (!node.isAttached()) continue
|
|
5292
|
-
if (caret.getNodeAtCaret() && $isElementNode(node)) { caret.insert($createLineBreakNode()); }
|
|
5293
|
-
|
|
5294
|
-
caret.insert(this.#convertNodeToCodeChild(node));
|
|
5295
|
-
}
|
|
5296
|
-
|
|
5297
|
-
caret.getNodeAtCaret().selectEnd();
|
|
5298
|
-
}
|
|
5299
|
-
|
|
5300
|
-
#convertNodeToCodeChild(node) {
|
|
5301
|
-
if ($isLineBreakNode(node)) {
|
|
5302
|
-
return node
|
|
5303
|
-
} else {
|
|
5304
|
-
node.remove();
|
|
5305
|
-
return $createTextNode(node.getTextContent())
|
|
5306
|
-
}
|
|
5307
|
-
}
|
|
5308
|
-
|
|
5309
|
-
}
|
|
5310
|
-
|
|
5311
|
-
// Lexical will split a QuoteNode when inserting other Elements - we want them simply inserted as-is
|
|
5312
|
-
class QuoteNodeInserter extends NodeInserter {
|
|
5313
|
-
static handles(selection) {
|
|
5314
|
-
return $getNearestNodeOfType(selection.anchor?.getNode(), QuoteNode)
|
|
5315
|
-
}
|
|
5316
|
-
|
|
5317
|
-
insertNodes(nodes) {
|
|
5318
|
-
if (!this.selection.isCollapsed()) { this.selection.removeText(); }
|
|
5319
|
-
|
|
5320
|
-
$ensureForwardRangeSelection(this.selection);
|
|
5321
|
-
let lastNode = this.selection.focus.getNode();
|
|
5322
|
-
for (const node of nodes) {
|
|
5323
|
-
lastNode = lastNode.insertAfter(node);
|
|
5324
|
-
}
|
|
5325
|
-
|
|
5326
|
-
lastNode.selectEnd();
|
|
5327
|
-
}
|
|
5328
|
-
}
|
|
5329
|
-
|
|
5330
|
-
class ShadowRootNodeInserter extends NodeInserter {
|
|
5331
|
-
static handles(selection) {
|
|
5332
|
-
return $isShadowRoot(selection?.anchor.getNode())
|
|
5333
|
-
}
|
|
5334
|
-
|
|
5335
|
-
insertNodes(nodes) {
|
|
5336
|
-
const anchorNode = this.selection.anchor.getNode();
|
|
5337
|
-
const paragraph = $createParagraphNode();
|
|
5338
|
-
anchorNode.append(paragraph);
|
|
5339
|
-
|
|
5340
|
-
paragraph.selectStart().insertNodes(nodes);
|
|
5341
|
-
}
|
|
5342
|
-
}
|
|
5343
|
-
|
|
5344
|
-
class NodeSelectionNodeInserter extends NodeInserter {
|
|
5345
|
-
static handles(selection) {
|
|
5346
|
-
return $isNodeSelection(selection)
|
|
5347
|
-
}
|
|
5348
|
-
|
|
5349
|
-
insertNodes(nodes) {
|
|
5350
|
-
const selectedNodes = this.selection.getNodes();
|
|
5351
|
-
|
|
5352
|
-
// Overrides Lexical's default behavior of _removing_ the currently selected nodes
|
|
5353
|
-
// https://github.com/facebook/lexical/blob/v0.38.2/packages/lexical/src/LexicalSelection.ts#L412
|
|
5354
|
-
let lastNode = selectedNodes.at(-1);
|
|
5355
|
-
for (const node of nodes) {
|
|
5356
|
-
lastNode = lastNode.insertAfter(node);
|
|
5357
|
-
}
|
|
5358
|
-
}
|
|
5359
|
-
}
|
|
5360
|
-
|
|
5361
5342
|
class Clipboard {
|
|
5362
5343
|
constructor(editorElement) {
|
|
5363
5344
|
this.editorElement = editorElement;
|
|
@@ -5664,6 +5645,107 @@ function unwrapSpans(element) {
|
|
|
5664
5645
|
return element
|
|
5665
5646
|
}
|
|
5666
5647
|
|
|
5648
|
+
class ProvisionalParagraphNode extends ParagraphNode {
|
|
5649
|
+
$config() {
|
|
5650
|
+
return this.config("provisonal_paragraph", {
|
|
5651
|
+
extends: ParagraphNode,
|
|
5652
|
+
importDOM: () => null,
|
|
5653
|
+
$transform: (node) => {
|
|
5654
|
+
node.concretizeIfEdited(node);
|
|
5655
|
+
node.removeUnlessRequired(node);
|
|
5656
|
+
}
|
|
5657
|
+
})
|
|
5658
|
+
}
|
|
5659
|
+
|
|
5660
|
+
static neededBetween(nodeBefore, nodeAfter) {
|
|
5661
|
+
return !$isSelectableElement(nodeBefore, "next")
|
|
5662
|
+
&& !$isSelectableElement(nodeAfter, "previous")
|
|
5663
|
+
}
|
|
5664
|
+
|
|
5665
|
+
createDOM(editor) {
|
|
5666
|
+
const p = super.createDOM(editor);
|
|
5667
|
+
const selected = this.isSelected($getSelection());
|
|
5668
|
+
p.classList.add("provisional-paragraph");
|
|
5669
|
+
p.classList.toggle("hidden", !selected);
|
|
5670
|
+
return p
|
|
5671
|
+
}
|
|
5672
|
+
|
|
5673
|
+
updateDOM(_prevNode, dom) {
|
|
5674
|
+
const selected = this.isSelected($getSelection());
|
|
5675
|
+
dom.classList.toggle("hidden", !selected);
|
|
5676
|
+
return false
|
|
5677
|
+
}
|
|
5678
|
+
|
|
5679
|
+
getTextContent() {
|
|
5680
|
+
return ""
|
|
5681
|
+
}
|
|
5682
|
+
|
|
5683
|
+
exportDOM() {
|
|
5684
|
+
return {
|
|
5685
|
+
element: null
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
|
|
5689
|
+
// override as Lexical has an interesting view of collapsed selection in ElementNodes
|
|
5690
|
+
// https://github.com/facebook/lexical/blob/f1e4f66014377b1f2595aec2b0ee17f5b7ef4dfc/packages/lexical/src/LexicalNode.ts#L646
|
|
5691
|
+
isSelected(selection = null) {
|
|
5692
|
+
const targetSelection = selection || $getSelection();
|
|
5693
|
+
if (!targetSelection) return false
|
|
5694
|
+
|
|
5695
|
+
if (targetSelection.getNodes().some(node => node.is(this) || this.isParentOf(node))) return true
|
|
5696
|
+
|
|
5697
|
+
// A collapsed range selection on the parent element at an offset adjacent to
|
|
5698
|
+
// this node means the caret is visually at this paragraph's position. Treat it
|
|
5699
|
+
// as selected so the paragraph is visible and the caret renders correctly.
|
|
5700
|
+
//
|
|
5701
|
+
// Both the offset matching our index (cursor just before us) and index + 1
|
|
5702
|
+
// (cursor just after us) count, because the provisional paragraph is an
|
|
5703
|
+
// invisible spacer: the browser resolves both offsets to the same visual spot.
|
|
5704
|
+
if ($isRangeSelection(targetSelection) && targetSelection.isCollapsed()) {
|
|
5705
|
+
const { anchor } = targetSelection;
|
|
5706
|
+
const parent = this.getParent();
|
|
5707
|
+
if (parent && anchor.getNode().is(parent) && anchor.type === "element") {
|
|
5708
|
+
const index = this.getIndexWithinParent();
|
|
5709
|
+
return anchor.offset === index || anchor.offset === index + 1
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
return false
|
|
5714
|
+
}
|
|
5715
|
+
|
|
5716
|
+
removeUnlessRequired(self = this.getLatest()) {
|
|
5717
|
+
if (!self.required) self.remove();
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5720
|
+
concretizeIfEdited(self = this.getLatest()) {
|
|
5721
|
+
if (self.getTextContentSize() > 0) {
|
|
5722
|
+
self.replace($createParagraphNode(), true);
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
|
|
5727
|
+
get required() {
|
|
5728
|
+
return this.isDirectRootChild && ProvisionalParagraphNode.neededBetween(...this.immediateSiblings)
|
|
5729
|
+
}
|
|
5730
|
+
|
|
5731
|
+
get isDirectRootChild() {
|
|
5732
|
+
const parent = this.getParent();
|
|
5733
|
+
return $isRootOrShadowRoot(parent)
|
|
5734
|
+
}
|
|
5735
|
+
|
|
5736
|
+
get immediateSiblings() {
|
|
5737
|
+
return [ this.getPreviousSibling(), this.getNextSibling() ]
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
|
|
5741
|
+
function $isProvisionalParagraphNode(node) {
|
|
5742
|
+
return node instanceof ProvisionalParagraphNode
|
|
5743
|
+
}
|
|
5744
|
+
|
|
5745
|
+
function $isSelectableElement(node, direction) {
|
|
5746
|
+
return $isElementNode(node) && (direction === "next" ? node.canInsertTextBefore() : node.canInsertTextAfter())
|
|
5747
|
+
}
|
|
5748
|
+
|
|
5667
5749
|
class ProvisionalParagraphExtension extends LexxyExtension {
|
|
5668
5750
|
get lexicalExtension() {
|
|
5669
5751
|
return defineExtension({
|
|
@@ -5685,6 +5767,8 @@ class ProvisionalParagraphExtension extends LexxyExtension {
|
|
|
5685
5767
|
}
|
|
5686
5768
|
|
|
5687
5769
|
function $insertRequiredProvisionalParagraphs(rootNode) {
|
|
5770
|
+
const nodeBeforeRootSelection = $nodeBeforeRootSelection(rootNode);
|
|
5771
|
+
|
|
5688
5772
|
const firstNode = rootNode.getFirstChild();
|
|
5689
5773
|
if (ProvisionalParagraphNode.neededBetween(null, firstNode)) {
|
|
5690
5774
|
$insertFirst(rootNode, new ProvisionalParagraphNode);
|
|
@@ -5694,10 +5778,18 @@ function $insertRequiredProvisionalParagraphs(rootNode) {
|
|
|
5694
5778
|
const nextNode = node.getNextSibling();
|
|
5695
5779
|
if (ProvisionalParagraphNode.neededBetween(node, nextNode)) {
|
|
5696
5780
|
node.insertAfter(new ProvisionalParagraphNode);
|
|
5781
|
+
if (node.is(nodeBeforeRootSelection)) node.selectNext();
|
|
5697
5782
|
}
|
|
5698
5783
|
}
|
|
5699
5784
|
}
|
|
5700
5785
|
|
|
5786
|
+
function $nodeBeforeRootSelection(rootNode) {
|
|
5787
|
+
const selection = $getSelection();
|
|
5788
|
+
if (!$isRootOrShadowRoot(selection?.anchor?.getNode())) return null
|
|
5789
|
+
|
|
5790
|
+
return rootNode.getChildAtIndex(selection.anchor.offset - 1)
|
|
5791
|
+
}
|
|
5792
|
+
|
|
5701
5793
|
function $removeUnneededProvisionalParagraphs(rootNode) {
|
|
5702
5794
|
for (const provisionalParagraph of $getAllProvisionalParagraphs(rootNode)) {
|
|
5703
5795
|
provisionalParagraph.removeUnlessRequired();
|
|
@@ -5705,6 +5797,9 @@ function $removeUnneededProvisionalParagraphs(rootNode) {
|
|
|
5705
5797
|
}
|
|
5706
5798
|
|
|
5707
5799
|
function $markAllProvisionalParagraphsDirty() {
|
|
5800
|
+
// Selection-driven visibility updates must not become standalone undo steps.
|
|
5801
|
+
$addUpdateTag(HISTORY_MERGE_TAG);
|
|
5802
|
+
|
|
5708
5803
|
for (const provisionalParagraph of $getAllProvisionalParagraphs()) {
|
|
5709
5804
|
provisionalParagraph.markDirty();
|
|
5710
5805
|
}
|
|
@@ -6585,54 +6680,58 @@ class LinkOpenerExtension extends LexxyExtension {
|
|
|
6585
6680
|
get lexicalExtension() {
|
|
6586
6681
|
return defineExtension({
|
|
6587
6682
|
name: "lexxy/link-opener",
|
|
6588
|
-
register: () =>
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
}
|
|
6683
|
+
register: (editor) => mergeRegister$1(
|
|
6684
|
+
editor.registerCommand(CLICK_COMMAND, this.#handleClick.bind(this), COMMAND_PRIORITY_NORMAL),
|
|
6685
|
+
registerEventListener(this.editorElement.editorContentElement, "auxclick", this.#handleAuxClick.bind(this)),
|
|
6686
|
+
registerEventListener(window, "keydown", this.#handleKey.bind(this)),
|
|
6687
|
+
registerEventListener(window, "keyup", this.#handleKey.bind(this)),
|
|
6688
|
+
registerEventListener(window, "focus", this.#handleFocus.bind(this))
|
|
6689
|
+
)
|
|
6596
6690
|
})
|
|
6597
6691
|
}
|
|
6598
6692
|
|
|
6599
|
-
#
|
|
6693
|
+
#handleClick(event) {
|
|
6600
6694
|
if (this.#isModified(event)) {
|
|
6601
|
-
|
|
6695
|
+
return $openLink(event.target)
|
|
6602
6696
|
} else {
|
|
6603
|
-
|
|
6697
|
+
return false
|
|
6604
6698
|
}
|
|
6605
6699
|
}
|
|
6606
6700
|
|
|
6607
|
-
#
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
}, 200);
|
|
6701
|
+
#handleAuxClick(event) {
|
|
6702
|
+
if (event.button === 1) {
|
|
6703
|
+
this.editorElement.editor.read(() => $openLink(event.target));
|
|
6704
|
+
}
|
|
6612
6705
|
}
|
|
6613
6706
|
|
|
6614
|
-
#
|
|
6615
|
-
|
|
6707
|
+
#handleKey(event) {
|
|
6708
|
+
this.#updateOpenableAttribute(event);
|
|
6616
6709
|
}
|
|
6617
6710
|
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
anchor.setAttribute("rel", "noopener noreferrer");
|
|
6623
|
-
}
|
|
6711
|
+
// Chrome dispatches events without modifier keys *for a while* after changing tabs
|
|
6712
|
+
async #handleFocus() {
|
|
6713
|
+
await delay(200);
|
|
6714
|
+
this.editorElement.addEventListener("mousemove", this.#updateOpenableAttribute.bind(this), { once: true });
|
|
6624
6715
|
}
|
|
6625
6716
|
|
|
6626
|
-
#
|
|
6627
|
-
|
|
6628
|
-
anchor.removeAttribute("contenteditable");
|
|
6629
|
-
anchor.removeAttribute("target");
|
|
6630
|
-
anchor.removeAttribute("rel");
|
|
6631
|
-
}
|
|
6717
|
+
#updateOpenableAttribute(event) {
|
|
6718
|
+
this.editorElement.toggleAttribute("data-links-openable", this.#isModified(event));
|
|
6632
6719
|
}
|
|
6633
6720
|
|
|
6634
|
-
|
|
6635
|
-
return
|
|
6721
|
+
#isModified(event) {
|
|
6722
|
+
return IS_APPLE ? event.metaKey : event.ctrlKey
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
|
|
6726
|
+
function $openLink(target) {
|
|
6727
|
+
const node = $getNearestNodeFromDOMNode(target);
|
|
6728
|
+
const linkNode = $findMatchingParent(node, $isLinkNode);
|
|
6729
|
+
if (linkNode) {
|
|
6730
|
+
const url = linkNode.sanitizeUrl(linkNode.getURL());
|
|
6731
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
6732
|
+
return true
|
|
6733
|
+
} else {
|
|
6734
|
+
return false
|
|
6636
6735
|
}
|
|
6637
6736
|
}
|
|
6638
6737
|
|
|
@@ -6648,6 +6747,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6648
6747
|
#editorInitializedRafId = null
|
|
6649
6748
|
#listeners = new ListenerBin()
|
|
6650
6749
|
#disposables = []
|
|
6750
|
+
#historyState = { undo: false, redo: false }
|
|
6651
6751
|
|
|
6652
6752
|
constructor() {
|
|
6653
6753
|
super();
|
|
@@ -6739,6 +6839,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6739
6839
|
HighlightExtension,
|
|
6740
6840
|
TrixContentExtension,
|
|
6741
6841
|
TablesExtension,
|
|
6842
|
+
RewritableHistoryExtension,
|
|
6742
6843
|
AttachmentsExtension,
|
|
6743
6844
|
FormatEscapeExtension,
|
|
6744
6845
|
LinkOpenerExtension
|
|
@@ -6821,9 +6922,19 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6821
6922
|
}
|
|
6822
6923
|
|
|
6823
6924
|
focus() {
|
|
6925
|
+
// `editor.focus()` commits a reconciler update to position the cursor.
|
|
6926
|
+
// Skip if the contenteditable already owns focus — the update would be a
|
|
6927
|
+
// no-op but still triggers a full style/layout pass on pages with large
|
|
6928
|
+
// DOMs.
|
|
6929
|
+
if (this.#isContentFocused) return
|
|
6930
|
+
|
|
6824
6931
|
this.editor.focus(() => this.#onFocus());
|
|
6825
6932
|
}
|
|
6826
6933
|
|
|
6934
|
+
get #isContentFocused() {
|
|
6935
|
+
return !!this.editorContentElement && this.editorContentElement.contains(document.activeElement)
|
|
6936
|
+
}
|
|
6937
|
+
|
|
6827
6938
|
get value() {
|
|
6828
6939
|
if (!this.cachedValue) {
|
|
6829
6940
|
this.editor?.getEditorState().read(() => {
|
|
@@ -6836,19 +6947,21 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6836
6947
|
|
|
6837
6948
|
set value(html) {
|
|
6838
6949
|
this.editor.update(() => {
|
|
6839
|
-
$
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
root.selectEnd();
|
|
6950
|
+
$getRoot()
|
|
6951
|
+
.clear()
|
|
6952
|
+
.selectEnd()
|
|
6953
|
+
.insertNodes(this.#parseHtmlIntoLexicalNodes(html));
|
|
6844
6954
|
|
|
6845
6955
|
this.#toggleEmptyStatus();
|
|
6956
|
+
}, { discrete: true, tag: SKIP_DOM_SELECTION_TAG });
|
|
6957
|
+
}
|
|
6846
6958
|
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6959
|
+
get canUndo() {
|
|
6960
|
+
return this.#historyState.undo
|
|
6961
|
+
}
|
|
6962
|
+
|
|
6963
|
+
get canRedo() {
|
|
6964
|
+
return this.#historyState.redo
|
|
6852
6965
|
}
|
|
6853
6966
|
|
|
6854
6967
|
#parseHtmlIntoLexicalNodes(html) {
|
|
@@ -6884,6 +6997,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6884
6997
|
this.#registerComponents();
|
|
6885
6998
|
this.#handleEnter();
|
|
6886
6999
|
this.#registerFocusEvents();
|
|
7000
|
+
this.#registerHistoryEvents();
|
|
6887
7001
|
this.#attachDebugHooks();
|
|
6888
7002
|
this.#attachToolbar();
|
|
6889
7003
|
this.#configureSanitizer();
|
|
@@ -6980,8 +7094,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6980
7094
|
}
|
|
6981
7095
|
|
|
6982
7096
|
#loadInitialValue() {
|
|
6983
|
-
const initialHtml = this.valueBeforeDisconnect || this.getAttribute("value") || "<p></p>";
|
|
6984
|
-
this.
|
|
7097
|
+
const initialHtml = this.valueBeforeDisconnect || this.getAttribute("value") || "<p><br></p>";
|
|
7098
|
+
this.editor.update(() => {
|
|
7099
|
+
this.value = this.#initialValue = initialHtml;
|
|
7100
|
+
}, { tag: HISTORY_MERGE_TAG });
|
|
6985
7101
|
}
|
|
6986
7102
|
|
|
6987
7103
|
#resetBeforeTurboCaches() {
|
|
@@ -7031,8 +7147,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7031
7147
|
} else {
|
|
7032
7148
|
registered.push(registerPlainText(this.editor));
|
|
7033
7149
|
}
|
|
7034
|
-
this.historyState = createEmptyHistoryState();
|
|
7035
|
-
registered.push(registerHistory(this.editor, this.historyState, 20));
|
|
7036
7150
|
|
|
7037
7151
|
this.#listeners.track(...registered);
|
|
7038
7152
|
}
|
|
@@ -7115,6 +7229,12 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7115
7229
|
}
|
|
7116
7230
|
}
|
|
7117
7231
|
|
|
7232
|
+
#registerHistoryEvents() {
|
|
7233
|
+
this.#listeners.track(
|
|
7234
|
+
this.editor.registerCommand(CAN_UNDO_COMMAND, (enabled) => { this.#historyState.undo = enabled; }, COMMAND_PRIORITY_NORMAL),
|
|
7235
|
+
this.editor.registerCommand(CAN_REDO_COMMAND, (enabled) => { this.#historyState.redo = enabled; }, COMMAND_PRIORITY_NORMAL)
|
|
7236
|
+
);
|
|
7237
|
+
}
|
|
7118
7238
|
|
|
7119
7239
|
#attachDebugHooks() {
|
|
7120
7240
|
return
|
|
@@ -7205,8 +7325,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7205
7325
|
heading: { active: format.isInHeading, enabled: true },
|
|
7206
7326
|
"unordered-list": { active: format.isInList && format.listType === "bullet", enabled: true },
|
|
7207
7327
|
"ordered-list": { active: format.isInList && format.listType === "number", enabled: true },
|
|
7208
|
-
undo: { active: false, enabled: this.
|
|
7209
|
-
redo: { active: false, enabled: this.
|
|
7328
|
+
undo: { active: false, enabled: this.canUndo },
|
|
7329
|
+
redo: { active: false, enabled: this.canRedo }
|
|
7210
7330
|
};
|
|
7211
7331
|
|
|
7212
7332
|
linkHref = linkNode ? linkNode.getURL() : null;
|
|
@@ -7282,7 +7402,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7282
7402
|
return { element, name: cssValue }
|
|
7283
7403
|
});
|
|
7284
7404
|
|
|
7285
|
-
|
|
7405
|
+
styleResolverRoot().appendChild(container);
|
|
7286
7406
|
|
|
7287
7407
|
const resolved = resolvers.map(({ element, name }) => ({
|
|
7288
7408
|
name,
|
|
@@ -68,6 +68,10 @@
|
|
|
68
68
|
outline-offset: var(--lexxy-focus-ring-offset);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
&[data-links-openable] a {
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
/* Tables */
|
|
72
76
|
/* ------------------------------------------------------------------------ */
|
|
73
77
|
|
|
@@ -399,6 +403,13 @@
|
|
|
399
403
|
min-block-size: var(--lexxy-editor-rows);
|
|
400
404
|
outline: 0;
|
|
401
405
|
padding: var(--lexxy-editor-padding);
|
|
406
|
+
|
|
407
|
+
/* Isolate the contenteditable root's layout and style. Lexical's reconciler
|
|
408
|
+
commits mutations inside this element (nodes appended, text inserted,
|
|
409
|
+
class flipped) on every update; containment keeps those mutations from
|
|
410
|
+
invalidating ancestor-dependent selectors and sibling layout elsewhere
|
|
411
|
+
in the editor. */
|
|
412
|
+
contain: layout style;
|
|
402
413
|
}
|
|
403
414
|
|
|
404
415
|
:where(.lexxy-editor--drag-over) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@37signals/lexxy",
|
|
3
|
-
"version": "0.9.9-beta",
|
|
3
|
+
"version": "0.9.9-beta.preview2",
|
|
4
4
|
"description": "Lexxy - A modern rich text editor for Rails.",
|
|
5
5
|
"module": "dist/lexxy.esm.js",
|
|
6
6
|
"type": "module",
|
|
@@ -48,21 +48,21 @@
|
|
|
48
48
|
"release": "yarn build:npm && yarn publish"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@lexical/clipboard": "^0.
|
|
52
|
-
"@lexical/code": "^0.
|
|
53
|
-
"@lexical/extension": "^0.
|
|
54
|
-
"@lexical/history": "^0.
|
|
55
|
-
"@lexical/html": "^0.
|
|
56
|
-
"@lexical/link": "^0.
|
|
57
|
-
"@lexical/list": "^0.
|
|
58
|
-
"@lexical/markdown": "^0.
|
|
59
|
-
"@lexical/plain-text": "^0.
|
|
60
|
-
"@lexical/rich-text": "^0.
|
|
61
|
-
"@lexical/selection": "^0.
|
|
62
|
-
"@lexical/table": "^0.
|
|
63
|
-
"@lexical/utils": "^0.
|
|
51
|
+
"@lexical/clipboard": "^0.43.0",
|
|
52
|
+
"@lexical/code": "^0.43.0",
|
|
53
|
+
"@lexical/extension": "^0.43.0",
|
|
54
|
+
"@lexical/history": "^0.43.0",
|
|
55
|
+
"@lexical/html": "^0.43.0",
|
|
56
|
+
"@lexical/link": "^0.43.0",
|
|
57
|
+
"@lexical/list": "^0.43.0",
|
|
58
|
+
"@lexical/markdown": "^0.43.0",
|
|
59
|
+
"@lexical/plain-text": "^0.43.0",
|
|
60
|
+
"@lexical/rich-text": "^0.43.0",
|
|
61
|
+
"@lexical/selection": "^0.43.0",
|
|
62
|
+
"@lexical/table": "^0.43.0",
|
|
63
|
+
"@lexical/utils": "^0.43.0",
|
|
64
64
|
"dompurify": "^3.3.0",
|
|
65
|
-
"lexical": "^0.
|
|
65
|
+
"lexical": "^0.43.0",
|
|
66
66
|
"marked": "^16.4.1",
|
|
67
67
|
"prismjs": "^1.30.0"
|
|
68
68
|
},
|