@37signals/lexxy 0.8.2-beta → 0.8.5-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lexxy.esm.js +492 -38
- package/dist/stylesheets/lexxy-content.css +6 -10
- package/dist/stylesheets/lexxy-editor.css +102 -10
- package/package.json +1 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -10,7 +10,7 @@ import 'prismjs/components/prism-json';
|
|
|
10
10
|
import 'prismjs/components/prism-diff';
|
|
11
11
|
import DOMPurify from 'dompurify';
|
|
12
12
|
import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
|
|
13
|
-
import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, DecoratorNode, $createNodeSelection, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $createParagraphNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, $isRootOrShadowRoot, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $getEditor, $getNearestRootOrShadowRoot, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $isDecoratorNode, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, ElementNode, $splitNode, $createLineBreakNode, $isRootNode, ParagraphNode, RootNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
13
|
+
import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, DecoratorNode, $createNodeSelection, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $createParagraphNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, $createTextNode, $isRootOrShadowRoot, UNDO_COMMAND, REDO_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $getEditor, $getNearestRootOrShadowRoot, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $isDecoratorNode, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, $isParagraphNode, ElementNode, $splitNode, $createLineBreakNode, $isRootNode, ParagraphNode, RootNode, DRAGSTART_COMMAND, DROP_COMMAND, CLEAR_HISTORY_COMMAND, $addUpdateTag, COMMAND_PRIORITY_CRITICAL, KEY_SPACE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DOWN_COMMAND } from 'lexical';
|
|
14
14
|
import { buildEditorFromExtensions } from '@lexical/extension';
|
|
15
15
|
import { ListNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListItemNode, $getListDepth, $isListItemNode, $isListNode, $createListNode, registerList } from '@lexical/list';
|
|
16
16
|
import { $createAutoLinkNode, $toggleLink, LinkNode, $createLinkNode, AutoLinkNode, $isLinkNode } from '@lexical/link';
|
|
@@ -632,7 +632,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
632
632
|
}
|
|
633
633
|
|
|
634
634
|
get #buttons() {
|
|
635
|
-
return Array.from(this.querySelectorAll(":scope > button"))
|
|
635
|
+
return Array.from(this.querySelectorAll(":scope > button:not([data-prevent-overflow='true'])"))
|
|
636
636
|
}
|
|
637
637
|
|
|
638
638
|
get #focusableItems() {
|
|
@@ -702,7 +702,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
702
702
|
${ToolbarIcons.ol}
|
|
703
703
|
</button>
|
|
704
704
|
|
|
705
|
-
<button class="lexxy-editor__toolbar-button" type="button" name="upload" data-command="uploadAttachments" title="Upload file">
|
|
705
|
+
<button class="lexxy-editor__toolbar-button" type="button" name="upload" data-command="uploadAttachments" data-prevent-overflow="true" title="Upload file">
|
|
706
706
|
${ToolbarIcons.attachment}
|
|
707
707
|
</button>
|
|
708
708
|
|
|
@@ -713,9 +713,9 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
713
713
|
<button class="lexxy-editor__toolbar-button" type="button" name="divider" data-command="insertHorizontalDivider" title="Insert a divider">
|
|
714
714
|
${ToolbarIcons.hr}
|
|
715
715
|
</button>
|
|
716
|
-
|
|
716
|
+
|
|
717
717
|
<div class="lexxy-editor__toolbar-spacer" role="separator"></div>
|
|
718
|
-
|
|
718
|
+
|
|
719
719
|
<button class="lexxy-editor__toolbar-button" type="button" name="undo" data-command="undo" title="Undo">
|
|
720
720
|
${ToolbarIcons.undo}
|
|
721
721
|
</button>
|
|
@@ -1675,6 +1675,8 @@ class CommandDispatcher {
|
|
|
1675
1675
|
}
|
|
1676
1676
|
|
|
1677
1677
|
#handleDragEnter(event) {
|
|
1678
|
+
if (this.#isInternalDrag(event)) return
|
|
1679
|
+
|
|
1678
1680
|
this.dragCounter++;
|
|
1679
1681
|
if (this.dragCounter === 1) {
|
|
1680
1682
|
this.#saveSelectionBeforeDrag();
|
|
@@ -1683,6 +1685,8 @@ class CommandDispatcher {
|
|
|
1683
1685
|
}
|
|
1684
1686
|
|
|
1685
1687
|
#handleDragLeave(event) {
|
|
1688
|
+
if (this.#isInternalDrag(event)) return
|
|
1689
|
+
|
|
1686
1690
|
this.dragCounter--;
|
|
1687
1691
|
if (this.dragCounter === 0) {
|
|
1688
1692
|
this.#selectionBeforeDrag = null;
|
|
@@ -1691,10 +1695,14 @@ class CommandDispatcher {
|
|
|
1691
1695
|
}
|
|
1692
1696
|
|
|
1693
1697
|
#handleDragOver(event) {
|
|
1698
|
+
if (this.#isInternalDrag(event)) return
|
|
1699
|
+
|
|
1694
1700
|
event.preventDefault();
|
|
1695
1701
|
}
|
|
1696
1702
|
|
|
1697
1703
|
#handleDrop(event) {
|
|
1704
|
+
if (this.#isInternalDrag(event)) return
|
|
1705
|
+
|
|
1698
1706
|
event.preventDefault();
|
|
1699
1707
|
|
|
1700
1708
|
this.dragCounter = 0;
|
|
@@ -1728,6 +1736,10 @@ class CommandDispatcher {
|
|
|
1728
1736
|
this.#selectionBeforeDrag = null;
|
|
1729
1737
|
}
|
|
1730
1738
|
|
|
1739
|
+
#isInternalDrag(event) {
|
|
1740
|
+
return event.dataTransfer?.types.includes("application/x-lexxy-node-key")
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1731
1743
|
#handleTabKey(event) {
|
|
1732
1744
|
if (this.selection.isInsideList) {
|
|
1733
1745
|
return this.#handleTabForList(event)
|
|
@@ -1976,6 +1988,8 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
1976
1988
|
|
|
1977
1989
|
createAttachmentFigure() {
|
|
1978
1990
|
const figure = createAttachmentFigure(this.contentType, this.isPreviewableAttachment, this.fileName);
|
|
1991
|
+
figure.draggable = true;
|
|
1992
|
+
figure.dataset.lexicalNodeKey = this.__key;
|
|
1979
1993
|
|
|
1980
1994
|
const deleteButton = createElement("lexxy-node-delete-button");
|
|
1981
1995
|
figure.appendChild(deleteButton);
|
|
@@ -1993,7 +2007,9 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
1993
2007
|
|
|
1994
2008
|
#createDOMForImage(options = {}) {
|
|
1995
2009
|
const img = createElement("img", { src: this.src, draggable: false, alt: this.altText, ...this.#imageDimensions, ...options });
|
|
1996
|
-
|
|
2010
|
+
const container = createElement("div", { className: "attachment__container" });
|
|
2011
|
+
container.appendChild(img);
|
|
2012
|
+
return container
|
|
1997
2013
|
}
|
|
1998
2014
|
|
|
1999
2015
|
get #imageDimensions() {
|
|
@@ -2286,8 +2302,12 @@ class Selection {
|
|
|
2286
2302
|
if (!anchorNode) return null
|
|
2287
2303
|
|
|
2288
2304
|
if ($isTextNode(anchorNode)) {
|
|
2289
|
-
if (offset
|
|
2290
|
-
|
|
2305
|
+
if (offset === anchorNode.getTextContentSize()) return this.#getNextNodeFromTextEnd(anchorNode)
|
|
2306
|
+
if (this.#isCursorOnLastVisualLineOfBlock(anchorNode)) {
|
|
2307
|
+
const topLevelElement = anchorNode.getTopLevelElement();
|
|
2308
|
+
return topLevelElement ? topLevelElement.getNextSibling() : null
|
|
2309
|
+
}
|
|
2310
|
+
return null
|
|
2291
2311
|
}
|
|
2292
2312
|
|
|
2293
2313
|
if ($isElementNode(anchorNode)) {
|
|
@@ -2317,8 +2337,12 @@ class Selection {
|
|
|
2317
2337
|
if (!anchorNode) return null
|
|
2318
2338
|
|
|
2319
2339
|
if ($isTextNode(anchorNode)) {
|
|
2320
|
-
if (offset
|
|
2321
|
-
|
|
2340
|
+
if (offset === 0) return this.#getPreviousNodeFromTextStart(anchorNode)
|
|
2341
|
+
if (this.#isCursorOnFirstVisualLineOfBlock(anchorNode)) {
|
|
2342
|
+
const topLevelElement = anchorNode.getTopLevelElement();
|
|
2343
|
+
return topLevelElement ? topLevelElement.getPreviousSibling() : null
|
|
2344
|
+
}
|
|
2345
|
+
return null
|
|
2322
2346
|
}
|
|
2323
2347
|
|
|
2324
2348
|
if ($isElementNode(anchorNode)) {
|
|
@@ -2542,7 +2566,7 @@ class Selection {
|
|
|
2542
2566
|
const node = backwards ? this.nodeBeforeCursor : this.nodeAfterCursor;
|
|
2543
2567
|
if (!$isDecoratorNode(node)) return false
|
|
2544
2568
|
|
|
2545
|
-
if (this.#collapseListItemToParagraph()) return true
|
|
2569
|
+
if (this.#collapseListItemToParagraph(node)) return true
|
|
2546
2570
|
|
|
2547
2571
|
this.#removeEmptyElementAnchorNode();
|
|
2548
2572
|
|
|
@@ -2553,12 +2577,16 @@ class Selection {
|
|
|
2553
2577
|
// When the cursor is inside a list item, collapse the list item into a
|
|
2554
2578
|
// paragraph instead of selecting the decorator. This lets the user
|
|
2555
2579
|
// delete a list that immediately follows an attachment without the
|
|
2556
|
-
// attachment becoming selected.
|
|
2557
|
-
|
|
2580
|
+
// attachment becoming selected. Only applies when the decorator is
|
|
2581
|
+
// outside the list item (e.g. a block attachment before the list),
|
|
2582
|
+
// not when it's an inline mention inside the list item.
|
|
2583
|
+
#collapseListItemToParagraph(decoratorNode) {
|
|
2558
2584
|
const anchorNode = $getSelection()?.anchor?.getNode();
|
|
2559
2585
|
const listItem = anchorNode && $getNearestNodeOfType(anchorNode, ListItemNode);
|
|
2560
2586
|
if (!listItem) return false
|
|
2561
2587
|
|
|
2588
|
+
if (listItem.isParentOf(decoratorNode)) return false
|
|
2589
|
+
|
|
2562
2590
|
const listNode = $getNearestNodeOfType(listItem, ListNode);
|
|
2563
2591
|
if (!listNode) return false
|
|
2564
2592
|
|
|
@@ -2743,6 +2771,63 @@ class Selection {
|
|
|
2743
2771
|
}
|
|
2744
2772
|
return current ? current.getPreviousSibling() : null
|
|
2745
2773
|
}
|
|
2774
|
+
|
|
2775
|
+
#isCursorOnFirstVisualLineOfBlock(anchorNode) {
|
|
2776
|
+
return this.#isCursorOnEdgeLineOfBlock(anchorNode, "first")
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
#isCursorOnLastVisualLineOfBlock(anchorNode) {
|
|
2780
|
+
return this.#isCursorOnEdgeLineOfBlock(anchorNode, "last")
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
// Check whether the cursor sits on the first or last visual line of its
|
|
2784
|
+
// top-level block by comparing the Y position of the cursor with the Y
|
|
2785
|
+
// position of the block's start (first line) or end (last line).
|
|
2786
|
+
#isCursorOnEdgeLineOfBlock(anchorNode, edge) {
|
|
2787
|
+
const topLevelElement = anchorNode.getTopLevelElement();
|
|
2788
|
+
if (!topLevelElement) return false
|
|
2789
|
+
|
|
2790
|
+
const domElement = this.editor.getElementByKey(topLevelElement.getKey());
|
|
2791
|
+
if (!domElement) return false
|
|
2792
|
+
|
|
2793
|
+
const nativeSelection = window.getSelection();
|
|
2794
|
+
if (!nativeSelection?.rangeCount) return false
|
|
2795
|
+
|
|
2796
|
+
const cursorRect = this.#getReliableRectFromRange(nativeSelection.getRangeAt(0));
|
|
2797
|
+
if (!cursorRect || this.#isRectUnreliable(cursorRect)) return false
|
|
2798
|
+
|
|
2799
|
+
const edgeRect = this.#getEdgeCharRect(domElement, edge);
|
|
2800
|
+
if (!edgeRect || this.#isRectUnreliable(edgeRect)) return false
|
|
2801
|
+
|
|
2802
|
+
const tolerance = edgeRect.height > 0 ? edgeRect.height * 0.5 : 5;
|
|
2803
|
+
return Math.abs(cursorRect.top - edgeRect.top) < tolerance
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// Get a reliable bounding rect for the first or last character in a DOM
|
|
2807
|
+
// element by creating a non-collapsed range around it.
|
|
2808
|
+
#getEdgeCharRect(element, edge) {
|
|
2809
|
+
const walker = document.createTreeWalker(element, 4 /* NodeFilter.SHOW_TEXT */);
|
|
2810
|
+
let textNode;
|
|
2811
|
+
|
|
2812
|
+
if (edge === "first") {
|
|
2813
|
+
textNode = walker.nextNode();
|
|
2814
|
+
} else {
|
|
2815
|
+
while (walker.nextNode()) textNode = walker.currentNode;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
if (!textNode || textNode.length === 0) return null
|
|
2819
|
+
|
|
2820
|
+
const range = document.createRange();
|
|
2821
|
+
if (edge === "first") {
|
|
2822
|
+
range.setStart(textNode, 0);
|
|
2823
|
+
range.setEnd(textNode, 1);
|
|
2824
|
+
} else {
|
|
2825
|
+
range.setStart(textNode, textNode.length - 1);
|
|
2826
|
+
range.setEnd(textNode, textNode.length);
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
return range.getBoundingClientRect()
|
|
2830
|
+
}
|
|
2746
2831
|
}
|
|
2747
2832
|
|
|
2748
2833
|
function sanitize(html) {
|
|
@@ -3636,14 +3721,12 @@ class ImageGalleryNode extends ElementNode {
|
|
|
3636
3721
|
static importDOM() {
|
|
3637
3722
|
return {
|
|
3638
3723
|
div: (element) => {
|
|
3639
|
-
|
|
3640
|
-
if (!containsAttachment) return null
|
|
3724
|
+
if (!this.#isGalleryElement(element)) return null
|
|
3641
3725
|
|
|
3642
3726
|
return {
|
|
3643
3727
|
conversion: () => {
|
|
3644
3728
|
return {
|
|
3645
|
-
node: $createImageGalleryNode()
|
|
3646
|
-
after: children => children
|
|
3729
|
+
node: $createImageGalleryNode()
|
|
3647
3730
|
}
|
|
3648
3731
|
},
|
|
3649
3732
|
priority: 2
|
|
@@ -3660,6 +3743,13 @@ class ImageGalleryNode extends ElementNode {
|
|
|
3660
3743
|
return $isActionTextAttachmentNode(node) && node.isPreviewableImage
|
|
3661
3744
|
}
|
|
3662
3745
|
|
|
3746
|
+
static #isGalleryElement(element) {
|
|
3747
|
+
const attachmentChildren = element.querySelectorAll(`:scope > :is(${this.#attachmentTags.join()})`);
|
|
3748
|
+
return element.textContent.trim() === ""
|
|
3749
|
+
&& attachmentChildren.length > 0
|
|
3750
|
+
&& element.children.length === attachmentChildren.length
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3663
3753
|
static get #attachmentTags() {
|
|
3664
3754
|
return Object.keys(ActionTextAttachmentNode.importDOM())
|
|
3665
3755
|
}
|
|
@@ -5355,6 +5445,365 @@ class TablesExtension extends LexxyExtension {
|
|
|
5355
5445
|
}
|
|
5356
5446
|
}
|
|
5357
5447
|
|
|
5448
|
+
const MIME_TYPE = "application/x-lexxy-node-key";
|
|
5449
|
+
|
|
5450
|
+
class AttachmentDragAndDrop {
|
|
5451
|
+
#editor
|
|
5452
|
+
#draggedNodeKey = null
|
|
5453
|
+
#rafId = null
|
|
5454
|
+
#draggingRafId = null
|
|
5455
|
+
#cleanupFns = []
|
|
5456
|
+
|
|
5457
|
+
constructor(editor) {
|
|
5458
|
+
this.#editor = editor;
|
|
5459
|
+
|
|
5460
|
+
// Register Lexical commands at HIGH priority to intercept before the
|
|
5461
|
+
// base @lexical/rich-text handlers (which return true and consume the events).
|
|
5462
|
+
this.#cleanupFns.push(
|
|
5463
|
+
editor.registerCommand(DRAGSTART_COMMAND, (event) => this.#handleDragStart(event), COMMAND_PRIORITY_HIGH),
|
|
5464
|
+
editor.registerCommand(DROP_COMMAND, (event) => this.#handleDrop(event), COMMAND_PRIORITY_HIGH),
|
|
5465
|
+
);
|
|
5466
|
+
|
|
5467
|
+
// Use a root listener to register DOM-level dragover/dragend handlers
|
|
5468
|
+
// (these events need throttled rAF handling that works better as DOM listeners).
|
|
5469
|
+
const unregister = editor.registerRootListener((root, prevRoot) => {
|
|
5470
|
+
if (prevRoot) {
|
|
5471
|
+
prevRoot.removeEventListener("dragover", this.#onDragOver);
|
|
5472
|
+
prevRoot.removeEventListener("dragend", this.#onDragEnd);
|
|
5473
|
+
}
|
|
5474
|
+
if (root) {
|
|
5475
|
+
root.addEventListener("dragover", this.#onDragOver);
|
|
5476
|
+
root.addEventListener("dragend", this.#onDragEnd);
|
|
5477
|
+
}
|
|
5478
|
+
});
|
|
5479
|
+
this.#cleanupFns.push(unregister);
|
|
5480
|
+
}
|
|
5481
|
+
|
|
5482
|
+
destroy() {
|
|
5483
|
+
this.#cleanup();
|
|
5484
|
+
for (const fn of this.#cleanupFns) fn();
|
|
5485
|
+
this.#cleanupFns = [];
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
// -- Event handlers --------------------------------------------------------
|
|
5489
|
+
|
|
5490
|
+
#handleDragStart(event) {
|
|
5491
|
+
if (event.target.closest("textarea")) return false
|
|
5492
|
+
|
|
5493
|
+
const figure = event.target.closest("figure.attachment[data-lexical-node-key]");
|
|
5494
|
+
if (!figure) return false
|
|
5495
|
+
|
|
5496
|
+
this.#draggedNodeKey = figure.dataset.lexicalNodeKey;
|
|
5497
|
+
event.dataTransfer.setData(MIME_TYPE, this.#draggedNodeKey);
|
|
5498
|
+
event.dataTransfer.effectAllowed = "move";
|
|
5499
|
+
|
|
5500
|
+
// Add dragging class after a tick so it doesn't affect the drag image
|
|
5501
|
+
this.#draggingRafId = requestAnimationFrame(() => {
|
|
5502
|
+
this.#draggingRafId = null;
|
|
5503
|
+
figure.classList.add("lexxy-dragging");
|
|
5504
|
+
});
|
|
5505
|
+
|
|
5506
|
+
return true
|
|
5507
|
+
}
|
|
5508
|
+
|
|
5509
|
+
#onDragOver = (event) => {
|
|
5510
|
+
if (!this.#draggedNodeKey) return
|
|
5511
|
+
|
|
5512
|
+
event.preventDefault();
|
|
5513
|
+
event.dataTransfer.dropEffect = "move";
|
|
5514
|
+
|
|
5515
|
+
if (!this.#rafId) {
|
|
5516
|
+
this.#rafId = requestAnimationFrame(() => {
|
|
5517
|
+
this.#rafId = null;
|
|
5518
|
+
this.#updateDropTarget(event);
|
|
5519
|
+
});
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
#handleDrop(event) {
|
|
5524
|
+
if (!this.#draggedNodeKey) return false
|
|
5525
|
+
|
|
5526
|
+
event.preventDefault();
|
|
5527
|
+
|
|
5528
|
+
const target = this.#resolveDropTarget(event);
|
|
5529
|
+
const draggedKey = this.#draggedNodeKey;
|
|
5530
|
+
this.#cleanup();
|
|
5531
|
+
|
|
5532
|
+
if (target) {
|
|
5533
|
+
this.#performDrop(draggedKey, target);
|
|
5534
|
+
}
|
|
5535
|
+
return true
|
|
5536
|
+
}
|
|
5537
|
+
|
|
5538
|
+
#onDragEnd = () => {
|
|
5539
|
+
this.#cleanup();
|
|
5540
|
+
}
|
|
5541
|
+
|
|
5542
|
+
// -- Drop target resolution -----------------------------------------------
|
|
5543
|
+
|
|
5544
|
+
#updateDropTarget(event) {
|
|
5545
|
+
this.#clearDropIndicators();
|
|
5546
|
+
|
|
5547
|
+
const target = this.#resolveDropTarget(event);
|
|
5548
|
+
if (!target) return
|
|
5549
|
+
|
|
5550
|
+
if (target.type === "gallery" || target.type === "gallery-reorder") {
|
|
5551
|
+
target.element.classList.add(`lexxy-drop-target--gallery-${target.position}`);
|
|
5552
|
+
} else if (target.type === "list-item") {
|
|
5553
|
+
target.element.classList.add(`lexxy-drop-target--list-${target.position}`);
|
|
5554
|
+
} else {
|
|
5555
|
+
target.element.classList.add(`lexxy-drop-target--block-${target.position}`);
|
|
5556
|
+
}
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
#resolveDropTarget(event) {
|
|
5560
|
+
const element = document.elementFromPoint(event.clientX, event.clientY);
|
|
5561
|
+
if (!element) return null
|
|
5562
|
+
|
|
5563
|
+
const rootElement = this.#editor.getRootElement();
|
|
5564
|
+
if (!rootElement || !rootElement.contains(element)) return null
|
|
5565
|
+
|
|
5566
|
+
// Check if hovering over a previewable image (for gallery merge or reorder)
|
|
5567
|
+
const targetFigure = element.closest("figure.attachment--preview[data-lexical-node-key]");
|
|
5568
|
+
if (targetFigure && targetFigure.dataset.lexicalNodeKey !== this.#draggedNodeKey) {
|
|
5569
|
+
const targetGallery = targetFigure.closest(".attachment-gallery");
|
|
5570
|
+
if (targetGallery) {
|
|
5571
|
+
// If the dragged image is in the same gallery, this is a reorder
|
|
5572
|
+
const draggedFigure = rootElement.querySelector(`[data-lexical-node-key="${this.#draggedNodeKey}"]`);
|
|
5573
|
+
if (draggedFigure && targetGallery.contains(draggedFigure)) {
|
|
5574
|
+
const position = this.#computeHorizontalPosition(targetFigure, event.clientX);
|
|
5575
|
+
return { type: "gallery-reorder", element: targetFigure, nodeKey: targetFigure.dataset.lexicalNodeKey, position }
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
const position = this.#computeHorizontalPosition(targetFigure, event.clientX);
|
|
5579
|
+
return { type: "gallery", element: targetFigure, nodeKey: targetFigure.dataset.lexicalNodeKey, position }
|
|
5580
|
+
}
|
|
5581
|
+
|
|
5582
|
+
// Hovering over the dragged image itself inside a gallery — treat as no-op
|
|
5583
|
+
// to prevent fallthrough to the block handler, which would eject it from the gallery.
|
|
5584
|
+
if (targetFigure && targetFigure.closest(".attachment-gallery")) return null
|
|
5585
|
+
|
|
5586
|
+
// Check if hovering over a gallery's empty space (for reorder within gallery)
|
|
5587
|
+
const targetGallery = element.closest(".attachment-gallery");
|
|
5588
|
+
if (targetGallery) {
|
|
5589
|
+
let galleryFigure = element.closest("figure.attachment[data-lexical-node-key]");
|
|
5590
|
+
if (!galleryFigure) {
|
|
5591
|
+
galleryFigure = this.#findNearestFigureInGallery(targetGallery, event.clientX);
|
|
5592
|
+
}
|
|
5593
|
+
if (galleryFigure && galleryFigure.dataset.lexicalNodeKey !== this.#draggedNodeKey) {
|
|
5594
|
+
const position = this.#computeHorizontalPosition(galleryFigure, event.clientX);
|
|
5595
|
+
return { type: "gallery-reorder", element: galleryFigure, nodeKey: galleryFigure.dataset.lexicalNodeKey, position }
|
|
5596
|
+
}
|
|
5597
|
+
// Nearest figure is the dragged image — no-op to avoid block handler fallthrough
|
|
5598
|
+
if (galleryFigure) return null
|
|
5599
|
+
}
|
|
5600
|
+
|
|
5601
|
+
// Check if hovering over a list item (for list splitting)
|
|
5602
|
+
const listItem = element.closest("li");
|
|
5603
|
+
if (listItem && rootElement.contains(listItem)) {
|
|
5604
|
+
const position = this.#computeVerticalPosition(listItem, event.clientY);
|
|
5605
|
+
return { type: "list-item", element: listItem, position }
|
|
5606
|
+
}
|
|
5607
|
+
|
|
5608
|
+
// Otherwise, find nearest block-level element for between-block insertion.
|
|
5609
|
+
// Normalize so each gap has exactly one indicator: prefer "after" on the
|
|
5610
|
+
// previous sibling, falling back to "before" only for the first block.
|
|
5611
|
+
const block = this.#findNearestBlock(element, rootElement, event.clientY);
|
|
5612
|
+
if (!block) return null
|
|
5613
|
+
|
|
5614
|
+
const position = this.#computeVerticalPosition(block, event.clientY);
|
|
5615
|
+
if (position === "before" && block.previousElementSibling) {
|
|
5616
|
+
return { type: "block", element: block.previousElementSibling, position: "after" }
|
|
5617
|
+
}
|
|
5618
|
+
return { type: "block", element: block, position }
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
#findNearestBlock(element, rootElement, clientY) {
|
|
5622
|
+
let current = element;
|
|
5623
|
+
while (current && current !== rootElement) {
|
|
5624
|
+
if (current.parentElement === rootElement) return current
|
|
5625
|
+
current = current.parentElement;
|
|
5626
|
+
}
|
|
5627
|
+
|
|
5628
|
+
// elementFromPoint landed on the root itself (e.g. a margin gap between
|
|
5629
|
+
// blocks). Fall back to the nearest child by vertical distance.
|
|
5630
|
+
let nearest = null;
|
|
5631
|
+
let minDistance = Infinity;
|
|
5632
|
+
for (const child of rootElement.children) {
|
|
5633
|
+
const rect = child.getBoundingClientRect();
|
|
5634
|
+
const distance = Math.min(Math.abs(clientY - rect.top), Math.abs(clientY - rect.bottom));
|
|
5635
|
+
if (distance < minDistance) {
|
|
5636
|
+
minDistance = distance;
|
|
5637
|
+
nearest = child;
|
|
5638
|
+
}
|
|
5639
|
+
}
|
|
5640
|
+
return nearest
|
|
5641
|
+
}
|
|
5642
|
+
|
|
5643
|
+
#computeVerticalPosition(element, clientY) {
|
|
5644
|
+
const rect = element.getBoundingClientRect();
|
|
5645
|
+
return clientY < rect.top + rect.height / 2 ? "before" : "after"
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
#computeHorizontalPosition(element, clientX) {
|
|
5649
|
+
const rect = element.getBoundingClientRect();
|
|
5650
|
+
return clientX < rect.left + rect.width / 2 ? "before" : "after"
|
|
5651
|
+
}
|
|
5652
|
+
|
|
5653
|
+
#findNearestFigureInGallery(gallery, clientX) {
|
|
5654
|
+
const figures = gallery.querySelectorAll("figure.attachment[data-lexical-node-key]");
|
|
5655
|
+
let nearest = null;
|
|
5656
|
+
let minDistance = Infinity;
|
|
5657
|
+
for (const figure of figures) {
|
|
5658
|
+
const rect = figure.getBoundingClientRect();
|
|
5659
|
+
const center = rect.left + rect.width / 2;
|
|
5660
|
+
const distance = Math.abs(clientX - center);
|
|
5661
|
+
if (distance < minDistance) {
|
|
5662
|
+
minDistance = distance;
|
|
5663
|
+
nearest = figure;
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
return nearest
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5669
|
+
// -- Drop indicator --------------------------------------------------------
|
|
5670
|
+
|
|
5671
|
+
static #DROP_CLASSES = [
|
|
5672
|
+
"lexxy-drop-target--gallery-before", "lexxy-drop-target--gallery-after",
|
|
5673
|
+
"lexxy-drop-target--list-before", "lexxy-drop-target--list-after",
|
|
5674
|
+
"lexxy-drop-target--block-before", "lexxy-drop-target--block-after",
|
|
5675
|
+
]
|
|
5676
|
+
|
|
5677
|
+
#clearDropIndicators() {
|
|
5678
|
+
const rootElement = this.#editor.getRootElement();
|
|
5679
|
+
if (!rootElement) return
|
|
5680
|
+
|
|
5681
|
+
for (const el of rootElement.querySelectorAll("[class*='lexxy-drop-target--']")) {
|
|
5682
|
+
el.classList.remove(...AttachmentDragAndDrop.#DROP_CLASSES);
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
|
|
5686
|
+
// -- Node operations -------------------------------------------------------
|
|
5687
|
+
|
|
5688
|
+
#performDrop(draggedKey, target) {
|
|
5689
|
+
const draggedNode = $getNodeByKey(draggedKey);
|
|
5690
|
+
if (!draggedNode || !$isActionTextAttachmentNode(draggedNode)) return
|
|
5691
|
+
|
|
5692
|
+
if (target.type === "gallery") {
|
|
5693
|
+
this.#dropOntoImage(draggedNode, target.nodeKey, target.position);
|
|
5694
|
+
} else if (target.type === "gallery-reorder") {
|
|
5695
|
+
this.#reorderInGallery(draggedNode, target.nodeKey, target.position);
|
|
5696
|
+
} else if (target.type === "list-item") {
|
|
5697
|
+
this.#dropIntoList(draggedNode, target);
|
|
5698
|
+
} else {
|
|
5699
|
+
this.#dropBetweenBlocks(draggedNode, target);
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5702
|
+
// Clear selection to prevent a second history entry. Lexical dispatches
|
|
5703
|
+
// SELECTION_CHANGE_COMMAND during commit for non-range selections, which
|
|
5704
|
+
// creates a separate update. Null selection avoids that dispatch entirely
|
|
5705
|
+
// and also prevents Firefox's follow-up selectionchange from dirtying nodes.
|
|
5706
|
+
$setSelection(null);
|
|
5707
|
+
}
|
|
5708
|
+
|
|
5709
|
+
#dropOntoImage(draggedNode, targetKey, position) {
|
|
5710
|
+
const targetNode = $getNodeByKey(targetKey);
|
|
5711
|
+
if (!targetNode || !$isActionTextAttachmentNode(targetNode)) return
|
|
5712
|
+
if (draggedNode.is(targetNode)) return
|
|
5713
|
+
|
|
5714
|
+
draggedNode.remove();
|
|
5715
|
+
|
|
5716
|
+
const gallery = $findOrCreateGalleryForImage(targetNode);
|
|
5717
|
+
if (gallery) {
|
|
5718
|
+
if (position === "before") {
|
|
5719
|
+
targetNode.insertBefore(draggedNode);
|
|
5720
|
+
} else {
|
|
5721
|
+
targetNode.insertAfter(draggedNode);
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
#reorderInGallery(draggedNode, targetKey, position) {
|
|
5727
|
+
const targetNode = $getNodeByKey(targetKey);
|
|
5728
|
+
if (!targetNode || draggedNode.is(targetNode)) return
|
|
5729
|
+
|
|
5730
|
+
draggedNode.remove();
|
|
5731
|
+
|
|
5732
|
+
if (position === "before") {
|
|
5733
|
+
targetNode.insertBefore(draggedNode);
|
|
5734
|
+
} else {
|
|
5735
|
+
targetNode.insertAfter(draggedNode);
|
|
5736
|
+
}
|
|
5737
|
+
}
|
|
5738
|
+
|
|
5739
|
+
#dropIntoList(draggedNode, target) {
|
|
5740
|
+
const listItemNode = $getNearestNodeFromDOMNode(target.element);
|
|
5741
|
+
if (!listItemNode || !$isListItemNode(listItemNode)) return
|
|
5742
|
+
|
|
5743
|
+
const listNode = listItemNode.getParent();
|
|
5744
|
+
if (!listNode || !$isListNode(listNode)) return
|
|
5745
|
+
|
|
5746
|
+
const children = listNode.getChildren();
|
|
5747
|
+
const index = children.indexOf(listItemNode);
|
|
5748
|
+
if (index === -1) return
|
|
5749
|
+
|
|
5750
|
+
const splitIndex = target.position === "before" ? index : index + 1;
|
|
5751
|
+
|
|
5752
|
+
draggedNode.remove();
|
|
5753
|
+
|
|
5754
|
+
if (splitIndex === 0) {
|
|
5755
|
+
listNode.insertBefore(draggedNode);
|
|
5756
|
+
} else if (splitIndex >= children.length) {
|
|
5757
|
+
listNode.insertAfter(draggedNode);
|
|
5758
|
+
} else {
|
|
5759
|
+
const [ , listAfter ] = $splitNode(listNode, splitIndex);
|
|
5760
|
+
listAfter.insertBefore(draggedNode);
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
|
|
5764
|
+
#dropBetweenBlocks(draggedNode, target) {
|
|
5765
|
+
const targetNode = $getNearestNodeFromDOMNode(target.element);
|
|
5766
|
+
if (!targetNode) return
|
|
5767
|
+
|
|
5768
|
+
const topLevelTarget = targetNode.getTopLevelElement?.() || targetNode;
|
|
5769
|
+
if (draggedNode.is(topLevelTarget)) return
|
|
5770
|
+
|
|
5771
|
+
draggedNode.remove();
|
|
5772
|
+
|
|
5773
|
+
if (target.position === "before") {
|
|
5774
|
+
topLevelTarget.insertBefore(draggedNode);
|
|
5775
|
+
} else {
|
|
5776
|
+
topLevelTarget.insertAfter(draggedNode);
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
|
|
5780
|
+
// -- Lifecycle helpers -----------------------------------------------------
|
|
5781
|
+
|
|
5782
|
+
#cleanup() {
|
|
5783
|
+
this.#clearDropIndicators();
|
|
5784
|
+
|
|
5785
|
+
if (this.#draggedNodeKey) {
|
|
5786
|
+
const rootElement = this.#editor.getRootElement();
|
|
5787
|
+
if (rootElement) {
|
|
5788
|
+
const figure = rootElement.querySelector(`[data-lexical-node-key="${this.#draggedNodeKey}"]`);
|
|
5789
|
+
figure?.classList.remove("lexxy-dragging");
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
|
|
5793
|
+
this.#draggedNodeKey = null;
|
|
5794
|
+
|
|
5795
|
+
if (this.#rafId) {
|
|
5796
|
+
cancelAnimationFrame(this.#rafId);
|
|
5797
|
+
this.#rafId = null;
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
if (this.#draggingRafId) {
|
|
5801
|
+
cancelAnimationFrame(this.#draggingRafId);
|
|
5802
|
+
this.#draggingRafId = null;
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
|
|
5358
5807
|
class AttachmentsExtension extends LexxyExtension {
|
|
5359
5808
|
get enabled() {
|
|
5360
5809
|
return this.editorElement.supportsAttachments
|
|
@@ -5369,14 +5818,37 @@ class AttachmentsExtension extends LexxyExtension {
|
|
|
5369
5818
|
ImageGalleryNode
|
|
5370
5819
|
],
|
|
5371
5820
|
register(editor) {
|
|
5821
|
+
const dragAndDrop = new AttachmentDragAndDrop(editor);
|
|
5822
|
+
|
|
5372
5823
|
return mergeRegister(
|
|
5373
|
-
editor.
|
|
5824
|
+
editor.registerNodeTransform(ActionTextAttachmentNode, $extractAttachmentFromParagraph),
|
|
5825
|
+
editor.registerCommand(DELETE_CHARACTER_COMMAND, $collapseIntoGallery, COMMAND_PRIORITY_NORMAL),
|
|
5826
|
+
() => dragAndDrop.destroy()
|
|
5374
5827
|
)
|
|
5375
5828
|
}
|
|
5376
5829
|
})
|
|
5377
5830
|
}
|
|
5378
5831
|
}
|
|
5379
5832
|
|
|
5833
|
+
// Decorator nodes can be wrapped in a Paragraph Node by Lexical when contained in a <div>
|
|
5834
|
+
// We remove them, splitting the node as needed
|
|
5835
|
+
function $extractAttachmentFromParagraph(attachmentNode) {
|
|
5836
|
+
const parentNode = attachmentNode.getParent();
|
|
5837
|
+
if (!$isParagraphNode(parentNode)) return
|
|
5838
|
+
|
|
5839
|
+
if (parentNode.getChildrenSize() === 1) {
|
|
5840
|
+
parentNode.replace(attachmentNode);
|
|
5841
|
+
} else {
|
|
5842
|
+
const index = attachmentNode.getIndexWithinParent();
|
|
5843
|
+
const [ topParagraph, bottomParagraph ] = $splitNode(parentNode, index);
|
|
5844
|
+
topParagraph.insertAfter(attachmentNode);
|
|
5845
|
+
|
|
5846
|
+
for (const p of [ topParagraph, bottomParagraph ]) {
|
|
5847
|
+
if (p.isEmpty()) p.remove();
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
|
|
5380
5852
|
function $collapseIntoGallery(backwards) {
|
|
5381
5853
|
const anchor = $getSelection()?.anchor;
|
|
5382
5854
|
if (!anchor) return false
|
|
@@ -5623,7 +6095,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
5623
6095
|
return nodes
|
|
5624
6096
|
.filter(this.#isNotWhitespaceOnlyNode)
|
|
5625
6097
|
.map(this.#wrapTextNode)
|
|
5626
|
-
.map(this.#unwrapDecoratorNode)
|
|
5627
6098
|
}
|
|
5628
6099
|
|
|
5629
6100
|
// Whitespace-only text nodes (e.g. "\n" between block elements like <div>) and stray line break
|
|
@@ -5645,18 +6116,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
5645
6116
|
return paragraph
|
|
5646
6117
|
}
|
|
5647
6118
|
|
|
5648
|
-
// Custom decorator block elements such as action-text-attachments get wrapped into <p> automatically by Lexical.
|
|
5649
|
-
// We unwrap those.
|
|
5650
|
-
#unwrapDecoratorNode(node) {
|
|
5651
|
-
if ($isParagraphNode(node) && node.getChildrenSize() === 1) {
|
|
5652
|
-
const child = node.getFirstChild();
|
|
5653
|
-
if ($isDecoratorNode(child) && !child.isInline()) {
|
|
5654
|
-
return child
|
|
5655
|
-
}
|
|
5656
|
-
}
|
|
5657
|
-
return node
|
|
5658
|
-
}
|
|
5659
|
-
|
|
5660
6119
|
#initialize() {
|
|
5661
6120
|
this.#synchronizeWithChanges();
|
|
5662
6121
|
this.#registerComponents();
|
|
@@ -6968,21 +7427,16 @@ class NodeDeleteButton extends HTMLElement {
|
|
|
6968
7427
|
this.editor = this.editorElement.editor;
|
|
6969
7428
|
this.classList.add("lexxy-floating-controls");
|
|
6970
7429
|
|
|
6971
|
-
if (!this.
|
|
7430
|
+
if (!this.querySelector(".lexxy-node-delete")) {
|
|
6972
7431
|
this.#attachDeleteButton();
|
|
6973
7432
|
}
|
|
6974
7433
|
}
|
|
6975
7434
|
|
|
6976
7435
|
disconnectedCallback() {
|
|
6977
|
-
if (this.deleteButton && this.handleDeleteClick) {
|
|
6978
|
-
this.deleteButton.removeEventListener("click", this.handleDeleteClick);
|
|
6979
|
-
}
|
|
6980
|
-
|
|
6981
|
-
this.handleDeleteClick = null;
|
|
6982
|
-
this.deleteButton = null;
|
|
6983
7436
|
this.editor = null;
|
|
6984
7437
|
this.editorElement = null;
|
|
6985
7438
|
}
|
|
7439
|
+
|
|
6986
7440
|
#attachDeleteButton() {
|
|
6987
7441
|
const container = createElement("div", { className: "lexxy-floating-controls__group" });
|
|
6988
7442
|
|
|
@@ -419,16 +419,12 @@
|
|
|
419
419
|
text-align: center;
|
|
420
420
|
|
|
421
421
|
.attachment {
|
|
422
|
-
display: inline-
|
|
422
|
+
display: inline-flex;
|
|
423
|
+
flex-direction: column;
|
|
424
|
+
gap: 0;
|
|
423
425
|
inline-size: calc(33.333% - 0.8ch);
|
|
424
426
|
vertical-align: top;
|
|
425
427
|
|
|
426
|
-
img {
|
|
427
|
-
margin: auto;
|
|
428
|
-
max-block-size: 50rem;
|
|
429
|
-
object-fit: contain;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
428
|
.attachment__caption {
|
|
433
429
|
padding-block-end: 1ch;
|
|
434
430
|
}
|
|
@@ -451,10 +447,12 @@
|
|
|
451
447
|
--lexxy-attachment-image-size: 1em;
|
|
452
448
|
--lexxy-attachment-text-color: currentColor;
|
|
453
449
|
|
|
450
|
+
align-items: center;
|
|
454
451
|
background: var(--lexxy-attachment-bg-color);
|
|
455
452
|
border-radius: var(--lexxy-radius);
|
|
456
453
|
color: var(--lexxy-attachment-text-color);
|
|
457
|
-
display: inline;
|
|
454
|
+
display: inline-flex;
|
|
455
|
+
gap: 0.25ch;
|
|
458
456
|
margin: 0;
|
|
459
457
|
padding: 0;
|
|
460
458
|
position: relative;
|
|
@@ -464,8 +462,6 @@
|
|
|
464
462
|
block-size: var(--lexxy-attachment-image-size);
|
|
465
463
|
border-radius: 50%;
|
|
466
464
|
inline-size: var(--lexxy-attachment-image-size);
|
|
467
|
-
margin-inline-end: 0.25ch;
|
|
468
|
-
vertical-align: middle;
|
|
469
465
|
}
|
|
470
466
|
}
|
|
471
467
|
|
|
@@ -186,6 +186,8 @@
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
.attachment {
|
|
189
|
+
background-color: var(--lexxy-color-canvas);
|
|
190
|
+
|
|
189
191
|
progress {
|
|
190
192
|
max-inline-size: 10ch;
|
|
191
193
|
margin: auto;
|
|
@@ -205,6 +207,93 @@
|
|
|
205
207
|
inset-inline-start: unset;
|
|
206
208
|
}
|
|
207
209
|
}
|
|
210
|
+
|
|
211
|
+
&.attachment--error {
|
|
212
|
+
background: color-mix(var(--lexxy-color-red) 10%, transparent);
|
|
213
|
+
padding: 2ch;
|
|
214
|
+
|
|
215
|
+
&:before {
|
|
216
|
+
align-items: center;
|
|
217
|
+
aspect-ratio: 1;
|
|
218
|
+
background: var(--lexxy-color-red);
|
|
219
|
+
block-size: 1.5lh;
|
|
220
|
+
border-radius: 50%;
|
|
221
|
+
color: white;
|
|
222
|
+
content: "!";
|
|
223
|
+
display: flex;
|
|
224
|
+
justify-content: center;
|
|
225
|
+
margin: auto;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
> div {
|
|
229
|
+
flex: 1;
|
|
230
|
+
font-size: 0.85em;
|
|
231
|
+
padding: 1ch;
|
|
232
|
+
text-align: start;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.attachment[draggable] {
|
|
238
|
+
cursor: grab;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.attachment.lexxy-dragging {
|
|
242
|
+
opacity: 0.4;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
[class*="lexxy-drop-target--"] {
|
|
246
|
+
position: relative;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* Horizontal line indicator for block and list drops */
|
|
250
|
+
.lexxy-drop-target--block-before::before,
|
|
251
|
+
.lexxy-drop-target--block-after::after,
|
|
252
|
+
.lexxy-drop-target--list-before::before,
|
|
253
|
+
.lexxy-drop-target--list-after::after {
|
|
254
|
+
background-color: var(--lexxy-focus-ring-color);
|
|
255
|
+
block-size: 3px;
|
|
256
|
+
border-radius: 1px;
|
|
257
|
+
content: "";
|
|
258
|
+
inset-inline: 0;
|
|
259
|
+
pointer-events: none;
|
|
260
|
+
position: absolute;
|
|
261
|
+
transform: translate(0, 0.5ch);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.lexxy-drop-target--block-before::before,
|
|
265
|
+
.lexxy-drop-target--list-before::before {
|
|
266
|
+
transform: translate(0, -0.5ch);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.lexxy-drop-target--block-before::before,
|
|
270
|
+
.lexxy-drop-target--list-before::before {
|
|
271
|
+
inset-block-start: -2px;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.lexxy-drop-target--block-after::after,
|
|
275
|
+
.lexxy-drop-target--list-after::after {
|
|
276
|
+
inset-block-end: -2px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* Vertical line indicator for gallery merge and reorder */
|
|
280
|
+
.lexxy-drop-target--gallery-before::before,
|
|
281
|
+
.lexxy-drop-target--gallery-after::after {
|
|
282
|
+
background-color: var(--lexxy-focus-ring-color);
|
|
283
|
+
border-radius: 1px;
|
|
284
|
+
content: "";
|
|
285
|
+
inset-block: 0;
|
|
286
|
+
inline-size: 3px;
|
|
287
|
+
pointer-events: none;
|
|
288
|
+
position: absolute;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.lexxy-drop-target--gallery-before::before {
|
|
292
|
+
inset-inline-start: -4px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.lexxy-drop-target--gallery-after::after {
|
|
296
|
+
inset-inline-end: -4px;
|
|
208
297
|
}
|
|
209
298
|
|
|
210
299
|
.attachment:hover:not(.node--selected) {
|
|
@@ -225,27 +314,35 @@
|
|
|
225
314
|
/* ------------------------------------------------------------------------ */
|
|
226
315
|
|
|
227
316
|
.attachment-gallery {
|
|
317
|
+
--lexxy-attachment-gallery-columns: 3;
|
|
228
318
|
--lexxy-attachment-gallery-gap: 0.4ch;
|
|
229
319
|
--lexxy-focus-ring-offset: -6px;
|
|
230
320
|
|
|
231
|
-
display: block;
|
|
232
321
|
padding: 0;
|
|
233
322
|
|
|
234
323
|
.attachment {
|
|
324
|
+
background: transparent;
|
|
325
|
+
box-sizing: border-box;
|
|
235
326
|
margin: var(--lexxy-attachment-gallery-gap);
|
|
236
327
|
padding: 0;
|
|
237
328
|
padding-block-end: var(--lexxy-attachment-gap);
|
|
238
329
|
vertical-align: top;
|
|
239
330
|
|
|
240
|
-
&.attachment--error {
|
|
241
|
-
padding: 2ch;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
331
|
img {
|
|
245
332
|
box-sizing: border-box;
|
|
246
333
|
padding: 1ch;
|
|
247
334
|
padding-block-end: 0;
|
|
248
335
|
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
&.attachment--error {
|
|
339
|
+
background: color-mix(var(--lexxy-color-red) 10%, transparent);
|
|
340
|
+
padding: 2ch;
|
|
341
|
+
|
|
342
|
+
> div {
|
|
343
|
+
text-align: center;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
249
346
|
}
|
|
250
347
|
}
|
|
251
348
|
|
|
@@ -905,7 +1002,6 @@ action-text-attachment[content-type^="application/vnd.actiontext"] {
|
|
|
905
1002
|
}
|
|
906
1003
|
|
|
907
1004
|
lexxy-node-delete-button {
|
|
908
|
-
display: none;
|
|
909
1005
|
inset-inline-start: 0;
|
|
910
1006
|
line-height: 1lh;
|
|
911
1007
|
|
|
@@ -920,8 +1016,4 @@ action-text-attachment[content-type^="application/vnd.actiontext"] {
|
|
|
920
1016
|
}
|
|
921
1017
|
}
|
|
922
1018
|
}
|
|
923
|
-
|
|
924
|
-
&.node--selected lexxy-node-delete-button {
|
|
925
|
-
display: block;
|
|
926
|
-
}
|
|
927
1019
|
}
|