@domternal/vanilla 0.14.0 → 1.0.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.js CHANGED
@@ -1,4 +1,5 @@
1
- import { Document, Paragraph, Text, BaseKeymap, History, PluginKey, defaultIcons, Editor, ToolbarController, positionFloatingOnce, refocusEditorAfterCommand, defaultBubbleContexts, createBubbleMenuPlugin, createFloatingMenuPlugin, FloatingMenuController, positionFloating } from '@domternal/core';
1
+ import { Document, Paragraph, Text, BaseKeymap, History, PluginKey, defaultIcons, Editor, ToolbarController, positionFloatingOnce, refocusEditorAfterCommand, resolveBubbleNames, defaultBubbleContexts, buildBubbleItemMaps, createBubbleMenuPlugin, createBubbleShouldShow, resolveBubbleMenuItems, createFloatingMenuPlugin, FloatingMenuController, positionFloating } from '@domternal/core';
2
+ export { buildBubbleItemMaps as buildItemMaps, detectBubbleContext as detectContext, filterBubbleItemsBySchema as filterBySchema, getBubbleFormatItems as getFormatItems, isInsideTableCell, resolveBubbleNames as resolveNames } from '@domternal/core';
2
3
 
3
4
  // src/shared/isBrowser.ts
4
5
  var isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
@@ -82,7 +83,8 @@ var DomternalEditor = class extends EventTarget {
82
83
  extensions: [...defaults, ...options.extensions ?? []],
83
84
  content: options.content ?? "",
84
85
  editable: options.editable ?? true,
85
- autofocus: options.autofocus ?? false
86
+ autofocus: options.autofocus ?? false,
87
+ ...options.preset ? { preset: options.preset } : {}
86
88
  });
87
89
  this.#wireEditorEvents();
88
90
  this.#onCreate?.(this.editor);
@@ -359,6 +361,8 @@ var DomternalToolbar = class extends EventTarget {
359
361
  #lastGroupsRef = null;
360
362
  /** Maps top-level button/dropdown name -> rendered trigger element. */
361
363
  #buttonEls = /* @__PURE__ */ new Map();
364
+ /** Trigger markup last written, so a re-render does not rewrite it blindly. */
365
+ #triggerHtml = /* @__PURE__ */ new WeakMap();
362
366
  /** Rendered dropdown panel elements (created lazily when opened). */
363
367
  #dropdownPanelEl = null;
364
368
  #cleanupFloating = null;
@@ -452,6 +456,17 @@ var DomternalToolbar = class extends EventTarget {
452
456
  },
453
457
  { signal }
454
458
  );
459
+ document.addEventListener(
460
+ "keydown",
461
+ (e) => {
462
+ if (!this.#controller.openDropdown) return;
463
+ if (e.key !== "Escape") return;
464
+ if (this.host.contains(document.activeElement)) return;
465
+ this.closeDropdown();
466
+ e.preventDefault();
467
+ },
468
+ { signal }
469
+ );
455
470
  this.host.addEventListener(
456
471
  "keydown",
457
472
  (e) => {
@@ -549,7 +564,9 @@ var DomternalToolbar = class extends EventTarget {
549
564
  btn.setAttribute("aria-label", dd.label);
550
565
  btn.title = dd.label;
551
566
  btn.setAttribute("data-dropdown", dd.name);
552
- btn.innerHTML = this.#resolveDropdownTriggerHtml(dd);
567
+ const triggerHtml = this.#resolveDropdownTriggerHtml(dd);
568
+ this.#triggerHtml.set(btn, triggerHtml);
569
+ btn.innerHTML = triggerHtml;
553
570
  btn.addEventListener("mousedown", (e) => {
554
571
  e.preventDefault();
555
572
  });
@@ -626,7 +643,8 @@ var DomternalToolbar = class extends EventTarget {
626
643
  btn.setAttribute("aria-expanded", String(isOpen));
627
644
  btn.tabIndex = flat === focusedIndex ? 0 : -1;
628
645
  const newHtml = this.#resolveDropdownTriggerHtml(dd);
629
- if (btn.innerHTML !== newHtml) {
646
+ if (this.#triggerHtml.get(btn) !== newHtml) {
647
+ this.#triggerHtml.set(btn, newHtml);
630
648
  btn.innerHTML = newHtml;
631
649
  }
632
650
  }
@@ -872,129 +890,6 @@ var DomternalToolbar = class extends EventTarget {
872
890
  }
873
891
  };
874
892
 
875
- // src/bubble-menu/itemResolver.ts
876
- function buildItemMaps(editor) {
877
- const itemMap = /* @__PURE__ */ new Map();
878
- const dropdownMap = /* @__PURE__ */ new Map();
879
- for (const item of editor.toolbarItems) {
880
- if (item.type === "button") {
881
- itemMap.set(item.name, item);
882
- } else if (item.type === "dropdown") {
883
- dropdownMap.set(item.name, item);
884
- for (const sub of item.items) {
885
- itemMap.set(sub.name, sub);
886
- }
887
- }
888
- }
889
- return {
890
- itemMap,
891
- dropdownMap,
892
- bubbleDefaults: buildBubbleDefaults(editor)
893
- };
894
- }
895
- function buildBubbleDefaults(editor) {
896
- const byCtx = /* @__PURE__ */ new Map();
897
- const addItem = (btn) => {
898
- const ctx = btn["bubbleMenu"];
899
- if (!ctx) return;
900
- let arr = byCtx.get(ctx);
901
- if (!arr) {
902
- arr = [];
903
- byCtx.set(ctx, arr);
904
- }
905
- arr.push(btn);
906
- };
907
- for (const item of editor.toolbarItems) {
908
- if (item.type === "button") addItem(item);
909
- else if (item.type === "dropdown") {
910
- for (const sub of item.items) addItem(sub);
911
- }
912
- }
913
- const result = /* @__PURE__ */ new Map();
914
- for (const [ctx, ctxItems] of byCtx) {
915
- ctxItems.sort((a, b) => (b.priority ?? 100) - (a.priority ?? 100));
916
- const list = [];
917
- let lastGroup;
918
- let sepIdx = 0;
919
- for (const item of ctxItems) {
920
- if (lastGroup !== void 0 && item.group !== lastGroup) {
921
- list.push({ type: "separator", name: `bsep-${String(sepIdx++)}` });
922
- }
923
- list.push(item);
924
- lastGroup = item.group;
925
- }
926
- result.set(ctx, list);
927
- }
928
- return result;
929
- }
930
- function resolveNames(names, itemMap, dropdownMap) {
931
- const result = [];
932
- let sepIdx = 0;
933
- for (const name of names) {
934
- if (name === "|") {
935
- result.push({ type: "separator", name: `sep-${String(sepIdx++)}` });
936
- continue;
937
- }
938
- const dropdown = dropdownMap.get(name);
939
- if (dropdown) {
940
- result.push(dropdown);
941
- continue;
942
- }
943
- const item = itemMap.get(name);
944
- if (item) result.push(item);
945
- }
946
- return result;
947
- }
948
- function getFormatItems(itemMap) {
949
- return Array.from(itemMap.values()).filter((item) => item.group === "format").sort((a, b) => (b.priority ?? 100) - (a.priority ?? 100));
950
- }
951
- function detectContext(selection, ctxs) {
952
- if ("$anchorCell" in selection) return null;
953
- if (selection.node) return selection.node.type.name;
954
- if (selection.empty) return null;
955
- const fromCell = findCellNode(selection.$from);
956
- if (fromCell) {
957
- const toCell = findCellNode(selection.$to);
958
- if (toCell && fromCell !== toCell) return null;
959
- return "table";
960
- }
961
- const fromName = selection.$from.parent.type.name;
962
- if (fromName in ctxs) return fromName;
963
- if ("text" in ctxs && selection.$from.parent.type.spec.marks !== "") return "text";
964
- const toName = selection.$to.parent.type.name;
965
- if (toName in ctxs) return toName;
966
- if ("text" in ctxs && selection.$to.parent.type.spec.marks !== "") return "text";
967
- return null;
968
- }
969
- function filterBySchema(editor, contextName, schemaItems) {
970
- if (contextName === "text" || contextName === "table") return schemaItems;
971
- const schema = editor.state.schema;
972
- if (!schema) return schemaItems;
973
- const nodeType = schema.nodes[contextName];
974
- if (!nodeType) return schemaItems;
975
- return schemaItems.filter((item) => {
976
- const markName = typeof item.isActive === "string" ? item.isActive : null;
977
- if (!markName) return true;
978
- const markType = schema.marks[markName];
979
- if (!markType) return true;
980
- return nodeType.allowsMarkType(markType);
981
- });
982
- }
983
- function isInsideTableCell($pos) {
984
- for (let d = $pos.depth; d > 0; d--) {
985
- const name = $pos.node(d).type.name;
986
- if (name === "tableCell" || name === "tableHeader") return true;
987
- }
988
- return false;
989
- }
990
- function findCellNode(pos) {
991
- for (let d = pos.depth; d > 0; d--) {
992
- const node = pos.node(d);
993
- if (node.type.name === "tableCell" || node.type.name === "tableHeader") return node;
994
- }
995
- return null;
996
- }
997
-
998
893
  // src/bubble-menu/trailingState.ts
999
894
  var INITIAL_TRAILING_STATE = {
1000
895
  isNodeSelection: false,
@@ -1031,7 +926,8 @@ function computeTrailingState(editor, opts) {
1031
926
  }
1032
927
  return {
1033
928
  isNodeSelection: isNode,
1034
- showColorPickerButton: opts.hasNotionColorPicker,
929
+ // Hidden without a live notionColorOpen listener: the trigger would be dead.
930
+ showColorPickerButton: opts.hasNotionColorPicker && editor.listenerCount("notionColorOpen") > 0,
1035
931
  showBlockMenuButton: opts.hasBlockContextMenu,
1036
932
  blockMenuButtonDisabled: blockMenuDisabled,
1037
933
  currentTextColorVar: textVar,
@@ -1080,6 +976,15 @@ var DomternalBubbleMenu = class extends EventTarget {
1080
976
  #activeMap = /* @__PURE__ */ new Map();
1081
977
  #disabledMap = /* @__PURE__ */ new Map();
1082
978
  #trailing = { ...INITIAL_TRAILING_STATE };
979
+ // Structure rebuilt only on an item-list change, so a pressed button lives.
980
+ #structureKey = null;
981
+ #buttonEls = /* @__PURE__ */ new Map();
982
+ // Icon markup last WRITTEN. `btn.innerHTML` returns the browser's
983
+ // re-serialisation, so comparing against it rewrites every render and
984
+ // replaces the glyph under the pointer. Keyed by element: names repeat.
985
+ #iconHtml = /* @__PURE__ */ new WeakMap();
986
+ #colorTriggerEl = null;
987
+ #blockMenuTriggerEl = null;
1083
988
  // Dropdown state (text-align dropdown inside bubble menu)
1084
989
  #openDropdown = null;
1085
990
  #dropdownPanelEl = null;
@@ -1162,7 +1067,7 @@ var DomternalBubbleMenu = class extends EventTarget {
1162
1067
  if (this.#destroyed) return;
1163
1068
  this.#explicitItems = items;
1164
1069
  if (this.#maps) {
1165
- this.#defaultItemList = items ? resolveNames(items, this.#maps.itemMap, this.#maps.dropdownMap) : resolveNames(["bold", "italic", "underline"], this.#maps.itemMap, this.#maps.dropdownMap);
1070
+ this.#defaultItemList = items ? resolveBubbleNames(items, this.#maps.itemMap, this.#maps.dropdownMap) : resolveBubbleNames(["bold", "italic", "underline"], this.#maps.itemMap, this.#maps.dropdownMap);
1166
1071
  }
1167
1072
  this.#updateResolvedItems();
1168
1073
  this.#updateStates();
@@ -1186,6 +1091,7 @@ var DomternalBubbleMenu = class extends EventTarget {
1186
1091
  setIcons(icons) {
1187
1092
  if (this.#destroyed) return;
1188
1093
  this.#icons = icons;
1094
+ this.#structureKey = null;
1189
1095
  this.#scheduleRender();
1190
1096
  }
1191
1097
  /** Close any open dropdown (text-align). No-op if nothing is open. */
@@ -1218,9 +1124,9 @@ var DomternalBubbleMenu = class extends EventTarget {
1218
1124
  const exts = ed.extensionManager.extensions;
1219
1125
  this.#hasNotionColorPicker = exts.some((e) => e.name === "notionColorPicker");
1220
1126
  this.#hasBlockContextMenu = exts.some((e) => e.name === "blockContextMenu");
1221
- this.#maps = buildItemMaps(ed);
1127
+ this.#maps = buildBubbleItemMaps(ed);
1222
1128
  this.#effectiveContexts = this.#explicitContexts ?? (this.#explicitItems ? void 0 : defaultBubbleContexts(ed));
1223
- this.#defaultItemList = this.#explicitItems ? resolveNames(this.#explicitItems, this.#maps.itemMap, this.#maps.dropdownMap) : resolveNames(["bold", "italic", "underline"], this.#maps.itemMap, this.#maps.dropdownMap);
1129
+ this.#defaultItemList = this.#explicitItems ? resolveBubbleNames(this.#explicitItems, this.#maps.itemMap, this.#maps.dropdownMap) : resolveBubbleNames(["bold", "italic", "underline"], this.#maps.itemMap, this.#maps.dropdownMap);
1224
1130
  const shouldShow = this.#shouldShowOpt ?? this.#buildDefaultShouldShow();
1225
1131
  const plugin = createBubbleMenuPlugin({
1226
1132
  pluginKey: this.#pluginKey,
@@ -1252,76 +1158,28 @@ var DomternalBubbleMenu = class extends EventTarget {
1252
1158
  this.#render();
1253
1159
  }
1254
1160
  #buildDefaultShouldShow() {
1255
- const contexts = this.#effectiveContexts;
1256
- const defaults = this.#maps?.bubbleDefaults;
1257
- if (contexts) {
1258
- return ({ state }) => {
1259
- const ctx = detectContext(
1260
- state.selection,
1261
- contexts
1262
- );
1263
- if (!ctx) return false;
1264
- if (ctx in contexts) {
1265
- const val = contexts[ctx];
1266
- if (val === null) return false;
1267
- return val === true || Array.isArray(val) && val.length > 0;
1268
- }
1269
- return defaults?.has(ctx) ?? false;
1270
- };
1271
- }
1272
- return ({ state }) => {
1273
- const sel = state.selection;
1274
- if (sel.empty) return false;
1275
- if (sel.node) return defaults?.has(sel.node.type.name) ?? false;
1276
- if (isInsideTableCell(sel.$from)) return false;
1277
- return sel.$from.parent.type.spec.marks !== "" || sel.$to.parent.type.spec.marks !== "";
1278
- };
1161
+ const maps = this.#maps;
1162
+ if (!maps) return () => false;
1163
+ return createBubbleShouldShow(maps, this.#effectiveContexts);
1279
1164
  }
1280
1165
  // === State updates (called per transaction) ===
1166
+ /**
1167
+ * One call per transaction, into the resolver core owns. Every renderer
1168
+ * asks the same question and has to get the same answer, so the answer is
1169
+ * not written here.
1170
+ */
1281
1171
  #updateResolvedItems() {
1282
- if (!this.#maps) return;
1283
- const ed = this.#editor;
1284
- const contexts = this.#effectiveContexts;
1285
- if (contexts) {
1286
- const ctx = detectContext(
1287
- ed.state.selection,
1288
- contexts
1289
- );
1290
- if (!ctx) {
1291
- this.#resolvedItems = [];
1292
- return;
1293
- }
1294
- if (ctx in contexts) {
1295
- const val = contexts[ctx];
1296
- if (val === null || Array.isArray(val) && val.length === 0) {
1297
- this.#resolvedItems = [];
1298
- return;
1299
- }
1300
- if (val === true) {
1301
- this.#resolvedItems = filterBySchema(ed, ctx, getFormatItems(this.#maps.itemMap));
1302
- return;
1303
- }
1304
- if (Array.isArray(val)) {
1305
- const resolved = resolveNames(val, this.#maps.itemMap, this.#maps.dropdownMap);
1306
- const buttons = resolved.filter(
1307
- (i) => i.type !== "separator"
1308
- );
1309
- const allowed = new Set(filterBySchema(ed, ctx, buttons).map((b) => b.name));
1310
- this.#resolvedItems = resolved.filter(
1311
- (i) => i.type === "separator" || allowed.has(i.name)
1312
- );
1313
- return;
1314
- }
1315
- }
1316
- this.#resolvedItems = this.#maps.bubbleDefaults.get(ctx) ?? [];
1172
+ const maps = this.#maps;
1173
+ if (!maps) {
1174
+ this.#resolvedItems = [];
1317
1175
  return;
1318
1176
  }
1319
- const sel = ed.state.selection;
1320
- if (sel.node && this.#maps.bubbleDefaults.has(sel.node.type.name)) {
1321
- this.#resolvedItems = this.#maps.bubbleDefaults.get(sel.node.type.name) ?? [];
1322
- } else {
1323
- this.#resolvedItems = this.#defaultItemList;
1324
- }
1177
+ this.#resolvedItems = resolveBubbleMenuItems({
1178
+ editor: this.#editor,
1179
+ maps,
1180
+ contexts: this.#effectiveContexts,
1181
+ fallbackItems: this.#defaultItemList
1182
+ });
1325
1183
  }
1326
1184
  #updateStates() {
1327
1185
  const ed = this.#editor;
@@ -1362,19 +1220,51 @@ var DomternalBubbleMenu = class extends EventTarget {
1362
1220
  this.#render();
1363
1221
  });
1364
1222
  }
1223
+ /** Signature of what decides which nodes exist; equal keys reuse the DOM. */
1224
+ #computeStructureKey() {
1225
+ const items = this.#resolvedItems.map(
1226
+ (item) => item.type === "dropdown" ? `${item.type}:${item.name}:${item.items.map((sub) => sub.name).join(",")}` : `${item.type}:${item.name}`
1227
+ ).join("|");
1228
+ const t = this.#trailing;
1229
+ return [
1230
+ items,
1231
+ t.showColorPickerButton && !t.isNodeSelection ? "color" : "",
1232
+ t.showBlockMenuButton && !t.isNodeSelection ? "block" : "",
1233
+ this.#customContent ? "custom" : ""
1234
+ ].join("#");
1235
+ }
1236
+ /**
1237
+ * Runs on every transaction, and pointer motion alone dispatches those. A
1238
+ * render lands a frame late, so rebuilding wholesale can replace a button
1239
+ * between mousedown and mouseup: the two events share no element and the
1240
+ * browser fires no click at all. Structure only on an item-list change,
1241
+ * state in place otherwise, as the toolbar does.
1242
+ */
1365
1243
  #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
1244
  if (this.#openDropdown !== null) {
1373
1245
  const stillExists = this.#resolvedItems.some(
1374
1246
  (item) => item.type === "dropdown" && item.name === this.#openDropdown
1375
1247
  );
1376
- if (!stillExists) this.#openDropdown = null;
1248
+ if (!stillExists) {
1249
+ this.#openDropdown = null;
1250
+ this.#detachDropdown();
1251
+ }
1252
+ }
1253
+ const key = this.#computeStructureKey();
1254
+ if (key === this.#structureKey) {
1255
+ this.#updateButtons();
1256
+ } else {
1257
+ this.#renderStructure();
1258
+ this.#structureKey = key;
1377
1259
  }
1260
+ this.#syncDropdownPanel();
1261
+ }
1262
+ #renderStructure() {
1263
+ this.host.replaceChildren();
1264
+ this.#buttonEls.clear();
1265
+ this.#colorTriggerEl = null;
1266
+ this.#blockMenuTriggerEl = null;
1267
+ this.#detachDropdown();
1378
1268
  for (const item of this.#resolvedItems) {
1379
1269
  if (item.type === "separator") {
1380
1270
  this.host.appendChild(this.#createSeparator(item.name));
@@ -1385,12 +1275,18 @@ var DomternalBubbleMenu = class extends EventTarget {
1385
1275
  }
1386
1276
  }
1387
1277
  const t = this.#trailing;
1388
- if (t.showColorPickerButton && !t.isNodeSelection) {
1389
- this.host.appendChild(this.#createSeparator("trailing-sep-color"));
1278
+ const showColor = t.showColorPickerButton && !t.isNodeSelection;
1279
+ const showBlock = t.showBlockMenuButton && !t.isNodeSelection;
1280
+ if (showColor) {
1281
+ if (this.#resolvedItems.length > 0) {
1282
+ this.host.appendChild(this.#createSeparator("trailing-sep-color"));
1283
+ }
1390
1284
  this.host.appendChild(this.#createColorTrigger());
1391
1285
  }
1392
- if (t.showBlockMenuButton && !t.isNodeSelection) {
1393
- this.host.appendChild(this.#createSeparator("trailing-sep-block"));
1286
+ if (showBlock) {
1287
+ if (this.#resolvedItems.length > 0 || showColor) {
1288
+ this.host.appendChild(this.#createSeparator("trailing-sep-block"));
1289
+ }
1394
1290
  this.host.appendChild(this.#createBlockMenuTrigger());
1395
1291
  }
1396
1292
  if (this.#customContent) {
@@ -1415,13 +1311,14 @@ var DomternalBubbleMenu = class extends EventTarget {
1415
1311
  btn.setAttribute("aria-label", item.label);
1416
1312
  btn.setAttribute("aria-pressed", String(isActive));
1417
1313
  btn.title = item.label;
1418
- btn.innerHTML = resolveIcon(item.icon, this.#icons);
1314
+ this.#setIconHtml(btn, resolveIcon(item.icon, this.#icons));
1419
1315
  btn.addEventListener("mousedown", (e) => {
1420
1316
  e.preventDefault();
1421
1317
  });
1422
1318
  btn.addEventListener("click", (e) => {
1423
1319
  this.#onButtonClick(item, e);
1424
1320
  });
1321
+ this.#buttonEls.set(item.name, btn);
1425
1322
  return btn;
1426
1323
  }
1427
1324
  #createDropdownTrigger(dd) {
@@ -1441,7 +1338,7 @@ var DomternalBubbleMenu = class extends EventTarget {
1441
1338
  trigger.setAttribute("aria-label", dd.label);
1442
1339
  trigger.title = dd.label;
1443
1340
  trigger.dataset["dropdown"] = dd.name;
1444
- trigger.innerHTML = triggerHtml;
1341
+ this.#setIconHtml(trigger, triggerHtml);
1445
1342
  trigger.addEventListener("mousedown", (e) => {
1446
1343
  e.preventDefault();
1447
1344
  });
@@ -1449,12 +1346,7 @@ var DomternalBubbleMenu = class extends EventTarget {
1449
1346
  this.#onDropdownToggle(dd);
1450
1347
  });
1451
1348
  wrapper.appendChild(trigger);
1452
- if (this.#openDropdown === dd.name) {
1453
- const panel = this.#createDropdownPanel(dd);
1454
- wrapper.appendChild(panel);
1455
- this.#dropdownPanelEl = panel;
1456
- this.#attachDropdownListeners(trigger, panel);
1457
- }
1349
+ this.#buttonEls.set(dd.name, trigger);
1458
1350
  return wrapper;
1459
1351
  }
1460
1352
  #createDropdownPanel(dd) {
@@ -1472,6 +1364,7 @@ var DomternalBubbleMenu = class extends EventTarget {
1472
1364
  if (subActive) subBtn.classList.add("dm-toolbar-dropdown-item--active");
1473
1365
  subBtn.setAttribute("role", "menuitem");
1474
1366
  subBtn.setAttribute("aria-label", sub.label);
1367
+ subBtn.dataset["dropdownItem"] = sub.name;
1475
1368
  subBtn.innerHTML = subHtml;
1476
1369
  subBtn.addEventListener("mousedown", (e) => {
1477
1370
  e.preventDefault();
@@ -1507,6 +1400,7 @@ var DomternalBubbleMenu = class extends EventTarget {
1507
1400
  btn.addEventListener("click", () => {
1508
1401
  this.openColorPicker(btn);
1509
1402
  });
1403
+ this.#colorTriggerEl = btn;
1510
1404
  return btn;
1511
1405
  }
1512
1406
  #createBlockMenuTrigger() {
@@ -1525,8 +1419,83 @@ var DomternalBubbleMenu = class extends EventTarget {
1525
1419
  btn.addEventListener("click", () => {
1526
1420
  this.openBlockContextMenu(btn);
1527
1421
  });
1422
+ this.#blockMenuTriggerEl = btn;
1528
1423
  return btn;
1529
1424
  }
1425
+ // === In-place updates (the common render) ===
1426
+ #updateButtons() {
1427
+ for (const item of this.#resolvedItems) {
1428
+ if (item.type === "separator") continue;
1429
+ const el = this.#buttonEls.get(item.name);
1430
+ if (!el) continue;
1431
+ if (item.type === "dropdown") this.#updateDropdownTrigger(item, el);
1432
+ else this.#updateButton(item, el);
1433
+ }
1434
+ const t = this.#trailing;
1435
+ if (this.#colorTriggerEl) {
1436
+ this.#colorTriggerEl.classList.toggle("dm-toolbar-button--active", t.hasAnyColor);
1437
+ const glyph = this.#colorTriggerEl.querySelector(".dm-ncp-trigger-glyph");
1438
+ if (glyph) glyph.style.color = t.currentTextColorVar ?? "";
1439
+ const underline = this.#colorTriggerEl.querySelector(".dm-ncp-trigger-underline");
1440
+ if (underline) underline.style.backgroundColor = t.currentBgColorVar ?? "";
1441
+ }
1442
+ if (this.#blockMenuTriggerEl) {
1443
+ this.#blockMenuTriggerEl.disabled = t.blockMenuButtonDisabled;
1444
+ this.#blockMenuTriggerEl.title = t.blockMenuButtonDisabled ? "Block actions (select within a single block)" : "More options";
1445
+ }
1446
+ }
1447
+ #updateButton(item, btn) {
1448
+ const isActive = this.#activeMap.get(item.name) ?? false;
1449
+ const isDisabled = this.#disabledMap.get(item.name) ?? false;
1450
+ btn.classList.toggle("dm-toolbar-button--active", isActive);
1451
+ btn.disabled = isDisabled;
1452
+ btn.setAttribute("aria-pressed", String(isActive));
1453
+ this.#setIconHtml(btn, resolveIcon(item.icon, this.#icons));
1454
+ }
1455
+ /** Writes icon markup only when it differs from what was written last. */
1456
+ #setIconHtml(el, html) {
1457
+ if (this.#iconHtml.get(el) === html) return;
1458
+ this.#iconHtml.set(el, html);
1459
+ el.innerHTML = html;
1460
+ }
1461
+ #updateDropdownTrigger(dd, trigger) {
1462
+ const dropdownActive = dd.items.some((sub) => this.#activeMap.get(sub.name) ?? false);
1463
+ const activeChild = dd.dynamicIcon ? dd.items.find((sub) => this.#activeMap.get(sub.name) ?? false) : void 0;
1464
+ const html = resolveIcon(activeChild?.icon ?? dd.icon, this.#icons) + DROPDOWN_CARET2;
1465
+ trigger.classList.toggle("dm-toolbar-button--active", dropdownActive);
1466
+ trigger.setAttribute("aria-expanded", String(this.#openDropdown === dd.name));
1467
+ this.#setIconHtml(trigger, html);
1468
+ }
1469
+ /** Builds the open dropdown's panel, or refreshes the marks on an open one. */
1470
+ #syncDropdownPanel() {
1471
+ const open = this.#openDropdown;
1472
+ if (open === null) {
1473
+ if (this.#dropdownPanelEl) this.#detachDropdown();
1474
+ return;
1475
+ }
1476
+ const dd = this.#resolvedItems.find(
1477
+ (item) => item.type === "dropdown" && item.name === open
1478
+ );
1479
+ if (!dd) return;
1480
+ const panel = this.#dropdownPanelEl;
1481
+ if (panel?.isConnected === true) {
1482
+ for (const sub of dd.items) {
1483
+ const subBtn = panel.querySelector(`[data-dropdown-item="${sub.name}"]`);
1484
+ subBtn?.classList.toggle(
1485
+ "dm-toolbar-dropdown-item--active",
1486
+ this.#activeMap.get(sub.name) ?? false
1487
+ );
1488
+ }
1489
+ return;
1490
+ }
1491
+ const trigger = this.#buttonEls.get(open);
1492
+ const wrapper = trigger?.parentElement;
1493
+ if (!trigger || !wrapper) return;
1494
+ const fresh = this.#createDropdownPanel(dd);
1495
+ wrapper.appendChild(fresh);
1496
+ this.#dropdownPanelEl = fresh;
1497
+ this.#attachDropdownListeners(trigger, fresh);
1498
+ }
1530
1499
  // === Event handlers ===
1531
1500
  #onButtonClick(item, event) {
1532
1501
  if (this.#openDropdown) this.closeDropdown();
@@ -1844,6 +1813,14 @@ var DomternalFloatingMenu = class extends EventTarget {
1844
1813
  }
1845
1814
  }
1846
1815
  };
1816
+ function paletteFromExtensionOptions(options) {
1817
+ if (typeof options !== "object" || options === null || !("palette" in options)) return [];
1818
+ const palette = options.palette;
1819
+ if (!Array.isArray(palette) || !palette.every((token) => typeof token === "string")) {
1820
+ return [];
1821
+ }
1822
+ return [...palette];
1823
+ }
1847
1824
  var TOKEN_LABELS = {
1848
1825
  gray: "Gray",
1849
1826
  brown: "Brown",
@@ -1920,8 +1897,7 @@ var DomternalNotionColorPicker = class extends EventTarget {
1920
1897
  const ext = this.#editor.extensionManager.extensions.find(
1921
1898
  (e) => e.name === "notionColorPicker"
1922
1899
  );
1923
- const extOpts = ext?.options ?? null;
1924
- this.#palette = extOpts?.palette ? [...extOpts.palette] : [];
1900
+ this.#palette = paletteFromExtensionOptions(ext?.options);
1925
1901
  this.#onOpen = (...args) => {
1926
1902
  const detail = args[0];
1927
1903
  const incoming = detail?.anchorElement;
@@ -2002,9 +1978,7 @@ var DomternalNotionColorPicker = class extends EventTarget {
2002
1978
  this.#editor.commands.setTextColorToken(token);
2003
1979
  this.#syncFromSelection();
2004
1980
  this.#updateActiveClasses();
2005
- this.dispatchEvent(
2006
- new CustomEvent("apply", { detail: { kind: "text", token } })
2007
- );
1981
+ this.dispatchEvent(new CustomEvent("apply", { detail: { kind: "text", token } }));
2008
1982
  }
2009
1983
  /** Apply a background color token to the current selection. Picker stays open. */
2010
1984
  applyBg(token) {
@@ -2012,9 +1986,7 @@ var DomternalNotionColorPicker = class extends EventTarget {
2012
1986
  this.#editor.commands.setBackgroundColorToken(token);
2013
1987
  this.#syncFromSelection();
2014
1988
  this.#updateActiveClasses();
2015
- this.dispatchEvent(
2016
- new CustomEvent("apply", { detail: { kind: "bg", token } })
2017
- );
1989
+ this.dispatchEvent(new CustomEvent("apply", { detail: { kind: "bg", token } }));
2018
1990
  }
2019
1991
  /** Display label for a palette token (title-case fallback). */
2020
1992
  tokenLabel(token) {
@@ -2224,9 +2196,7 @@ var DomternalNotionColorPicker = class extends EventTarget {
2224
2196
  return rect;
2225
2197
  }
2226
2198
  if (this.#anchorBubbleMenu?.isConnected) {
2227
- const fresh = this.#anchorBubbleMenu.querySelector(
2228
- ".dm-ncp-trigger"
2229
- );
2199
+ const fresh = this.#anchorBubbleMenu.querySelector(".dm-ncp-trigger");
2230
2200
  if (fresh) {
2231
2201
  this.#anchor = fresh;
2232
2202
  const rect = fresh.getBoundingClientRect();
@@ -2296,15 +2266,13 @@ var DomternalNotionColorPicker = class extends EventTarget {
2296
2266
  #onPanelKeydown(event) {
2297
2267
  const cols = 5;
2298
2268
  if (!this.#panel) return;
2299
- const swatches = Array.from(
2300
- this.#panel.querySelectorAll(".dm-ncp-swatch")
2301
- );
2269
+ const swatches = Array.from(this.#panel.querySelectorAll(".dm-ncp-swatch"));
2302
2270
  if (!swatches.length) return;
2303
2271
  const active = document.activeElement;
2304
2272
  if (!(active instanceof HTMLElement)) return;
2305
2273
  const idx = swatches.indexOf(active);
2306
2274
  if (idx === -1) return;
2307
- let next = idx;
2275
+ let next;
2308
2276
  switch (event.key) {
2309
2277
  case "ArrowRight":
2310
2278
  event.preventDefault();
@@ -2531,11 +2499,7 @@ var DomternalEmojiPicker = class extends EventTarget {
2531
2499
  return panel;
2532
2500
  }
2533
2501
  #renderPanelChildren() {
2534
- return [
2535
- this.#renderSearch(),
2536
- this.#renderTabs(),
2537
- this.#renderGrid()
2538
- ];
2502
+ return [this.#renderSearch(), this.#renderTabs(), this.#renderGrid()];
2539
2503
  }
2540
2504
  #renderSearch() {
2541
2505
  const wrapper = document.createElement("div");
@@ -2650,9 +2614,7 @@ var DomternalEmojiPicker = class extends EventTarget {
2650
2614
  */
2651
2615
  #setSearchQuery(value) {
2652
2616
  this.#searchQuery = value;
2653
- const input = this.#panel?.querySelector(
2654
- ".dm-emoji-picker-search input"
2655
- );
2617
+ const input = this.#panel?.querySelector(".dm-emoji-picker-search input");
2656
2618
  if (input && input.value !== value) input.value = value;
2657
2619
  }
2658
2620
  /** Replace grid contents only (used when searchQuery changes). */
@@ -2716,9 +2678,7 @@ var DomternalEmojiPicker = class extends EventTarget {
2716
2678
  #onGridKeydown(event) {
2717
2679
  const grid = this.#panel?.querySelector(".dm-emoji-picker-grid");
2718
2680
  if (!grid) return;
2719
- const swatches = Array.from(
2720
- grid.querySelectorAll(".dm-emoji-swatch")
2721
- );
2681
+ const swatches = Array.from(grid.querySelectorAll(".dm-emoji-swatch"));
2722
2682
  if (!swatches.length) return;
2723
2683
  const active = document.activeElement;
2724
2684
  const idx = active instanceof HTMLElement ? swatches.indexOf(active) : -1;
@@ -2730,7 +2690,7 @@ var DomternalEmojiPicker = class extends EventTarget {
2730
2690
  return;
2731
2691
  }
2732
2692
  const cols = 8;
2733
- let next = idx;
2693
+ let next;
2734
2694
  switch (event.key) {
2735
2695
  case "ArrowRight":
2736
2696
  event.preventDefault();
@@ -2768,9 +2728,7 @@ var DomternalEmojiPicker = class extends EventTarget {
2768
2728
  offsetValue: 4
2769
2729
  });
2770
2730
  }
2771
- const input = this.#panel.querySelector(
2772
- ".dm-emoji-picker-search input"
2773
- );
2731
+ const input = this.#panel.querySelector(".dm-emoji-picker-search input");
2774
2732
  input?.focus({ preventScroll: true });
2775
2733
  });
2776
2734
  }
@@ -2809,6 +2767,6 @@ var DomternalEmojiPicker = class extends EventTarget {
2809
2767
  }
2810
2768
  };
2811
2769
 
2812
- export { DEFAULT_EXTENSIONS, DROPDOWN_CARET, DomternalBubbleMenu, DomternalEditor, DomternalEmojiPicker, DomternalFloatingMenu, DomternalNotionColorPicker, DomternalToolbar, INITIAL_TRAILING_STATE, assertBrowser, buildItemMaps, computeTrailingState, createIconCache, createPluginKey, detectContext, filterBySchema, getComputedStyleAtCursor, getFormatItems, getInlineStyleAtCursor, getTooltip, isBrowser, isInsideTableCell, renderIconInto, resolveIcon, resolveNames, subscribe };
2770
+ export { DEFAULT_EXTENSIONS, DROPDOWN_CARET, DomternalBubbleMenu, DomternalEditor, DomternalEmojiPicker, DomternalFloatingMenu, DomternalNotionColorPicker, DomternalToolbar, INITIAL_TRAILING_STATE, assertBrowser, computeTrailingState, createIconCache, createPluginKey, getComputedStyleAtCursor, getInlineStyleAtCursor, getTooltip, isBrowser, renderIconInto, resolveIcon, subscribe };
2813
2771
  //# sourceMappingURL=index.js.map
2814
2772
  //# sourceMappingURL=index.js.map