@jackuait/blok 0.4.1-beta.11 → 0.4.1-beta.13

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.
Files changed (29) hide show
  1. package/dist/blok.mjs +2 -2
  2. package/dist/chunks/{blok-oNSQ3HA6.mjs → blok-Xfgk2kCJ.mjs} +665 -637
  3. package/dist/chunks/{i18next-loader-BdNRw4n4.mjs → i18next-loader-BMO6Rg_l.mjs} +1 -1
  4. package/dist/chunks/{index-DHgXmfki.mjs → index-DyPp5v5e.mjs} +1 -1
  5. package/dist/chunks/{inline-tool-convert-CRqgjRim.mjs → inline-tool-convert-DhHW7EYl.mjs} +2 -1
  6. package/dist/full.mjs +2 -2
  7. package/dist/tools.mjs +57 -31
  8. package/package.json +25 -7
  9. package/src/components/inline-tools/inline-tool-convert.ts +1 -0
  10. package/src/components/inline-tools/inline-tool-link.ts +1 -0
  11. package/src/components/modules/toolbar/blockSettings.ts +2 -1
  12. package/src/components/modules/toolbar/index.ts +97 -116
  13. package/src/components/modules/ui.ts +11 -7
  14. package/src/components/ui/toolbox.ts +14 -5
  15. package/src/components/utils/data-model-transform.ts +38 -21
  16. package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +1 -1
  17. package/src/components/utils/popover/components/popover-item/popover-item.ts +11 -0
  18. package/src/components/utils/popover/popover-abstract.ts +1 -1
  19. package/src/components/utils/popover/popover-desktop.ts +8 -2
  20. package/src/stories/Popover.stories.ts +0 -85
  21. package/src/styles/main.css +7 -4
  22. package/src/tools/header/index.ts +1 -0
  23. package/src/tools/list/index.ts +34 -4
  24. package/types/configs/sanitizer-config.d.ts +25 -1
  25. package/types/index.d.ts +1 -0
  26. package/types/tools/block-tool.d.ts +2 -2
  27. package/types/tools/tool-settings.d.ts +7 -0
  28. package/types/utils/popover/popover-item.d.ts +6 -0
  29. package/types/utils/popover/popover.d.ts +6 -0
@@ -1,4 +1,4 @@
1
- import { e as i } from "./blok-oNSQ3HA6.mjs";
1
+ import { e as i } from "./blok-Xfgk2kCJ.mjs";
2
2
  const l = async (e, r) => {
3
3
  const n = (await import("./i18next-CugVlwWp.mjs")).default.createInstance(), s = {
4
4
  lng: e,
@@ -1,4 +1,4 @@
1
- import { t as f, q as i } from "./inline-tool-convert-CRqgjRim.mjs";
1
+ import { t as f, q as i } from "./inline-tool-convert-DhHW7EYl.mjs";
2
2
  const a = {
3
3
  wrapper: i(
4
4
  "fixed z-[2] bottom-5 left-5",
@@ -18,7 +18,7 @@ let nt = (o = 21) => {
18
18
  return t;
19
19
  };
20
20
  var ot = /* @__PURE__ */ ((o) => (o.VERBOSE = "VERBOSE", o.INFO = "INFO", o.WARN = "WARN", o.ERROR = "ERROR", o))(ot || {});
21
- const rt = () => "0.4.1-beta.11", Ct = {
21
+ const rt = () => "0.4.1-beta.13", Ct = {
22
22
  BACKSPACE: 8,
23
23
  TAB: 9,
24
24
  ENTER: 13,
@@ -1895,6 +1895,7 @@ const te = `
1895
1895
  },
1896
1896
  children: {
1897
1897
  items: s,
1898
+ width: "auto",
1898
1899
  onOpen: () => {
1899
1900
  c && (this.selectionAPI.setFakeBackground(), this.selectionAPI.save());
1900
1901
  },
package/dist/full.mjs CHANGED
@@ -10,10 +10,10 @@ var e = (a, l, o) => l in a ? n(a, l, { enumerable: !0, configurable: !0, writab
10
10
  d.call(l, o) && e(a, o, l[o]);
11
11
  return a;
12
12
  }, r = (a, l) => t(a, c(l));
13
- import { B as v, v as A } from "./chunks/blok-oNSQ3HA6.mjs";
13
+ import { B as v, v as A } from "./chunks/blok-Xfgk2kCJ.mjs";
14
14
  import { List as p, Header as f, Paragraph as I, Link as k, Italic as u, Bold as B } from "./tools.mjs";
15
15
  import { defaultBlockTools as H, defaultInlineTools as P } from "./tools.mjs";
16
- import { D as _ } from "./chunks/inline-tool-convert-CRqgjRim.mjs";
16
+ import { D as _ } from "./chunks/inline-tool-convert-DhHW7EYl.mjs";
17
17
  const m = {
18
18
  paragraph: {
19
19
  class: I,
package/dist/tools.mjs CHANGED
@@ -2,7 +2,7 @@ var nt = Object.defineProperty, rt = Object.defineProperties;
2
2
  var st = Object.getOwnPropertyDescriptors;
3
3
  var K = Object.getOwnPropertySymbols;
4
4
  var ot = Object.prototype.hasOwnProperty, it = Object.prototype.propertyIsEnumerable;
5
- var U = (f, t, e) => t in f ? nt(f, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : f[t] = e, w = (f, t) => {
5
+ var U = (f, t, e) => t in f ? nt(f, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : f[t] = e, O = (f, t) => {
6
6
  for (var e in t || (t = {}))
7
7
  ot.call(t, e) && U(f, e, t[e]);
8
8
  if (K)
@@ -10,8 +10,8 @@ var U = (f, t, e) => t in f ? nt(f, t, { enumerable: !0, configurable: !0, writa
10
10
  it.call(t, e) && U(f, e, t[e]);
11
11
  return f;
12
12
  }, P = (f, t) => rt(f, st(t));
13
- import { t as x, D as m, a9 as et, aa as at, ab as lt, A as ct, ac as dt, ad as ut, ae as ht, af as ft, ag as pt, ah as mt, ai as G, aj as j, ak as $, f as A, al as gt, am as Et, S as H, P as Tt, an as Ct, l as At, J as yt } from "./chunks/inline-tool-convert-CRqgjRim.mjs";
14
- import { a0 as Dt } from "./chunks/inline-tool-convert-CRqgjRim.mjs";
13
+ import { t as x, D as m, a9 as et, aa as at, ab as lt, A as ct, ac as dt, ad as ut, ae as ht, af as ft, ag as pt, ah as mt, ai as G, aj as j, ak as $, f as A, al as gt, am as Et, S as H, P as Tt, an as Ct, l as At, J as yt } from "./chunks/inline-tool-convert-DhHW7EYl.mjs";
14
+ import { a0 as Dt } from "./chunks/inline-tool-convert-DhHW7EYl.mjs";
15
15
  const W = [
16
16
  "empty:before:pointer-events-none",
17
17
  "empty:before:text-gray-text",
@@ -531,7 +531,8 @@ const L = class L {
531
531
  titleKey: t.nameKey,
532
532
  name: `header-${t.number}`,
533
533
  data: { level: t.number },
534
- searchTerms: [`h${t.number}`, "title", "header", "heading"]
534
+ searchTerms: [`h${t.number}`, "title", "header", "heading"],
535
+ shortcut: "#".repeat(t.number)
535
536
  }));
536
537
  }
537
538
  };
@@ -554,15 +555,32 @@ const u = class u {
554
555
  })));
555
556
  }, this.api = n, this.readOnly = r, this._settings = e || {}, this._data = this.normalizeData(t), s && (this.blockId = s.id), this._data.style === "ordered" && this.api.events.on("block changed", this.handleBlockChanged);
556
557
  }
558
+ /**
559
+ * Legacy list item structure for backward compatibility
560
+ */
561
+ static isLegacyFormat(t) {
562
+ return typeof t == "object" && t !== null && "items" in t && Array.isArray(t.items);
563
+ }
557
564
  normalizeData(t) {
558
565
  var n;
559
566
  const e = this._settings.defaultStyle || "unordered";
560
- return !t || typeof t != "object" ? {
561
- text: "",
562
- style: e,
563
- checked: !1,
564
- depth: 0
565
- } : w({
567
+ if (!t || typeof t != "object")
568
+ return {
569
+ text: "",
570
+ style: e,
571
+ checked: !1,
572
+ depth: 0
573
+ };
574
+ if (u.isLegacyFormat(t)) {
575
+ const r = t.items[0], s = (r == null ? void 0 : r.content) || "", o = (r == null ? void 0 : r.checked) || !1;
576
+ return O({
577
+ text: s,
578
+ style: t.style || e,
579
+ checked: !!o,
580
+ depth: 0
581
+ }, t.start !== void 0 && t.start !== 1 ? { start: t.start } : {});
582
+ }
583
+ return O({
566
584
  text: t.text || "",
567
585
  style: t.style || e,
568
586
  checked: !!t.checked,
@@ -1154,7 +1172,7 @@ const u = class u {
1154
1172
  charCount: r.charCount,
1155
1173
  node: s,
1156
1174
  offset: e - r.charCount
1157
- } : P(w({}, r), {
1175
+ } : P(O({}, r), {
1158
1176
  charCount: r.charCount + o
1159
1177
  });
1160
1178
  },
@@ -1201,7 +1219,7 @@ const u = class u {
1201
1219
  this.syncContentFromDOM();
1202
1220
  const s = n + 1;
1203
1221
  this._data.depth = s;
1204
- const o = await this.api.blocks.update(this.blockId || "", P(w({}, this._data), {
1222
+ const o = await this.api.blocks.update(this.blockId || "", P(O({}, this._data), {
1205
1223
  depth: s
1206
1224
  }));
1207
1225
  this.setCaretToBlockContent(o);
@@ -1212,7 +1230,7 @@ const u = class u {
1212
1230
  this.syncContentFromDOM();
1213
1231
  const e = t - 1;
1214
1232
  this._data.depth = e;
1215
- const n = await this.api.blocks.update(this.blockId || "", P(w({}, this._data), {
1233
+ const n = await this.api.blocks.update(this.blockId || "", P(O({}, this._data), {
1216
1234
  depth: e
1217
1235
  }));
1218
1236
  this.setCaretToBlockContent(n);
@@ -1342,7 +1360,11 @@ const u = class u {
1342
1360
  return {
1343
1361
  text: {
1344
1362
  br: !0,
1345
- a: !0,
1363
+ a: {
1364
+ href: !0,
1365
+ target: "_blank",
1366
+ rel: "nofollow"
1367
+ },
1346
1368
  b: !0,
1347
1369
  i: !0,
1348
1370
  mark: !0
@@ -1421,7 +1443,8 @@ const u = class u {
1421
1443
  titleKey: "bulletedList",
1422
1444
  data: { style: "unordered" },
1423
1445
  name: "bulleted-list",
1424
- searchTerms: ["ul", "bullet", "unordered", "list"]
1446
+ searchTerms: ["ul", "bullet", "unordered", "list"],
1447
+ shortcut: "-"
1425
1448
  },
1426
1449
  {
1427
1450
  icon: j,
@@ -1429,7 +1452,8 @@ const u = class u {
1429
1452
  titleKey: "numberedList",
1430
1453
  data: { style: "ordered" },
1431
1454
  name: "numbered-list",
1432
- searchTerms: ["ol", "ordered", "number", "list"]
1455
+ searchTerms: ["ol", "ordered", "number", "list"],
1456
+ shortcut: "1."
1433
1457
  },
1434
1458
  {
1435
1459
  icon: $,
@@ -1437,7 +1461,8 @@ const u = class u {
1437
1461
  titleKey: "todoList",
1438
1462
  data: { style: "checklist" },
1439
1463
  name: "check-list",
1440
- searchTerms: ["checkbox", "task", "todo", "check", "list"]
1464
+ searchTerms: ["checkbox", "task", "todo", "check", "list"],
1465
+ shortcut: "[]"
1441
1466
  }
1442
1467
  ];
1443
1468
  }
@@ -2082,7 +2107,7 @@ const i = class i {
2082
2107
  return;
2083
2108
  const s = `strong[${i.DATA_ATTR_COLLAPSED_ACTIVE}="true"]`;
2084
2109
  r.querySelectorAll(s).forEach((l) => {
2085
- var k, I, F, z, q;
2110
+ var k, I, _, z, q;
2086
2111
  const d = l.getAttribute(i.DATA_ATTR_PREV_LENGTH), c = l.previousSibling;
2087
2112
  if (!d || !c || c.nodeType !== Node.TEXT_NODE)
2088
2113
  return;
@@ -2097,13 +2122,13 @@ const i = class i {
2097
2122
  const C = E.match(/^[\u00A0\s]+/);
2098
2123
  if (C && !l.hasAttribute(i.DATA_ATTR_LEADING_WHITESPACE) && l.setAttribute(i.DATA_ATTR_LEADING_WHITESPACE, C[0]), E.length === 0)
2099
2124
  return;
2100
- const T = (I = l.textContent) != null ? I : "", N = T + E, B = (F = l.getAttribute(i.DATA_ATTR_LEADING_WHITESPACE)) != null ? F : "", R = B.length > 0 && T.length === 0 && !N.startsWith(B) ? B + N : N, b = document.createTextNode(R);
2125
+ const T = (I = l.textContent) != null ? I : "", N = T + E, B = (_ = l.getAttribute(i.DATA_ATTR_LEADING_WHITESPACE)) != null ? _ : "", R = B.length > 0 && T.length === 0 && !N.startsWith(B) ? B + N : N, b = document.createTextNode(R);
2101
2126
  for (; l.firstChild; )
2102
2127
  l.removeChild(l.firstChild);
2103
2128
  if (l.appendChild(b), !(t != null && t.isCollapsed) || !i.isNodeWithin(t.focusNode, p))
2104
2129
  return;
2105
- const S = document.createRange(), _ = (q = (z = b.textContent) == null ? void 0 : z.length) != null ? q : 0;
2106
- S.setStart(b, _), S.collapse(!0), t.removeAllRanges(), t.addRange(S);
2130
+ const S = document.createRange(), F = (q = (z = b.textContent) == null ? void 0 : z.length) != null ? q : 0;
2131
+ S.setStart(b, F), S.collapse(!0), t.removeAllRanges(), t.addRange(S);
2107
2132
  });
2108
2133
  }
2109
2134
  /**
@@ -2312,7 +2337,7 @@ const i = class i {
2312
2337
  if (!r)
2313
2338
  return;
2314
2339
  r.querySelectorAll(`strong[${i.DATA_ATTR_COLLAPSED_LENGTH}]`).forEach((a) => {
2315
- var v, R, b, S, _;
2340
+ var v, R, b, S, F;
2316
2341
  const l = a, d = l.getAttribute(i.DATA_ATTR_COLLAPSED_LENGTH);
2317
2342
  if (!d)
2318
2343
  return;
@@ -2323,11 +2348,11 @@ const i = class i {
2323
2348
  if (B && T) {
2324
2349
  const k = N.slice(0, E), I = N.slice(E);
2325
2350
  T.textContent = k;
2326
- const F = document.createTextNode(I);
2327
- (b = l.parentNode) == null || b.insertBefore(F, l.nextSibling);
2351
+ const _ = document.createTextNode(I);
2352
+ (b = l.parentNode) == null || b.insertBefore(_, l.nextSibling);
2328
2353
  }
2329
2354
  if (B && l.removeAttribute(i.DATA_ATTR_PREV_LENGTH), t != null && t.isCollapsed && g && i.isNodeWithin(t.focusNode, l)) {
2330
- const k = document.createRange(), I = (_ = (S = g.textContent) == null ? void 0 : S.length) != null ? _ : 0;
2355
+ const k = document.createRange(), I = (F = (S = g.textContent) == null ? void 0 : S.length) != null ? F : 0;
2331
2356
  k.setStart(g, I), k.collapse(!0), t.removeAllRanges(), t.addRange(k);
2332
2357
  }
2333
2358
  p && l.removeAttribute(i.DATA_ATTR_COLLAPSED_LENGTH);
@@ -2594,7 +2619,7 @@ const i = class i {
2594
2619
  };
2595
2620
  i.isInline = !0, i.title = "Bold", i.titleKey = "bold", i.shortcutListenerRegistered = !1, i.selectionListenerRegistered = !1, i.inputListenerRegistered = !1, i.beforeInputListenerRegistered = !1, i.globalListenersInitialized = i.initializeGlobalListeners(), i.collapsedExitRecords = /* @__PURE__ */ new Set(), i.markerSequence = 0, i.isProcessingMutation = !1, i.DATA_ATTR_COLLAPSED_LENGTH = "data-blok-bold-collapsed-length", i.DATA_ATTR_COLLAPSED_ACTIVE = "data-blok-bold-collapsed-active", i.DATA_ATTR_PREV_LENGTH = "data-blok-bold-prev-length", i.DATA_ATTR_LEADING_WHITESPACE = "data-blok-bold-leading-ws", i.instances = /* @__PURE__ */ new Set(), i.pendingBoundaryCaretAdjustments = /* @__PURE__ */ new WeakSet(), i.shortcut = "CMD+B";
2596
2621
  let Z = i;
2597
- const O = class O {
2622
+ const D = class D {
2598
2623
  /**
2599
2624
  * Sanitizer Rule
2600
2625
  * Leave <i> and <em> tags
@@ -2865,9 +2890,9 @@ const O = class O {
2865
2890
  n.insertBefore(a, e.nextSibling), n.insertBefore(t, a);
2866
2891
  }
2867
2892
  };
2868
- O.isInline = !0, O.title = "Italic", O.titleKey = "italic", O.shortcut = "CMD+I";
2869
- let Q = O;
2870
- const D = class D {
2893
+ D.isInline = !0, D.title = "Italic", D.titleKey = "italic", D.shortcut = "CMD+I";
2894
+ let Q = D;
2895
+ const w = class w {
2871
2896
  /**
2872
2897
  * @param api - Blok API
2873
2898
  */
@@ -2907,6 +2932,7 @@ const D = class D {
2907
2932
  isActive: () => !!this.selection.findParentTag("A"),
2908
2933
  children: {
2909
2934
  hideChevron: !0,
2935
+ width: "200px",
2910
2936
  items: [
2911
2937
  {
2912
2938
  type: Tt.Html,
@@ -3093,8 +3119,8 @@ const D = class D {
3093
3119
  t && t.setAttribute(e, n ? "true" : "false");
3094
3120
  }
3095
3121
  };
3096
- D.isInline = !0, D.title = "Link", D.titleKey = "link", D.shortcut = "CMD+K";
3097
- let tt = D;
3122
+ w.isInline = !0, w.title = "Link", w.titleKey = "link", w.shortcut = "CMD+K";
3123
+ let tt = w;
3098
3124
  const vt = {
3099
3125
  paragraph: { preserveBlank: !0 },
3100
3126
  header: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackuait/blok",
3
- "version": "0.4.1-beta.11",
3
+ "version": "0.4.1-beta.13",
4
4
  "description": "Blok — headless, highly extensible rich text editor built for developers who need to implement a block-based editing experience (similar to Notion) without building it from scratch",
5
5
  "module": "dist/blok.mjs",
6
6
  "types": "./types/index.d.ts",
@@ -96,14 +96,12 @@
96
96
  "verify:package:local": "npm pack && node scripts/verify-published-package.mjs --local",
97
97
  "verify:version": "node scripts/verify-version.mjs",
98
98
  "unpublish": "node scripts/unpublish-package.mjs",
99
- "bundle:track": "node scripts/track-bundle-size.mjs --verbose",
100
- "bundle:variants": "node scripts/build-bundle-variants.mjs --verbose",
101
- "bundle:trends": "node scripts/view-bundle-trends.mjs --trends",
102
- "bundle:history": "node scripts/view-bundle-trends.mjs",
103
99
  "perf:analyze": "node scripts/analyze-performance.mjs",
104
100
  "perf:compare": "node scripts/analyze-performance.mjs --baseline",
105
101
  "perf:dashboard": "node scripts/generate-performance-dashboard.mjs",
106
- "e2e:validate-categories": "node scripts/validate-test-categories.mjs"
102
+ "e2e:validate-categories": "node scripts/validate-test-categories.mjs",
103
+ "size": "size-limit",
104
+ "size:why": "size-limit --why"
107
105
  },
108
106
  "author": "JackUait",
109
107
  "contributors": [
@@ -123,6 +121,8 @@
123
121
  "@playwright/test": "1.57.0",
124
122
  "@semantic-release/changelog": "^6.0.3",
125
123
  "@semantic-release/git": "^10.0.1",
124
+ "@size-limit/esbuild-why": "^12.0.0",
125
+ "@size-limit/preset-small-lib": "^12.0.0",
126
126
  "@storybook/addon-a11y": "10.1.1",
127
127
  "@storybook/addon-vitest": "10.1.1",
128
128
  "@storybook/global": "5.0.0",
@@ -150,6 +150,7 @@
150
150
  "postcss": "8.5.6",
151
151
  "rollup-plugin-license": "3.6.0",
152
152
  "semantic-release": "^25.0.2",
153
+ "size-limit": "^12.0.0",
153
154
  "storybook": "10.1.1",
154
155
  "tailwindcss": "3",
155
156
  "tslint": "6.1.3",
@@ -163,5 +164,22 @@
163
164
  "dependencies": {
164
165
  "i18next": "^25.7.3",
165
166
  "nanoid": "^5.1.6"
166
- }
167
+ },
168
+ "size-limit": [
169
+ {
170
+ "name": "Minimum (core only)",
171
+ "path": "src/variants/blok-minimum.ts",
172
+ "limit": "300 KB"
173
+ },
174
+ {
175
+ "name": "Normal (with tools)",
176
+ "path": "src/blok.ts",
177
+ "limit": "300 KB"
178
+ },
179
+ {
180
+ "name": "Maximum (all locales)",
181
+ "path": "src/variants/blok-maximum.ts",
182
+ "limit": "300 KB"
183
+ }
184
+ ]
167
185
  }
@@ -123,6 +123,7 @@ export class ConvertInlineTool implements InlineTool {
123
123
  },
124
124
  children: {
125
125
  items: convertToItems,
126
+ width: 'auto',
126
127
  onOpen: () => {
127
128
  if (isDesktop) {
128
129
  this.selectionAPI.setFakeBackground();
@@ -133,6 +133,7 @@ export class LinkInlineTool implements InlineTool {
133
133
  isActive: () => !!this.selection.findParentTag('A'),
134
134
  children: {
135
135
  hideChevron: true,
136
+ width: '200px',
136
137
  items: [
137
138
  {
138
139
  type: PopoverItemType.Html,
@@ -164,10 +164,11 @@ export class BlockSettings extends Module<BlockSettingsNodes> {
164
164
 
165
165
  const PopoverClass = isMobileScreen() ? PopoverMobile : PopoverDesktop;
166
166
  const popoverParams: PopoverParams & { flipper?: Flipper } = {
167
- searchable: true,
167
+ searchable: false,
168
168
  trigger: trigger || this.nodes.wrapper,
169
169
  items: await this.getTunesItems(block, commonTunes, toolTunes),
170
170
  scopeElement: this.Blok.API.methods.ui.nodes.redactor,
171
+ width: 'auto',
171
172
  messages: {
172
173
  nothingFound: this.Blok.I18n.t('popover.nothingFound'),
173
174
  search: this.Blok.I18n.t('popover.search'),
@@ -109,18 +109,6 @@ export class Toolbar extends Module<ToolbarNodes> {
109
109
  */
110
110
  private toolboxInstance: Toolbox | null = null;
111
111
 
112
- /**
113
- * Mouse position when mousedown occurred on settings toggler
114
- * Used to distinguish between click and drag
115
- */
116
- private settingsTogglerMouseDownPosition: { x: number; y: number } | null = null;
117
-
118
- /**
119
- * Mouse position when mousedown occurred on plus button
120
- * Used to distinguish between click and drag
121
- */
122
- private plusButtonMouseDownPosition: { x: number; y: number } | null = null;
123
-
124
112
  /**
125
113
  * Last calculated toolbar Y position
126
114
  * Used to avoid unnecessary repositioning when the position hasn't changed
@@ -133,6 +121,12 @@ export class Toolbar extends Module<ToolbarNodes> {
133
121
  */
134
122
  private ignoreNextSettingsMouseUp = false;
135
123
 
124
+ /**
125
+ * Set of pending document-level mouseup listeners that need cleanup on destroy.
126
+ * Each listener is added on mousedown and removed after mouseup fires.
127
+ */
128
+ private pendingMouseUpListeners: Set<(e: MouseEvent) => void> = new Set();
129
+
136
130
  /**
137
131
  * @class
138
132
  * @param moduleConfiguration - Module Configuration
@@ -218,7 +212,6 @@ export class Toolbar extends Module<ToolbarNodes> {
218
212
  'not-mobile:w-6'
219
213
  ),
220
214
  settingsTogglerHidden: 'hidden',
221
- settingsTogglerOpened: '',
222
215
  };
223
216
  }
224
217
 
@@ -660,60 +653,25 @@ export class Toolbar extends Module<ToolbarNodes> {
660
653
 
661
654
  /**
662
655
  * Plus button mousedown handler
663
- * Stores the initial mouse position and sets up a document-level mouseup listener.
664
- * Using document-level mouseup ensures we catch the event even if the mouse
665
- * moves slightly off the button element during the click.
656
+ * Uses click-vs-drag detection to distinguish clicks from drags.
666
657
  */
667
658
  this.readOnlyMutableListeners.on(plusButton, 'mousedown', (e) => {
668
659
  hide();
669
660
 
670
- const mouseEvent = e as MouseEvent;
671
-
672
- /**
673
- * Store the mouse position when mousedown occurs
674
- * This will be used to determine if the user dragged or clicked
675
- */
676
- this.plusButtonMouseDownPosition = {
677
- x: mouseEvent.clientX,
678
- y: mouseEvent.clientY,
679
- };
680
-
681
- /**
682
- * Add document-level mouseup listener to catch the event even if mouse
683
- * moves slightly off the button. This is removed after firing once.
684
- */
685
- const onMouseUp = (mouseUpEvent: MouseEvent): void => {
686
- document.removeEventListener('mouseup', onMouseUp, true);
687
-
688
- const mouseDownPos = this.plusButtonMouseDownPosition;
689
-
690
- this.plusButtonMouseDownPosition = null;
691
-
692
- if (mouseDownPos === null) {
693
- return;
694
- }
695
-
696
- const wasDragged = (
697
- Math.abs(mouseUpEvent.clientX - mouseDownPos.x) > DRAG_THRESHOLD ||
698
- Math.abs(mouseUpEvent.clientY - mouseDownPos.y) > DRAG_THRESHOLD
699
- );
661
+ this.setupClickVsDrag(
662
+ e as MouseEvent,
663
+ (mouseUpEvent) => {
664
+ /**
665
+ * Check for modifier key to determine insert direction:
666
+ * - Option/Alt on Mac, Ctrl on Windows → insert above
667
+ * - No modifier → insert below (default)
668
+ */
669
+ const userOS = getUserOS();
670
+ const insertAbove = userOS.win ? mouseUpEvent.ctrlKey : mouseUpEvent.altKey;
700
671
 
701
- if (wasDragged) {
702
- return;
672
+ this.plusButtonClicked(insertAbove);
703
673
  }
704
-
705
- /**
706
- * Check for modifier key to determine insert direction:
707
- * - Option/Alt on Mac, Ctrl on Windows → insert above
708
- * - No modifier → insert below (default)
709
- */
710
- const userOS = getUserOS();
711
- const insertAbove = userOS.win ? mouseUpEvent.ctrlKey : mouseUpEvent.altKey;
712
-
713
- this.plusButtonClicked(insertAbove);
714
- };
715
-
716
- document.addEventListener('mouseup', onMouseUp, true);
674
+ );
717
675
  }, true);
718
676
 
719
677
  /**
@@ -931,66 +889,35 @@ export class Toolbar extends Module<ToolbarNodes> {
931
889
  if (settingsToggler) {
932
890
  /**
933
891
  * Settings toggler mousedown handler
934
- * Stores the initial mouse position and sets up a document-level mouseup listener.
935
- * Using document-level mouseup ensures we catch the event even if the mouse
936
- * moves slightly off the toggler element during the click.
892
+ * Uses click-vs-drag detection to distinguish clicks from drags.
937
893
  */
938
894
  this.readOnlyMutableListeners.on(settingsToggler, 'mousedown', (e) => {
939
895
  hide();
940
896
 
941
- const mouseEvent = e as MouseEvent;
942
-
943
- /**
944
- * Store the mouse position when mousedown occurs
945
- * This will be used to determine if the user dragged or clicked
946
- */
947
- this.settingsTogglerMouseDownPosition = {
948
- x: mouseEvent.clientX,
949
- y: mouseEvent.clientY,
950
- };
951
-
952
- /**
953
- * Add document-level mouseup listener to catch the event even if mouse
954
- * moves slightly off the toggler. This is removed after firing once.
955
- */
956
- const onMouseUp = (mouseUpEvent: MouseEvent): void => {
957
- document.removeEventListener('mouseup', onMouseUp, true);
958
-
959
- /**
960
- * Ignore mouseup after a block drop to prevent settings menu from opening
961
- */
962
- if (this.ignoreNextSettingsMouseUp) {
963
- this.ignoreNextSettingsMouseUp = false;
964
- this.settingsTogglerMouseDownPosition = null;
965
-
966
- return;
967
- }
968
-
969
- const mouseDownPos = this.settingsTogglerMouseDownPosition;
970
-
971
- this.settingsTogglerMouseDownPosition = null;
972
-
973
- if (mouseDownPos === null) {
974
- return;
975
- }
976
-
977
- const wasDragged = (
978
- Math.abs(mouseUpEvent.clientX - mouseDownPos.x) > DRAG_THRESHOLD ||
979
- Math.abs(mouseUpEvent.clientY - mouseDownPos.y) > DRAG_THRESHOLD
980
- );
981
-
982
- if (wasDragged) {
983
- return;
984
- }
985
-
986
- this.settingsTogglerClicked();
987
-
988
- if (this.toolboxInstance?.opened) {
989
- this.toolboxInstance.close();
897
+ this.setupClickVsDrag(
898
+ e as MouseEvent,
899
+ () => {
900
+ this.settingsTogglerClicked();
901
+
902
+ if (this.toolboxInstance?.opened) {
903
+ this.toolboxInstance.close();
904
+ }
905
+ },
906
+ {
907
+ /**
908
+ * Check if we should ignore this mouseup (e.g., after a block drop)
909
+ */
910
+ beforeCallback: () => {
911
+ if (this.ignoreNextSettingsMouseUp) {
912
+ this.ignoreNextSettingsMouseUp = false;
913
+
914
+ return false;
915
+ }
916
+
917
+ return true;
918
+ },
990
919
  }
991
- };
992
-
993
- document.addEventListener('mouseup', onMouseUp, true);
920
+ );
994
921
  }, true);
995
922
  }
996
923
 
@@ -1302,6 +1229,50 @@ export class Toolbar extends Module<ToolbarNodes> {
1302
1229
  return container;
1303
1230
  }
1304
1231
 
1232
+ /**
1233
+ * Sets up a click-vs-drag detection pattern on an element.
1234
+ * Tracks mousedown position and fires callback only if mouse didn't move beyond threshold.
1235
+ * Uses document-level mouseup to catch events even if mouse moves off element.
1236
+ * @param element - Element to attach mousedown listener to
1237
+ * @param mouseEvent - The mousedown event
1238
+ * @param onClickCallback - Callback to fire if it was a click (not a drag)
1239
+ * @param options - Optional configuration
1240
+ * @param options.beforeCallback - Function called before click callback, return false to abort
1241
+ */
1242
+ private setupClickVsDrag(
1243
+ mouseEvent: MouseEvent,
1244
+ onClickCallback: (mouseUpEvent: MouseEvent) => void,
1245
+ options?: { beforeCallback?: () => boolean }
1246
+ ): void {
1247
+ const startPosition = {
1248
+ x: mouseEvent.clientX,
1249
+ y: mouseEvent.clientY,
1250
+ };
1251
+
1252
+ const onMouseUp = (mouseUpEvent: MouseEvent): void => {
1253
+ document.removeEventListener('mouseup', onMouseUp, true);
1254
+ this.pendingMouseUpListeners.delete(onMouseUp);
1255
+
1256
+ if (options?.beforeCallback && !options.beforeCallback()) {
1257
+ return;
1258
+ }
1259
+
1260
+ const wasDragged = (
1261
+ Math.abs(mouseUpEvent.clientX - startPosition.x) > DRAG_THRESHOLD ||
1262
+ Math.abs(mouseUpEvent.clientY - startPosition.y) > DRAG_THRESHOLD
1263
+ );
1264
+
1265
+ if (wasDragged) {
1266
+ return;
1267
+ }
1268
+
1269
+ onClickCallback(mouseUpEvent);
1270
+ };
1271
+
1272
+ this.pendingMouseUpListeners.add(onMouseUp);
1273
+ document.addEventListener('mouseup', onMouseUp, true);
1274
+ }
1275
+
1305
1276
  /**
1306
1277
  * Removes all created and saved HTMLElements
1307
1278
  * It is used in Read-Only mode
@@ -1311,5 +1282,15 @@ export class Toolbar extends Module<ToolbarNodes> {
1311
1282
  if (this.toolboxInstance) {
1312
1283
  this.toolboxInstance.destroy();
1313
1284
  }
1285
+
1286
+ /**
1287
+ * Clean up any pending document-level mouseup listeners.
1288
+ * These are added on mousedown and normally removed on mouseup,
1289
+ * but if the component is destroyed mid-click, they need manual cleanup.
1290
+ */
1291
+ for (const listener of this.pendingMouseUpListeners) {
1292
+ document.removeEventListener('mouseup', listener, true);
1293
+ }
1294
+ this.pendingMouseUpListeners.clear();
1314
1295
  }
1315
1296
  }