@37signals/lexxy 0.9.9-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 +59 -10
- package/dist/stylesheets/lexxy-editor.css +7 -0
- package/package.json +1 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -1464,6 +1464,35 @@ function isAttachmentSpacerTextNode(node, previousNode, index, childCount) {
|
|
|
1464
1464
|
&& previousNode instanceof CustomActionTextAttachmentNode
|
|
1465
1465
|
}
|
|
1466
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
|
+
|
|
1467
1496
|
function isSelectionHighlighted(selection) {
|
|
1468
1497
|
if (!$isRangeSelection(selection)) return false
|
|
1469
1498
|
|
|
@@ -1544,10 +1573,11 @@ class StyleCanonicalizer {
|
|
|
1544
1573
|
}
|
|
1545
1574
|
}
|
|
1546
1575
|
|
|
1547
|
-
// Separates DOM writes from layout reads to avoid forced reflows
|
|
1548
|
-
// elements
|
|
1549
|
-
//
|
|
1550
|
-
//
|
|
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.
|
|
1551
1581
|
function computeStyleValues(property, values) {
|
|
1552
1582
|
const fragment = document.createDocumentFragment();
|
|
1553
1583
|
|
|
@@ -1557,7 +1587,7 @@ function computeStyleValues(property, values) {
|
|
|
1557
1587
|
return element
|
|
1558
1588
|
});
|
|
1559
1589
|
|
|
1560
|
-
|
|
1590
|
+
styleResolverRoot().appendChild(fragment);
|
|
1561
1591
|
|
|
1562
1592
|
const computed = elements.map(element =>
|
|
1563
1593
|
window.getComputedStyle(element).getPropertyValue(property)
|
|
@@ -6644,6 +6674,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6644
6674
|
static observedAttributes = [ "connected", "required" ]
|
|
6645
6675
|
|
|
6646
6676
|
#initialValue = ""
|
|
6677
|
+
#initialValueLoaded = false
|
|
6647
6678
|
#validationTextArea = document.createElement("textarea")
|
|
6648
6679
|
#editorInitializedRafId = null
|
|
6649
6680
|
#listeners = new ListenerBin()
|
|
@@ -6821,9 +6852,19 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6821
6852
|
}
|
|
6822
6853
|
|
|
6823
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
|
+
|
|
6824
6861
|
this.editor.focus(() => this.#onFocus());
|
|
6825
6862
|
}
|
|
6826
6863
|
|
|
6864
|
+
get #isContentFocused() {
|
|
6865
|
+
return !!this.editorContentElement && this.editorContentElement.contains(document.activeElement)
|
|
6866
|
+
}
|
|
6867
|
+
|
|
6827
6868
|
get value() {
|
|
6828
6869
|
if (!this.cachedValue) {
|
|
6829
6870
|
this.editor?.getEditorState().read(() => {
|
|
@@ -6835,6 +6876,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6835
6876
|
}
|
|
6836
6877
|
|
|
6837
6878
|
set value(html) {
|
|
6879
|
+
const wasEmpty = !this.#initialValueLoaded;
|
|
6880
|
+
|
|
6838
6881
|
this.editor.update(() => {
|
|
6839
6882
|
$addUpdateTag(SKIP_DOM_SELECTION_TAG);
|
|
6840
6883
|
const root = $getRoot();
|
|
@@ -6844,11 +6887,17 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
6844
6887
|
|
|
6845
6888
|
this.#toggleEmptyStatus();
|
|
6846
6889
|
|
|
6847
|
-
// The first time you set the value
|
|
6848
|
-
// in an inconsistent state until
|
|
6849
|
-
// fails because no root node detected.
|
|
6850
|
-
|
|
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
|
+
}
|
|
6851
6898
|
});
|
|
6899
|
+
|
|
6900
|
+
this.#initialValueLoaded = true;
|
|
6852
6901
|
}
|
|
6853
6902
|
|
|
6854
6903
|
#parseHtmlIntoLexicalNodes(html) {
|
|
@@ -7282,7 +7331,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
7282
7331
|
return { element, name: cssValue }
|
|
7283
7332
|
});
|
|
7284
7333
|
|
|
7285
|
-
|
|
7334
|
+
styleResolverRoot().appendChild(container);
|
|
7286
7335
|
|
|
7287
7336
|
const resolved = resolvers.map(({ element, name }) => ({
|
|
7288
7337
|
name,
|
|
@@ -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) {
|