@37signals/lexxy 0.9.8-beta → 0.9.9-beta-preview1
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 +200 -52
- package/dist/lexxy_helpers.esm.js +5 -1
- package/dist/stylesheets/lexxy-editor.css +7 -0
- package/package.json +1 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { createElement, extractPlainTextFromHtml, createAttachmentFigure, isPreviewableImage, dispatch, parseHtml, addBlockSpacing, generateDomId } from './lexxy_helpers.esm.js';
|
|
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
4
|
import { getStyleObjectFromCSS, getCSSFromStyleObject, $isAtNodeEnd, $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType, $forEachSelectedTextNode, $ensureForwardRangeSelection } from '@lexical/selection';
|
|
5
5
|
import { SKIP_DOM_SELECTION_TAG, $getSelection, $isRangeSelection, DecoratorNode, $createParagraphNode, $getNodeByKey, $isTextNode, $createRangeSelection, $setSelection, $createTextNode, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $createNodeSelection, $isDecoratorNode, $isLineBreakNode, $isElementNode, TextNode, createCommand, createState, defineExtension, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, 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, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, ParagraphNode, $isRootOrShadowRoot, ElementNode, $splitNode, $isParagraphNode, $createLineBreakNode, $isRootNode, $getChildCaretAtIndex, RootNode, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, INSERT_PARAGRAPH_COMMAND, CLEAR_HISTORY_COMMAND, $addUpdateTag, 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 { $createAutoLinkNode, $toggleLink,
|
|
9
|
-
import { $getNearestNodeOfType, $wrapNodeInElement, $lastToFirstIterator, mergeRegister, $insertFirst, $unwrapAndFilterDescendants, $firstToLastIterator, $getNearestBlockElementAncestorOrThrow, $descendantsMatching } from '@lexical/utils';
|
|
8
|
+
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, AutoLinkNode, $isLinkNode } from '@lexical/link';
|
|
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
11
|
import { RichTextExtension, $isQuoteNode, $isHeadingNode, $createHeadingNode, $createQuoteNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
12
12
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
@@ -209,10 +209,6 @@ class NextElementFinder {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
function isActiveAndVisible(element) {
|
|
213
|
-
return element && !element.disabled && element.checkVisibility()
|
|
214
|
-
}
|
|
215
|
-
|
|
216
212
|
var ToolbarIcons = {
|
|
217
213
|
"bold":
|
|
218
214
|
`<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -486,7 +482,8 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
486
482
|
}
|
|
487
483
|
|
|
488
484
|
#handleEditorFocus = () => {
|
|
489
|
-
this.#focusableItems
|
|
485
|
+
const firstVisible = this.#focusableItems.find(isActiveAndVisible);
|
|
486
|
+
if (firstVisible) firstVisible.tabIndex = 0;
|
|
490
487
|
}
|
|
491
488
|
|
|
492
489
|
#handleEditorBlur = () => {
|
|
@@ -579,12 +576,6 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
579
576
|
}
|
|
580
577
|
}
|
|
581
578
|
|
|
582
|
-
#toolbarIsOverflowing() {
|
|
583
|
-
// Safari can report inconsistent clientWidth values on more than 100% window zoom level,
|
|
584
|
-
// that was affecting the toolbar overflow calculation. We're adding +1 to get around this issue.
|
|
585
|
-
return (this.scrollWidth - this.#overflow.clientWidth) > this.clientWidth + 1
|
|
586
|
-
}
|
|
587
|
-
|
|
588
579
|
#refreshToolbarOverflow = () => {
|
|
589
580
|
this.#resetToolbarOverflow();
|
|
590
581
|
this.#compactMenu();
|
|
@@ -597,29 +588,46 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
597
588
|
this.#overflowMenu.toggleAttribute("disabled", !isOverflowing);
|
|
598
589
|
}
|
|
599
590
|
|
|
591
|
+
// Separates layout reads from DOM writes to avoid forced reflows during init.
|
|
592
|
+
// Measures every button's right edge in a single read pass, figures out which
|
|
593
|
+
// buttons overflow using math, and then moves them in a single write pass.
|
|
594
|
+
// The previous implementation interleaved `scrollWidth`/`clientWidth` reads with
|
|
595
|
+
// `prepend()` writes inside a loop, forcing one full browser reflow per button.
|
|
600
596
|
#compactMenu() {
|
|
601
|
-
const buttons = this.#buttons
|
|
602
|
-
|
|
597
|
+
const buttons = this.#buttons;
|
|
598
|
+
if (buttons.length === 0) return
|
|
603
599
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
600
|
+
const availableWidth = this.clientWidth + 1; // +1 for Safari zoom rounding
|
|
601
|
+
const buttonRightEdges = buttons.map(button => button.offsetLeft + button.offsetWidth);
|
|
602
|
+
|
|
603
|
+
let firstOverflowing = -1;
|
|
604
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
605
|
+
if (buttonRightEdges[i] > availableWidth) {
|
|
606
|
+
firstOverflowing = i;
|
|
610
607
|
break
|
|
611
608
|
}
|
|
612
609
|
}
|
|
610
|
+
|
|
611
|
+
if (firstOverflowing === -1) return
|
|
612
|
+
|
|
613
|
+
// Move one extra button to reserve space for the overflow control, which is
|
|
614
|
+
// `display: none` until we show it — matching the previous implementation's
|
|
615
|
+
// "move one more after it stops overflowing" behaviour.
|
|
616
|
+
const overflowIndex = Math.max(0, firstOverflowing - 1);
|
|
617
|
+
const overflowButtons = buttons.slice(overflowIndex).reverse();
|
|
618
|
+
for (const button of overflowButtons) {
|
|
619
|
+
this.#overflowMenu.prepend(button);
|
|
620
|
+
}
|
|
613
621
|
}
|
|
614
622
|
|
|
615
623
|
#resetToolbarOverflow() {
|
|
616
624
|
const items = Array.from(this.#overflowMenu.children);
|
|
617
625
|
items.sort((a, b) => this.#itemPosition(b) - this.#itemPosition(a));
|
|
618
626
|
|
|
619
|
-
|
|
627
|
+
for (const item of items) {
|
|
620
628
|
const nextItem = this.querySelector(`[data-position="${this.#itemPosition(item) + 1}"]`) ?? this.#overflow;
|
|
621
629
|
this.insertBefore(item, nextItem);
|
|
622
|
-
}
|
|
630
|
+
}
|
|
623
631
|
}
|
|
624
632
|
|
|
625
633
|
#itemPosition(item) {
|
|
@@ -1456,6 +1464,35 @@ function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
|
|
|
1456
1464
|
&& previousNode instanceof CustomActionTextAttachmentNode
|
|
1457
1465
|
}
|
|
1458
1466
|
|
|
1467
|
+
// Shared, strictly-contained element used to attach ephemeral nodes when we
|
|
1468
|
+
// need to read computed styles (e.g. canonicalizing style values, resolving
|
|
1469
|
+
// CSS custom properties). The container is created once and attached to
|
|
1470
|
+
// `document.body` once; subsequent child mutations happen *inside* the
|
|
1471
|
+
// contained subtree so they do not invalidate style on the rest of the page.
|
|
1472
|
+
//
|
|
1473
|
+
// Without this, `document.body.appendChild(...)` / `element.remove()` calls
|
|
1474
|
+
// forced the browser to re-evaluate every ancestor-dependent selector (`:has()`,
|
|
1475
|
+
// descendant combinators, universal sibling rules) across the document on each
|
|
1476
|
+
// invocation — a 13,000+ element style recalc per call on a typical Basecamp
|
|
1477
|
+
// page.
|
|
1478
|
+
|
|
1479
|
+
let resolverRoot = null;
|
|
1480
|
+
|
|
1481
|
+
function styleResolverRoot() {
|
|
1482
|
+
if (resolverRoot && resolverRoot.isConnected) return resolverRoot
|
|
1483
|
+
|
|
1484
|
+
resolverRoot = document.createElement("div");
|
|
1485
|
+
resolverRoot.setAttribute("aria-hidden", "true");
|
|
1486
|
+
resolverRoot.setAttribute("data-lexxy-style-resolver", "");
|
|
1487
|
+
// `contain: strict` (size, layout, paint, style) isolates everything.
|
|
1488
|
+
// The root itself paints nothing (visibility hidden), has zero
|
|
1489
|
+
// geometric impact (position fixed, intrinsic size via contain), and
|
|
1490
|
+
// never leaks style invalidation to its ancestors.
|
|
1491
|
+
resolverRoot.style.cssText = "contain: strict; position: fixed; top: 0; left: 0; visibility: hidden; pointer-events: none; width: 0; height: 0;";
|
|
1492
|
+
document.body.appendChild(resolverRoot);
|
|
1493
|
+
return resolverRoot
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1459
1496
|
function isSelectionHighlighted(selection) {
|
|
1460
1497
|
if (!$isRangeSelection(selection)) return false
|
|
1461
1498
|
|
|
@@ -1525,26 +1562,39 @@ class StyleCanonicalizer {
|
|
|
1525
1562
|
|
|
1526
1563
|
#resolveCannonicalValue(value) {
|
|
1527
1564
|
let index = this.#computedAllowedValues.indexOf(value);
|
|
1528
|
-
index
|
|
1565
|
+
if (index === -1) {
|
|
1566
|
+
index = this.#computedAllowedValues.indexOf(computeStyleValues(this._property, [ value ])[0]);
|
|
1567
|
+
}
|
|
1529
1568
|
return index === -1 ? null : this._allowedValues[index]
|
|
1530
1569
|
}
|
|
1531
1570
|
|
|
1532
1571
|
get #computedAllowedValues() {
|
|
1533
|
-
return this._computedAllowedValues ||= this._allowedValues
|
|
1534
|
-
value => getComputedStyleForProperty(this._property, value)
|
|
1535
|
-
)
|
|
1572
|
+
return this._computedAllowedValues ||= computeStyleValues(this._property, this._allowedValues)
|
|
1536
1573
|
}
|
|
1537
1574
|
}
|
|
1538
1575
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1576
|
+
// Separates DOM writes from layout reads to avoid forced reflows, and attaches
|
|
1577
|
+
// resolver elements to a strictly-contained root (outside the normal document
|
|
1578
|
+
// flow) so neither the attach nor the detach invalidate styles on the rest of
|
|
1579
|
+
// the page. Without containment, appending to `document.body` triggered a
|
|
1580
|
+
// page-wide style recalc on every canonicalization pass.
|
|
1581
|
+
function computeStyleValues(property, values) {
|
|
1582
|
+
const fragment = document.createDocumentFragment();
|
|
1583
|
+
|
|
1584
|
+
const elements = values.map(value => {
|
|
1585
|
+
const element = createElement("span", { style: `display: none; ${property}: ${value};` });
|
|
1586
|
+
fragment.appendChild(element);
|
|
1587
|
+
return element
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
styleResolverRoot().appendChild(fragment);
|
|
1541
1591
|
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
element.remove();
|
|
1592
|
+
const computed = elements.map(element =>
|
|
1593
|
+
window.getComputedStyle(element).getPropertyValue(property)
|
|
1594
|
+
);
|
|
1546
1595
|
|
|
1547
|
-
|
|
1596
|
+
elements.forEach(element => element.remove());
|
|
1597
|
+
return computed
|
|
1548
1598
|
}
|
|
1549
1599
|
|
|
1550
1600
|
class LexxyExtension {
|
|
@@ -2162,7 +2212,9 @@ class CommandDispatcher {
|
|
|
2162
2212
|
const selection = $getSelection();
|
|
2163
2213
|
if (!$isRangeSelection(selection)) return
|
|
2164
2214
|
|
|
2165
|
-
|
|
2215
|
+
const anchorNode = selection.anchor.getNode();
|
|
2216
|
+
|
|
2217
|
+
if (selection.isCollapsed() && !$getNearestNodeOfType(anchorNode, LinkNode)) {
|
|
2166
2218
|
const autoLinkNode = $createAutoLinkNode(url);
|
|
2167
2219
|
const textNode = $createTextNode(url);
|
|
2168
2220
|
autoLinkNode.append(textNode);
|
|
@@ -6478,6 +6530,10 @@ class FormatEscapeExtension extends LexxyExtension {
|
|
|
6478
6530
|
return this.editorElement.supportsRichText
|
|
6479
6531
|
}
|
|
6480
6532
|
|
|
6533
|
+
get allowedElements() {
|
|
6534
|
+
return [ { tag: "li", attributes: [ "value" ] } ]
|
|
6535
|
+
}
|
|
6536
|
+
|
|
6481
6537
|
get lexicalExtension() {
|
|
6482
6538
|
return defineExtension({
|
|
6483
6539
|
name: "lexxy/format-escape",
|
|
@@ -6551,6 +6607,65 @@ function $handleArrowDownInCodeBlock(event) {
|
|
|
6551
6607
|
return false
|
|
6552
6608
|
}
|
|
6553
6609
|
|
|
6610
|
+
class LinkOpenerExtension extends LexxyExtension {
|
|
6611
|
+
get enabled() {
|
|
6612
|
+
return this.editorElement.supportsRichText
|
|
6613
|
+
}
|
|
6614
|
+
|
|
6615
|
+
get lexicalExtension() {
|
|
6616
|
+
return defineExtension({
|
|
6617
|
+
name: "lexxy/link-opener",
|
|
6618
|
+
register: () => {
|
|
6619
|
+
return mergeRegister(
|
|
6620
|
+
registerEventListener(window, "keydown", this.#update.bind(this)),
|
|
6621
|
+
registerEventListener(window, "keyup", this.#update.bind(this)),
|
|
6622
|
+
registerEventListener(window, "blur", this.#disable.bind(this)),
|
|
6623
|
+
registerEventListener(window, "focus", this.#refresh.bind(this))
|
|
6624
|
+
)
|
|
6625
|
+
}
|
|
6626
|
+
})
|
|
6627
|
+
}
|
|
6628
|
+
|
|
6629
|
+
#update(event) {
|
|
6630
|
+
if (this.#isModified(event)) {
|
|
6631
|
+
this.#enable();
|
|
6632
|
+
} else {
|
|
6633
|
+
this.#disable();
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
|
|
6637
|
+
#refresh() {
|
|
6638
|
+
// Chrome dispatches events without modifier keys *for a while* after changing tabs
|
|
6639
|
+
setTimeout(() => {
|
|
6640
|
+
window.addEventListener("mousemove", this.#update.bind(this), { once: true });
|
|
6641
|
+
}, 200);
|
|
6642
|
+
}
|
|
6643
|
+
|
|
6644
|
+
#isModified(event) {
|
|
6645
|
+
return IS_APPLE ? event.metaKey : event.ctrlKey
|
|
6646
|
+
}
|
|
6647
|
+
|
|
6648
|
+
#enable() {
|
|
6649
|
+
for (const anchor of this.#anchors) {
|
|
6650
|
+
anchor.setAttribute("contenteditable", "false");
|
|
6651
|
+
anchor.setAttribute("target", "_blank");
|
|
6652
|
+
anchor.setAttribute("rel", "noopener noreferrer");
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
|
|
6656
|
+
#disable() {
|
|
6657
|
+
for (const anchor of this.#anchors) {
|
|
6658
|
+
anchor.removeAttribute("contenteditable");
|
|
6659
|
+
anchor.removeAttribute("target");
|
|
6660
|
+
anchor.removeAttribute("rel");
|
|
6661
|
+
}
|
|
6662
|
+
}
|
|
6663
|
+
|
|
6664
|
+
get #anchors() {
|
|
6665
|
+
return this.editorElement.editorContentElement?.querySelectorAll("a") ?? []
|
|
6666
|
+
}
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6554
6669
|
class LexicalEditorElement extends HTMLElement {
|
|
6555
6670
|
static formAssociated = true
|
|
6556
6671
|
static debug = false
|
|
@@ -6559,6 +6674,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6559
6674
|
static observedAttributes = [ "connected", "required" ]
|
|
6560
6675
|
|
|
6561
6676
|
#initialValue = ""
|
|
6677
|
+
#initialValueLoaded = false
|
|
6562
6678
|
#validationTextArea = document.createElement("textarea")
|
|
6563
6679
|
#editorInitializedRafId = null
|
|
6564
6680
|
#listeners = new ListenerBin()
|
|
@@ -6655,7 +6771,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6655
6771
|
TrixContentExtension,
|
|
6656
6772
|
TablesExtension,
|
|
6657
6773
|
AttachmentsExtension,
|
|
6658
|
-
FormatEscapeExtension
|
|
6774
|
+
FormatEscapeExtension,
|
|
6775
|
+
LinkOpenerExtension
|
|
6659
6776
|
]
|
|
6660
6777
|
}
|
|
6661
6778
|
|
|
@@ -6735,9 +6852,19 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6735
6852
|
}
|
|
6736
6853
|
|
|
6737
6854
|
focus() {
|
|
6855
|
+
// `editor.focus()` commits a reconciler update to position the cursor.
|
|
6856
|
+
// Skip if the contenteditable already owns focus — the update would be a
|
|
6857
|
+
// no-op but still triggers a full style/layout pass on pages with large
|
|
6858
|
+
// DOMs.
|
|
6859
|
+
if (this.#isContentFocused) return
|
|
6860
|
+
|
|
6738
6861
|
this.editor.focus(() => this.#onFocus());
|
|
6739
6862
|
}
|
|
6740
6863
|
|
|
6864
|
+
get #isContentFocused() {
|
|
6865
|
+
return !!this.editorContentElement && this.editorContentElement.contains(document.activeElement)
|
|
6866
|
+
}
|
|
6867
|
+
|
|
6741
6868
|
get value() {
|
|
6742
6869
|
if (!this.cachedValue) {
|
|
6743
6870
|
this.editor?.getEditorState().read(() => {
|
|
@@ -6749,6 +6876,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6749
6876
|
}
|
|
6750
6877
|
|
|
6751
6878
|
set value(html) {
|
|
6879
|
+
const wasEmpty = !this.#initialValueLoaded;
|
|
6880
|
+
|
|
6752
6881
|
this.editor.update(() => {
|
|
6753
6882
|
$addUpdateTag(SKIP_DOM_SELECTION_TAG);
|
|
6754
6883
|
const root = $getRoot();
|
|
@@ -6758,11 +6887,17 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6758
6887
|
|
|
6759
6888
|
this.#toggleEmptyStatus();
|
|
6760
6889
|
|
|
6761
|
-
// The first time you set the value
|
|
6762
|
-
// in an inconsistent state until
|
|
6763
|
-
// fails because no root node detected.
|
|
6764
|
-
|
|
6890
|
+
// The first time you set the value on an empty editor, Lexical can be
|
|
6891
|
+
// left in an inconsistent state until the next update (adding attachments
|
|
6892
|
+
// fails because no root node is detected). A no-op update works around
|
|
6893
|
+
// it. Only fire on the first load — subsequent set value calls don't hit
|
|
6894
|
+
// the inconsistent state and the extra reconciler cycle is pure overhead.
|
|
6895
|
+
if (wasEmpty) {
|
|
6896
|
+
requestAnimationFrame(() => this.editor?.update(() => { }));
|
|
6897
|
+
}
|
|
6765
6898
|
});
|
|
6899
|
+
|
|
6900
|
+
this.#initialValueLoaded = true;
|
|
6766
6901
|
}
|
|
6767
6902
|
|
|
6768
6903
|
#parseHtmlIntoLexicalNodes(html) {
|
|
@@ -6905,7 +7040,9 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6905
7040
|
}
|
|
6906
7041
|
|
|
6907
7042
|
#handleTurboBeforeCache = (event) => {
|
|
6908
|
-
this
|
|
7043
|
+
if (!this.closest("[data-turbo-permanent]")) {
|
|
7044
|
+
this.#reset();
|
|
7045
|
+
}
|
|
6909
7046
|
}
|
|
6910
7047
|
|
|
6911
7048
|
#synchronizeWithChanges() {
|
|
@@ -7178,19 +7315,30 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7178
7315
|
]
|
|
7179
7316
|
}
|
|
7180
7317
|
|
|
7318
|
+
// Builds one resolver element per CSS value inside a hidden container, attaches
|
|
7319
|
+
// the container in a single DOM write, then reads all computed values in one pass
|
|
7320
|
+
// — triggering at most one forced reflow. The previous implementation interleaved
|
|
7321
|
+
// setProperty/getComputedStyle/removeProperty on the same element, forcing a style
|
|
7322
|
+
// recalc on every iteration during editor initialization.
|
|
7181
7323
|
#resolveColors(property, cssValues) {
|
|
7182
|
-
const
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
return { name: cssValue, value }
|
|
7324
|
+
const container = document.createElement("span");
|
|
7325
|
+
container.style.display = "none";
|
|
7326
|
+
|
|
7327
|
+
const resolvers = cssValues.map(cssValue => {
|
|
7328
|
+
const element = document.createElement("span");
|
|
7329
|
+
element.style.setProperty(property, cssValue);
|
|
7330
|
+
container.appendChild(element);
|
|
7331
|
+
return { element, name: cssValue }
|
|
7191
7332
|
});
|
|
7192
7333
|
|
|
7193
|
-
|
|
7334
|
+
styleResolverRoot().appendChild(container);
|
|
7335
|
+
|
|
7336
|
+
const resolved = resolvers.map(({ element, name }) => ({
|
|
7337
|
+
name,
|
|
7338
|
+
value: window.getComputedStyle(element).getPropertyValue(property)
|
|
7339
|
+
}));
|
|
7340
|
+
|
|
7341
|
+
container.remove();
|
|
7194
7342
|
return resolved
|
|
7195
7343
|
}
|
|
7196
7344
|
|
|
@@ -82,6 +82,10 @@ function extractPlainTextFromHtml(innerHtml = "") {
|
|
|
82
82
|
return parseHtml(innerHtml).body.textContent.trim()
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function isActiveAndVisible(element) {
|
|
86
|
+
return element && !element.disabled && element.checkVisibility()
|
|
87
|
+
}
|
|
88
|
+
|
|
85
89
|
function highlightCode() {
|
|
86
90
|
const elements = document.querySelectorAll("pre[data-language]");
|
|
87
91
|
|
|
@@ -216,4 +220,4 @@ function collectTextNodes(root) {
|
|
|
216
220
|
return nodes
|
|
217
221
|
}
|
|
218
222
|
|
|
219
|
-
export { addBlockSpacing, createAttachmentFigure, createElement, dispatch, extractPlainTextFromHtml, generateDomId, highlightCode, isPreviewableImage, parseHtml };
|
|
223
|
+
export { addBlockSpacing, createAttachmentFigure, createElement, dispatch, extractPlainTextFromHtml, generateDomId, highlightCode, isActiveAndVisible, isPreviewableImage, parseHtml };
|
|
@@ -399,6 +399,13 @@
|
|
|
399
399
|
min-block-size: var(--lexxy-editor-rows);
|
|
400
400
|
outline: 0;
|
|
401
401
|
padding: var(--lexxy-editor-padding);
|
|
402
|
+
|
|
403
|
+
/* Isolate the contenteditable root's layout and style. Lexical's reconciler
|
|
404
|
+
commits mutations inside this element (nodes appended, text inserted,
|
|
405
|
+
class flipped) on every update; containment keeps those mutations from
|
|
406
|
+
invalidating ancestor-dependent selectors and sibling layout elsewhere
|
|
407
|
+
in the editor. */
|
|
408
|
+
contain: layout style;
|
|
402
409
|
}
|
|
403
410
|
|
|
404
411
|
:where(.lexxy-editor--drag-over) {
|