@domternal/vanilla 0.13.0 → 0.15.0
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/index.d.ts +13 -8
- package/dist/index.js +152 -19
- package/dist/index.js.map +1 -1
- package/package.json +10 -12
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PluginKey, IconSet, Editor, AnyExtension, Content, FocusPosition, JSONContent, ToolbarController, ToolbarLayoutEntry, ToolbarButton, ToolbarDropdown, BubbleMenuOptions, FloatingMenuController, FloatingMenuOptions, FloatingMenuItemsOverride, FloatingMenuKeymap } from '@domternal/core';
|
|
1
|
+
import { PluginKey, IconSet, Editor, AnyExtension, Content, EditorPreset, FocusPosition, JSONContent, ToolbarController, ToolbarLayoutEntry, ToolbarButton, ToolbarDropdown, BubbleMenuOptions, FloatingMenuController, FloatingMenuOptions, FloatingMenuItemsOverride, FloatingMenuKeymap } from '@domternal/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* SSR-safe environment check.
|
|
@@ -200,6 +200,12 @@ interface DomternalEditorOptions {
|
|
|
200
200
|
content?: Content;
|
|
201
201
|
/** Whether the editor is editable. @default true */
|
|
202
202
|
editable?: boolean;
|
|
203
|
+
/**
|
|
204
|
+
* Editing experience preset. `'notion'` paints `dm-notion-mode` on the
|
|
205
|
+
* `.dm-editor` host and switches preset-aware extensions to their Notion
|
|
206
|
+
* behavior, replacing the hand-written class. Create-time only.
|
|
207
|
+
*/
|
|
208
|
+
preset?: EditorPreset;
|
|
203
209
|
/** Where to autofocus on mount. @default false */
|
|
204
210
|
autofocus?: FocusPosition;
|
|
205
211
|
/**
|
|
@@ -584,14 +590,13 @@ interface DomternalBubbleMenuOptions extends CustomContentOption {
|
|
|
584
590
|
* "..." for block context menu) are rendered automatically when the
|
|
585
591
|
* corresponding extensions are loaded.
|
|
586
592
|
*
|
|
587
|
-
* Re-renders are batched via `requestAnimationFrame`.
|
|
588
|
-
*
|
|
589
|
-
*
|
|
593
|
+
* Re-renders are batched via `requestAnimationFrame`. The structure is rebuilt
|
|
594
|
+
* only when the item list changes; a state-only render updates the nodes on
|
|
595
|
+
* screen, so a press that outlives a transaction still produces a click.
|
|
590
596
|
*
|
|
591
|
-
* **Stable identity convention.**
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
* (e.g. as a popover anchor) should:
|
|
597
|
+
* **Stable identity convention.** A trigger survives a state-only render but
|
|
598
|
+
* NOT an item-list change, so consumers holding a reference to one (e.g. as a
|
|
599
|
+
* popover anchor) should still:
|
|
595
600
|
* - Either listen for `selectionUpdate` / `transaction` and re-resolve the
|
|
596
601
|
* anchor via `host.querySelector('.dm-ncp-trigger')` on demand
|
|
597
602
|
* - Or match the trigger by class + container (`closest('.dm-bubble-menu')`)
|
package/dist/index.js
CHANGED
|
@@ -82,7 +82,8 @@ var DomternalEditor = class extends EventTarget {
|
|
|
82
82
|
extensions: [...defaults, ...options.extensions ?? []],
|
|
83
83
|
content: options.content ?? "",
|
|
84
84
|
editable: options.editable ?? true,
|
|
85
|
-
autofocus: options.autofocus ?? false
|
|
85
|
+
autofocus: options.autofocus ?? false,
|
|
86
|
+
...options.preset ? { preset: options.preset } : {}
|
|
86
87
|
});
|
|
87
88
|
this.#wireEditorEvents();
|
|
88
89
|
this.#onCreate?.(this.editor);
|
|
@@ -359,6 +360,8 @@ var DomternalToolbar = class extends EventTarget {
|
|
|
359
360
|
#lastGroupsRef = null;
|
|
360
361
|
/** Maps top-level button/dropdown name -> rendered trigger element. */
|
|
361
362
|
#buttonEls = /* @__PURE__ */ new Map();
|
|
363
|
+
/** Trigger markup last written, so a re-render does not rewrite it blindly. */
|
|
364
|
+
#triggerHtml = /* @__PURE__ */ new WeakMap();
|
|
362
365
|
/** Rendered dropdown panel elements (created lazily when opened). */
|
|
363
366
|
#dropdownPanelEl = null;
|
|
364
367
|
#cleanupFloating = null;
|
|
@@ -452,6 +455,17 @@ var DomternalToolbar = class extends EventTarget {
|
|
|
452
455
|
},
|
|
453
456
|
{ signal }
|
|
454
457
|
);
|
|
458
|
+
document.addEventListener(
|
|
459
|
+
"keydown",
|
|
460
|
+
(e) => {
|
|
461
|
+
if (!this.#controller.openDropdown) return;
|
|
462
|
+
if (e.key !== "Escape") return;
|
|
463
|
+
if (this.host.contains(document.activeElement)) return;
|
|
464
|
+
this.closeDropdown();
|
|
465
|
+
e.preventDefault();
|
|
466
|
+
},
|
|
467
|
+
{ signal }
|
|
468
|
+
);
|
|
455
469
|
this.host.addEventListener(
|
|
456
470
|
"keydown",
|
|
457
471
|
(e) => {
|
|
@@ -549,7 +563,9 @@ var DomternalToolbar = class extends EventTarget {
|
|
|
549
563
|
btn.setAttribute("aria-label", dd.label);
|
|
550
564
|
btn.title = dd.label;
|
|
551
565
|
btn.setAttribute("data-dropdown", dd.name);
|
|
552
|
-
|
|
566
|
+
const triggerHtml = this.#resolveDropdownTriggerHtml(dd);
|
|
567
|
+
this.#triggerHtml.set(btn, triggerHtml);
|
|
568
|
+
btn.innerHTML = triggerHtml;
|
|
553
569
|
btn.addEventListener("mousedown", (e) => {
|
|
554
570
|
e.preventDefault();
|
|
555
571
|
});
|
|
@@ -626,7 +642,8 @@ var DomternalToolbar = class extends EventTarget {
|
|
|
626
642
|
btn.setAttribute("aria-expanded", String(isOpen));
|
|
627
643
|
btn.tabIndex = flat === focusedIndex ? 0 : -1;
|
|
628
644
|
const newHtml = this.#resolveDropdownTriggerHtml(dd);
|
|
629
|
-
if (btn
|
|
645
|
+
if (this.#triggerHtml.get(btn) !== newHtml) {
|
|
646
|
+
this.#triggerHtml.set(btn, newHtml);
|
|
630
647
|
btn.innerHTML = newHtml;
|
|
631
648
|
}
|
|
632
649
|
}
|
|
@@ -1031,7 +1048,8 @@ function computeTrailingState(editor, opts) {
|
|
|
1031
1048
|
}
|
|
1032
1049
|
return {
|
|
1033
1050
|
isNodeSelection: isNode,
|
|
1034
|
-
|
|
1051
|
+
// Hidden without a live notionColorOpen listener: the trigger would be dead.
|
|
1052
|
+
showColorPickerButton: opts.hasNotionColorPicker && editor.listenerCount("notionColorOpen") > 0,
|
|
1035
1053
|
showBlockMenuButton: opts.hasBlockContextMenu,
|
|
1036
1054
|
blockMenuButtonDisabled: blockMenuDisabled,
|
|
1037
1055
|
currentTextColorVar: textVar,
|
|
@@ -1080,6 +1098,15 @@ var DomternalBubbleMenu = class extends EventTarget {
|
|
|
1080
1098
|
#activeMap = /* @__PURE__ */ new Map();
|
|
1081
1099
|
#disabledMap = /* @__PURE__ */ new Map();
|
|
1082
1100
|
#trailing = { ...INITIAL_TRAILING_STATE };
|
|
1101
|
+
// Structure rebuilt only on an item-list change, so a pressed button lives.
|
|
1102
|
+
#structureKey = null;
|
|
1103
|
+
#buttonEls = /* @__PURE__ */ new Map();
|
|
1104
|
+
// Icon markup last WRITTEN. `btn.innerHTML` returns the browser's
|
|
1105
|
+
// re-serialisation, so comparing against it rewrites every render and
|
|
1106
|
+
// replaces the glyph under the pointer. Keyed by element: names repeat.
|
|
1107
|
+
#iconHtml = /* @__PURE__ */ new WeakMap();
|
|
1108
|
+
#colorTriggerEl = null;
|
|
1109
|
+
#blockMenuTriggerEl = null;
|
|
1083
1110
|
// Dropdown state (text-align dropdown inside bubble menu)
|
|
1084
1111
|
#openDropdown = null;
|
|
1085
1112
|
#dropdownPanelEl = null;
|
|
@@ -1186,6 +1213,7 @@ var DomternalBubbleMenu = class extends EventTarget {
|
|
|
1186
1213
|
setIcons(icons) {
|
|
1187
1214
|
if (this.#destroyed) return;
|
|
1188
1215
|
this.#icons = icons;
|
|
1216
|
+
this.#structureKey = null;
|
|
1189
1217
|
this.#scheduleRender();
|
|
1190
1218
|
}
|
|
1191
1219
|
/** Close any open dropdown (text-align). No-op if nothing is open. */
|
|
@@ -1362,19 +1390,51 @@ var DomternalBubbleMenu = class extends EventTarget {
|
|
|
1362
1390
|
this.#render();
|
|
1363
1391
|
});
|
|
1364
1392
|
}
|
|
1393
|
+
/** Signature of what decides which nodes exist; equal keys reuse the DOM. */
|
|
1394
|
+
#computeStructureKey() {
|
|
1395
|
+
const items = this.#resolvedItems.map(
|
|
1396
|
+
(item) => item.type === "dropdown" ? `${item.type}:${item.name}:${item.items.map((sub) => sub.name).join(",")}` : `${item.type}:${item.name}`
|
|
1397
|
+
).join("|");
|
|
1398
|
+
const t = this.#trailing;
|
|
1399
|
+
return [
|
|
1400
|
+
items,
|
|
1401
|
+
t.showColorPickerButton && !t.isNodeSelection ? "color" : "",
|
|
1402
|
+
t.showBlockMenuButton && !t.isNodeSelection ? "block" : "",
|
|
1403
|
+
this.#customContent ? "custom" : ""
|
|
1404
|
+
].join("#");
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Runs on every transaction, and pointer motion alone dispatches those. A
|
|
1408
|
+
* render lands a frame late, so rebuilding wholesale can replace a button
|
|
1409
|
+
* between mousedown and mouseup: the two events share no element and the
|
|
1410
|
+
* browser fires no click at all. Structure only on an item-list change,
|
|
1411
|
+
* state in place otherwise, as the toolbar does.
|
|
1412
|
+
*/
|
|
1365
1413
|
#render() {
|
|
1366
|
-
this.host.replaceChildren();
|
|
1367
|
-
this.#cleanupDropdownFloating?.();
|
|
1368
|
-
this.#cleanupDropdownFloating = null;
|
|
1369
|
-
this.#dropdownAbortCtl?.abort();
|
|
1370
|
-
this.#dropdownAbortCtl = null;
|
|
1371
|
-
this.#dropdownPanelEl = null;
|
|
1372
1414
|
if (this.#openDropdown !== null) {
|
|
1373
1415
|
const stillExists = this.#resolvedItems.some(
|
|
1374
1416
|
(item) => item.type === "dropdown" && item.name === this.#openDropdown
|
|
1375
1417
|
);
|
|
1376
|
-
if (!stillExists)
|
|
1418
|
+
if (!stillExists) {
|
|
1419
|
+
this.#openDropdown = null;
|
|
1420
|
+
this.#detachDropdown();
|
|
1421
|
+
}
|
|
1377
1422
|
}
|
|
1423
|
+
const key = this.#computeStructureKey();
|
|
1424
|
+
if (key === this.#structureKey) {
|
|
1425
|
+
this.#updateButtons();
|
|
1426
|
+
} else {
|
|
1427
|
+
this.#renderStructure();
|
|
1428
|
+
this.#structureKey = key;
|
|
1429
|
+
}
|
|
1430
|
+
this.#syncDropdownPanel();
|
|
1431
|
+
}
|
|
1432
|
+
#renderStructure() {
|
|
1433
|
+
this.host.replaceChildren();
|
|
1434
|
+
this.#buttonEls.clear();
|
|
1435
|
+
this.#colorTriggerEl = null;
|
|
1436
|
+
this.#blockMenuTriggerEl = null;
|
|
1437
|
+
this.#detachDropdown();
|
|
1378
1438
|
for (const item of this.#resolvedItems) {
|
|
1379
1439
|
if (item.type === "separator") {
|
|
1380
1440
|
this.host.appendChild(this.#createSeparator(item.name));
|
|
@@ -1415,13 +1475,14 @@ var DomternalBubbleMenu = class extends EventTarget {
|
|
|
1415
1475
|
btn.setAttribute("aria-label", item.label);
|
|
1416
1476
|
btn.setAttribute("aria-pressed", String(isActive));
|
|
1417
1477
|
btn.title = item.label;
|
|
1418
|
-
btn
|
|
1478
|
+
this.#setIconHtml(btn, resolveIcon(item.icon, this.#icons));
|
|
1419
1479
|
btn.addEventListener("mousedown", (e) => {
|
|
1420
1480
|
e.preventDefault();
|
|
1421
1481
|
});
|
|
1422
1482
|
btn.addEventListener("click", (e) => {
|
|
1423
1483
|
this.#onButtonClick(item, e);
|
|
1424
1484
|
});
|
|
1485
|
+
this.#buttonEls.set(item.name, btn);
|
|
1425
1486
|
return btn;
|
|
1426
1487
|
}
|
|
1427
1488
|
#createDropdownTrigger(dd) {
|
|
@@ -1441,7 +1502,7 @@ var DomternalBubbleMenu = class extends EventTarget {
|
|
|
1441
1502
|
trigger.setAttribute("aria-label", dd.label);
|
|
1442
1503
|
trigger.title = dd.label;
|
|
1443
1504
|
trigger.dataset["dropdown"] = dd.name;
|
|
1444
|
-
trigger
|
|
1505
|
+
this.#setIconHtml(trigger, triggerHtml);
|
|
1445
1506
|
trigger.addEventListener("mousedown", (e) => {
|
|
1446
1507
|
e.preventDefault();
|
|
1447
1508
|
});
|
|
@@ -1449,12 +1510,7 @@ var DomternalBubbleMenu = class extends EventTarget {
|
|
|
1449
1510
|
this.#onDropdownToggle(dd);
|
|
1450
1511
|
});
|
|
1451
1512
|
wrapper.appendChild(trigger);
|
|
1452
|
-
|
|
1453
|
-
const panel = this.#createDropdownPanel(dd);
|
|
1454
|
-
wrapper.appendChild(panel);
|
|
1455
|
-
this.#dropdownPanelEl = panel;
|
|
1456
|
-
this.#attachDropdownListeners(trigger, panel);
|
|
1457
|
-
}
|
|
1513
|
+
this.#buttonEls.set(dd.name, trigger);
|
|
1458
1514
|
return wrapper;
|
|
1459
1515
|
}
|
|
1460
1516
|
#createDropdownPanel(dd) {
|
|
@@ -1472,6 +1528,7 @@ var DomternalBubbleMenu = class extends EventTarget {
|
|
|
1472
1528
|
if (subActive) subBtn.classList.add("dm-toolbar-dropdown-item--active");
|
|
1473
1529
|
subBtn.setAttribute("role", "menuitem");
|
|
1474
1530
|
subBtn.setAttribute("aria-label", sub.label);
|
|
1531
|
+
subBtn.dataset["dropdownItem"] = sub.name;
|
|
1475
1532
|
subBtn.innerHTML = subHtml;
|
|
1476
1533
|
subBtn.addEventListener("mousedown", (e) => {
|
|
1477
1534
|
e.preventDefault();
|
|
@@ -1507,6 +1564,7 @@ var DomternalBubbleMenu = class extends EventTarget {
|
|
|
1507
1564
|
btn.addEventListener("click", () => {
|
|
1508
1565
|
this.openColorPicker(btn);
|
|
1509
1566
|
});
|
|
1567
|
+
this.#colorTriggerEl = btn;
|
|
1510
1568
|
return btn;
|
|
1511
1569
|
}
|
|
1512
1570
|
#createBlockMenuTrigger() {
|
|
@@ -1525,8 +1583,83 @@ var DomternalBubbleMenu = class extends EventTarget {
|
|
|
1525
1583
|
btn.addEventListener("click", () => {
|
|
1526
1584
|
this.openBlockContextMenu(btn);
|
|
1527
1585
|
});
|
|
1586
|
+
this.#blockMenuTriggerEl = btn;
|
|
1528
1587
|
return btn;
|
|
1529
1588
|
}
|
|
1589
|
+
// === In-place updates (the common render) ===
|
|
1590
|
+
#updateButtons() {
|
|
1591
|
+
for (const item of this.#resolvedItems) {
|
|
1592
|
+
if (item.type === "separator") continue;
|
|
1593
|
+
const el = this.#buttonEls.get(item.name);
|
|
1594
|
+
if (!el) continue;
|
|
1595
|
+
if (item.type === "dropdown") this.#updateDropdownTrigger(item, el);
|
|
1596
|
+
else this.#updateButton(item, el);
|
|
1597
|
+
}
|
|
1598
|
+
const t = this.#trailing;
|
|
1599
|
+
if (this.#colorTriggerEl) {
|
|
1600
|
+
this.#colorTriggerEl.classList.toggle("dm-toolbar-button--active", t.hasAnyColor);
|
|
1601
|
+
const glyph = this.#colorTriggerEl.querySelector(".dm-ncp-trigger-glyph");
|
|
1602
|
+
if (glyph) glyph.style.color = t.currentTextColorVar ?? "";
|
|
1603
|
+
const underline = this.#colorTriggerEl.querySelector(".dm-ncp-trigger-underline");
|
|
1604
|
+
if (underline) underline.style.backgroundColor = t.currentBgColorVar ?? "";
|
|
1605
|
+
}
|
|
1606
|
+
if (this.#blockMenuTriggerEl) {
|
|
1607
|
+
this.#blockMenuTriggerEl.disabled = t.blockMenuButtonDisabled;
|
|
1608
|
+
this.#blockMenuTriggerEl.title = t.blockMenuButtonDisabled ? "Block actions (select within a single block)" : "More options";
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
#updateButton(item, btn) {
|
|
1612
|
+
const isActive = this.#activeMap.get(item.name) ?? false;
|
|
1613
|
+
const isDisabled = this.#disabledMap.get(item.name) ?? false;
|
|
1614
|
+
btn.classList.toggle("dm-toolbar-button--active", isActive);
|
|
1615
|
+
btn.disabled = isDisabled;
|
|
1616
|
+
btn.setAttribute("aria-pressed", String(isActive));
|
|
1617
|
+
this.#setIconHtml(btn, resolveIcon(item.icon, this.#icons));
|
|
1618
|
+
}
|
|
1619
|
+
/** Writes icon markup only when it differs from what was written last. */
|
|
1620
|
+
#setIconHtml(el, html) {
|
|
1621
|
+
if (this.#iconHtml.get(el) === html) return;
|
|
1622
|
+
this.#iconHtml.set(el, html);
|
|
1623
|
+
el.innerHTML = html;
|
|
1624
|
+
}
|
|
1625
|
+
#updateDropdownTrigger(dd, trigger) {
|
|
1626
|
+
const dropdownActive = dd.items.some((sub) => this.#activeMap.get(sub.name) ?? false);
|
|
1627
|
+
const activeChild = dd.dynamicIcon ? dd.items.find((sub) => this.#activeMap.get(sub.name) ?? false) : void 0;
|
|
1628
|
+
const html = resolveIcon(activeChild?.icon ?? dd.icon, this.#icons) + DROPDOWN_CARET2;
|
|
1629
|
+
trigger.classList.toggle("dm-toolbar-button--active", dropdownActive);
|
|
1630
|
+
trigger.setAttribute("aria-expanded", String(this.#openDropdown === dd.name));
|
|
1631
|
+
this.#setIconHtml(trigger, html);
|
|
1632
|
+
}
|
|
1633
|
+
/** Builds the open dropdown's panel, or refreshes the marks on an open one. */
|
|
1634
|
+
#syncDropdownPanel() {
|
|
1635
|
+
const open = this.#openDropdown;
|
|
1636
|
+
if (open === null) {
|
|
1637
|
+
if (this.#dropdownPanelEl) this.#detachDropdown();
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
const dd = this.#resolvedItems.find(
|
|
1641
|
+
(item) => item.type === "dropdown" && item.name === open
|
|
1642
|
+
);
|
|
1643
|
+
if (!dd) return;
|
|
1644
|
+
const panel = this.#dropdownPanelEl;
|
|
1645
|
+
if (panel?.isConnected === true) {
|
|
1646
|
+
for (const sub of dd.items) {
|
|
1647
|
+
const subBtn = panel.querySelector(`[data-dropdown-item="${sub.name}"]`);
|
|
1648
|
+
subBtn?.classList.toggle(
|
|
1649
|
+
"dm-toolbar-dropdown-item--active",
|
|
1650
|
+
this.#activeMap.get(sub.name) ?? false
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
const trigger = this.#buttonEls.get(open);
|
|
1656
|
+
const wrapper = trigger?.parentElement;
|
|
1657
|
+
if (!trigger || !wrapper) return;
|
|
1658
|
+
const fresh = this.#createDropdownPanel(dd);
|
|
1659
|
+
wrapper.appendChild(fresh);
|
|
1660
|
+
this.#dropdownPanelEl = fresh;
|
|
1661
|
+
this.#attachDropdownListeners(trigger, fresh);
|
|
1662
|
+
}
|
|
1530
1663
|
// === Event handlers ===
|
|
1531
1664
|
#onButtonClick(item, event) {
|
|
1532
1665
|
if (this.#openDropdown) this.closeDropdown();
|