@haklex/rich-plugin-toolbar 0.57.0 → 0.59.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.mjs CHANGED
@@ -1,13 +1,15 @@
1
1
  import { $isListNode, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from "@lexical/list";
2
- import { $createHeadingNode, $isHeadingNode } from "@lexical/rich-text";
2
+ import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode } from "@lexical/rich-text";
3
3
  import { $getSelectionStyleValueForProperty, $patchStyleText, $setBlocksType } from "@lexical/selection";
4
4
  import { $createParagraphNode, $getSelection, $isElementNode, $isRangeSelection, $isRootOrShadowRoot, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, REDO_COMMAND, UNDO_COMMAND } from "lexical";
5
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger, createTooltipHandle } from "@haklex/rich-editor-ui";
5
+ import { TURN_INTO_COMMAND_IDS, getCommandMeta, normalizeCommandItems } from "@haklex/rich-editor/command-registry";
6
+ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, Popover, PopoverPanel, PopoverTrigger, TextInput, TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger, createTooltipHandle } from "@haklex/rich-editor-ui";
6
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
- import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, ChevronDown, Code, Ellipsis, EyeOff, Heading1, Heading2, Heading3, Highlighter, Italic, List, ListChecks, ListOrdered, Pilcrow, Redo, Strikethrough, Underline, Undo } from "lucide-react";
8
+ import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, ChevronDown, Code, Ellipsis, EyeOff, Highlighter, Italic, Plus, Redo, Search, Strikethrough, Underline, Undo } from "lucide-react";
8
9
  import { $selectionTouchesSpoiler, $toggleSpoilerSelection, collectCommandItems } from "@haklex/rich-editor/commands";
10
+ import { SlashCommandList, filterSlashItems, getDefaultSlashItems, slashCommandItemId } from "@haklex/rich-plugin-slash-menu";
9
11
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
10
- import { useCallback, useEffect, useMemo, useState } from "react";
12
+ import { createElement, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
11
13
  import { $findMatchingParent } from "@lexical/utils";
12
14
  //#region src/actions.ts
13
15
  /**
@@ -27,7 +29,10 @@ function getBlockType(anchorNode) {
27
29
  if (listType === "check") return "check";
28
30
  return "other";
29
31
  }
30
- if (anchorNode.getType() === "paragraph") return "paragraph";
32
+ if ($isQuoteNode(anchorNode)) return "quote";
33
+ const type = anchorNode.getType();
34
+ if (type === "paragraph") return "paragraph";
35
+ if (type === "code") return "code";
31
36
  return "other";
32
37
  }
33
38
  /**
@@ -54,9 +59,29 @@ function applyBlockType(editor, type) {
54
59
  $setBlocksType(selection, () => $createParagraphNode());
55
60
  return;
56
61
  }
57
- if (type === "h1" || type === "h2" || type === "h3") $setBlocksType(selection, () => $createHeadingNode(type));
62
+ if (type === "h1" || type === "h2" || type === "h3") {
63
+ $setBlocksType(selection, () => $createHeadingNode(type));
64
+ return;
65
+ }
66
+ if (type === "quote") {
67
+ $setBlocksType(selection, () => $createQuoteNode());
68
+ return;
69
+ }
70
+ if (type === "code") $replaceWithCodeBlock(editor, selection);
58
71
  });
59
72
  }
73
+ function getCodeBlockKlass(editor) {
74
+ return editor._nodes.get("code-block")?.klass ?? null;
75
+ }
76
+ function $replaceWithCodeBlock(editor, selection) {
77
+ const CodeBlock = getCodeBlockKlass(editor);
78
+ if (!CodeBlock) return;
79
+ const blocks = [...new Set(selection.getNodes().concat(selection.anchor.getNode()).map((node) => node.getTopLevelElement()).filter((node) => node !== null))];
80
+ if (blocks.length === 0) return;
81
+ const code = blocks.map((block) => block.getTextContent()).join("\n");
82
+ blocks[0].replace(new CodeBlock(code, "text"));
83
+ for (const block of blocks.slice(1)) block.remove();
84
+ }
60
85
  /**
61
86
  * Patch the `font-family` style on the current selection.
62
87
  * Passing an empty string clears the override and inherits the editor default.
@@ -106,16 +131,8 @@ function getFontLabel(fontFamily) {
106
131
  for (const def of FONT_FAMILIES) if (def.value && fontFamily.startsWith(def.value.split(",")[0])) return def.label;
107
132
  return "默认";
108
133
  }
109
- var BLOCK_TYPE_LABELS = {
110
- paragraph: "Text",
111
- h1: "Heading 1",
112
- h2: "Heading 2",
113
- h3: "Heading 3",
114
- bullet: "Bulleted List",
115
- number: "Numbered List",
116
- check: "To-do List",
117
- other: "Other"
118
- };
134
+ var TURN_INTO_BLOCK_TYPES = TURN_INTO_COMMAND_IDS;
135
+ var BLOCK_TYPE_LABELS = Object.fromEntries([...TURN_INTO_BLOCK_TYPES.map((type) => [type, getCommandMeta(type).title]), ["other", "Other"]]);
119
136
  //#endregion
120
137
  //#region src/styles.css.ts
121
138
  var toolbarContainer = "_1e1ersc0";
@@ -125,7 +142,9 @@ var toolbarButtonActive = "_1e1ersc3";
125
142
  var toolbarDropdownTrigger = "_1e1ersc4";
126
143
  var toolbarDropdownTriggerChevron = "_1e1ersc5";
127
144
  var toolbarSeparator = "_1e1ersc6";
128
- var toolbarDropdownItemActive = "_1e1ersc7";
145
+ var insertSearch = "_1e1ersc7";
146
+ var insertSearchIcon = "_1e1ersc8";
147
+ var insertSearchInput = "_1e1ersc9";
129
148
  //#endregion
130
149
  //#region src/ToolbarButton.tsx
131
150
  function ToolbarButton({ icon, title, shortcut, active, disabled, onClick, tooltipHandle }) {
@@ -135,10 +154,8 @@ function ToolbarButton({ icon, title, shortcut, active, disabled, onClick, toolt
135
154
  className: `${toolbarButton}${active ? ` ${toolbarButtonActive}` : ""}`,
136
155
  disabled,
137
156
  type: "button",
138
- onMouseDown: (e) => {
139
- e.preventDefault();
140
- onClick();
141
- }
157
+ onClick,
158
+ onMouseDown: (e) => e.preventDefault()
142
159
  });
143
160
  if (tooltipHandle) return /* @__PURE__ */ jsx(TooltipTrigger, {
144
161
  handle: tooltipHandle,
@@ -152,13 +169,11 @@ function ToolbarButton({ icon, title, shortcut, active, disabled, onClick, toolt
152
169
  return /* @__PURE__ */ jsxs(TooltipRoot, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
153
170
  render: button,
154
171
  children: icon
155
- }), /* @__PURE__ */ jsxs(TooltipContent, {
172
+ }), /* @__PURE__ */ jsx(TooltipContent, {
173
+ shortcut,
156
174
  side: "bottom",
157
175
  sideOffset: 4,
158
- children: [title, shortcut && /* @__PURE__ */ jsx("span", {
159
- className: "_1e1ersc8",
160
- children: shortcut
161
- })]
176
+ children: title
162
177
  })] });
163
178
  }
164
179
  //#endregion
@@ -171,24 +186,21 @@ function ToolbarDropdown({ label, title, items, triggerWidth, tooltipHandle }) {
171
186
  });
172
187
  const triggerContent = /* @__PURE__ */ jsxs(Fragment, { children: [label, /* @__PURE__ */ jsx("span", {
173
188
  className: toolbarDropdownTriggerChevron,
174
- children: /* @__PURE__ */ jsx(ChevronDown, { size: 12 })
189
+ children: /* @__PURE__ */ jsx(ChevronDown, { size: 14 })
175
190
  })] });
191
+ const activeLabel = items.find((item) => item.active)?.label ?? null;
176
192
  const menu = /* @__PURE__ */ jsx(DropdownMenuContent, {
177
193
  sideOffset: 4,
178
- children: items.map((item) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
179
- className: item.active ? toolbarDropdownItemActive : void 0,
180
- style: item.style,
181
- onClick: item.onSelect,
182
- children: [item.icon && /* @__PURE__ */ jsx("span", {
183
- style: {
184
- marginRight: 8,
185
- display: "inline-flex",
186
- transform: "scale(0.8)",
187
- transformOrigin: "left center"
188
- },
189
- children: item.icon
190
- }), item.label]
191
- }, item.label))
194
+ children: /* @__PURE__ */ jsx(DropdownMenuRadioGroup, {
195
+ value: activeLabel,
196
+ children: items.map((item) => /* @__PURE__ */ jsxs(DropdownMenuRadioItem, {
197
+ closeOnClick: true,
198
+ style: item.style,
199
+ value: item.label,
200
+ onClick: item.onSelect,
201
+ children: [item.icon, item.label]
202
+ }, item.label))
203
+ })
192
204
  });
193
205
  if (tooltipHandle) return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
194
206
  handle: tooltipHandle,
@@ -206,10 +218,359 @@ function ToolbarDropdown({ label, title, items, triggerWidth, tooltipHandle }) {
206
218
  })] }), menu] });
207
219
  }
208
220
  //#endregion
221
+ //#region src/block-types.tsx
222
+ function blockTypeItems(editor, active) {
223
+ const hasCodeBlock = getCodeBlockKlass(editor) !== null;
224
+ return TURN_INTO_BLOCK_TYPES.filter((type) => type !== "code" || hasCodeBlock || active === "code").map((type) => {
225
+ const meta = getCommandMeta(type);
226
+ return {
227
+ label: meta.title,
228
+ icon: createElement(meta.icon, { size: 16 }),
229
+ active: active === type,
230
+ onSelect: () => applyBlockType(editor, type)
231
+ };
232
+ });
233
+ }
234
+ //#endregion
209
235
  //#region src/ToolbarSeparator.tsx
210
236
  function ToolbarSeparator() {
211
237
  return /* @__PURE__ */ jsx("div", { className: toolbarSeparator });
212
238
  }
239
+ var SEPARATOR_WIDTH = 13;
240
+ function separatorEntry(key) {
241
+ return {
242
+ key,
243
+ node: /* @__PURE__ */ jsx(ToolbarSeparator, {}, key),
244
+ separator: true,
245
+ width: SEPARATOR_WIDTH
246
+ };
247
+ }
248
+ function buttonEntry(spec, handle) {
249
+ return {
250
+ key: spec.key,
251
+ width: 28,
252
+ menu: { items: [spec] },
253
+ node: /* @__PURE__ */ jsx(ToolbarButton, {
254
+ active: spec.active,
255
+ disabled: spec.disabled,
256
+ icon: spec.icon,
257
+ shortcut: spec.shortcut,
258
+ title: spec.label,
259
+ tooltipHandle: handle,
260
+ onClick: spec.onSelect
261
+ }, spec.key)
262
+ };
263
+ }
264
+ function dropdownEntry(key, options, handle) {
265
+ return {
266
+ key,
267
+ width: options.width,
268
+ menu: {
269
+ label: options.title,
270
+ items: options.items.map((item) => ({
271
+ ...item,
272
+ onSelect: item.onSelect
273
+ }))
274
+ },
275
+ node: /* @__PURE__ */ jsx(ToolbarDropdown, {
276
+ items: options.items,
277
+ label: options.label,
278
+ title: options.title,
279
+ tooltipHandle: handle,
280
+ triggerWidth: options.width
281
+ }, key)
282
+ };
283
+ }
284
+ var icon = (Icon) => /* @__PURE__ */ jsx(Icon, {
285
+ size: 16,
286
+ strokeWidth: 2
287
+ });
288
+ function formattingButtons(editor, state) {
289
+ const text = (format) => () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
290
+ const align = (format) => () => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, format);
291
+ return [
292
+ [{
293
+ key: "undo",
294
+ label: "Undo",
295
+ icon: icon(Undo),
296
+ shortcut: "Mod+Z",
297
+ disabled: !state.canUndo,
298
+ onSelect: () => editor.dispatchCommand(UNDO_COMMAND, void 0)
299
+ }, {
300
+ key: "redo",
301
+ label: "Redo",
302
+ icon: icon(Redo),
303
+ shortcut: "Mod+Shift+Z",
304
+ disabled: !state.canRedo,
305
+ onSelect: () => editor.dispatchCommand(REDO_COMMAND, void 0)
306
+ }],
307
+ [
308
+ {
309
+ key: "bold",
310
+ label: "Bold",
311
+ icon: icon(Bold),
312
+ shortcut: "Mod+B",
313
+ active: state.isBold,
314
+ onSelect: text("bold")
315
+ },
316
+ {
317
+ key: "italic",
318
+ label: "Italic",
319
+ icon: icon(Italic),
320
+ shortcut: "Mod+I",
321
+ active: state.isItalic,
322
+ onSelect: text("italic")
323
+ },
324
+ {
325
+ key: "underline",
326
+ label: "Underline",
327
+ icon: icon(Underline),
328
+ shortcut: "Mod+U",
329
+ active: state.isUnderline,
330
+ onSelect: text("underline")
331
+ },
332
+ {
333
+ key: "strikethrough",
334
+ label: "Strikethrough",
335
+ icon: icon(Strikethrough),
336
+ active: state.isStrikethrough,
337
+ onSelect: text("strikethrough")
338
+ },
339
+ {
340
+ key: "code",
341
+ label: "Inline code",
342
+ icon: icon(Code),
343
+ active: state.isCode,
344
+ onSelect: text("code")
345
+ }
346
+ ],
347
+ [{
348
+ key: "highlight",
349
+ label: "Highlight",
350
+ icon: icon(Highlighter),
351
+ active: state.isHighlight,
352
+ onSelect: text("highlight")
353
+ }, {
354
+ key: "spoiler",
355
+ label: "Spoiler",
356
+ icon: icon(EyeOff),
357
+ active: state.isSpoiler,
358
+ onSelect: () => editor.update($toggleSpoilerSelection)
359
+ }],
360
+ [
361
+ {
362
+ key: "bullet",
363
+ label: getCommandMeta("bullet").title,
364
+ icon: icon(getCommandMeta("bullet").icon),
365
+ active: state.blockType === "bullet",
366
+ onSelect: () => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0)
367
+ },
368
+ {
369
+ key: "number",
370
+ label: getCommandMeta("number").title,
371
+ icon: icon(getCommandMeta("number").icon),
372
+ active: state.blockType === "number",
373
+ onSelect: () => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0)
374
+ },
375
+ {
376
+ key: "check",
377
+ label: getCommandMeta("check").title,
378
+ icon: icon(getCommandMeta("check").icon),
379
+ active: state.blockType === "check",
380
+ onSelect: () => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, void 0)
381
+ }
382
+ ],
383
+ [
384
+ {
385
+ key: "align-left",
386
+ label: "Align left",
387
+ icon: icon(AlignLeft),
388
+ active: state.elementFormat === "left" || state.elementFormat === "",
389
+ onSelect: align("left")
390
+ },
391
+ {
392
+ key: "align-center",
393
+ label: "Align center",
394
+ icon: icon(AlignCenter),
395
+ active: state.elementFormat === "center",
396
+ onSelect: align("center")
397
+ },
398
+ {
399
+ key: "align-right",
400
+ label: "Align right",
401
+ icon: icon(AlignRight),
402
+ active: state.elementFormat === "right",
403
+ onSelect: align("right")
404
+ },
405
+ {
406
+ key: "align-justify",
407
+ label: "Justify",
408
+ icon: icon(AlignJustify),
409
+ active: state.elementFormat === "justify",
410
+ onSelect: align("justify")
411
+ }
412
+ ]
413
+ ];
414
+ }
415
+ //#endregion
416
+ //#region src/InsertMenu.tsx
417
+ var TITLE = "Insert";
418
+ function InsertMenu({ handle, items, onSelect }) {
419
+ const [open, setOpen] = useState(false);
420
+ const [query, setQuery] = useState("");
421
+ const [highlighted, setHighlighted] = useState(0);
422
+ const listId = useId();
423
+ const filtered = useMemo(() => filterSlashItems(query, items), [query, items]);
424
+ const activeIndex = filtered.length === 0 ? null : Math.min(highlighted, filtered.length - 1);
425
+ useEffect(() => {
426
+ if (activeIndex === null) return;
427
+ filtered[activeIndex]?.ref?.current?.scrollIntoView?.({ block: "nearest" });
428
+ }, [activeIndex, filtered]);
429
+ const onOpenChange = (next) => {
430
+ setOpen(next);
431
+ if (!next) {
432
+ setQuery("");
433
+ setHighlighted(0);
434
+ }
435
+ };
436
+ const select = (item) => {
437
+ onOpenChange(false);
438
+ onSelect(item);
439
+ };
440
+ const onKeyDown = (event) => {
441
+ if (filtered.length === 0) return;
442
+ const current = activeIndex ?? 0;
443
+ if (event.key === "ArrowDown") {
444
+ event.preventDefault();
445
+ setHighlighted((current + 1) % filtered.length);
446
+ } else if (event.key === "ArrowUp") {
447
+ event.preventDefault();
448
+ setHighlighted((current - 1 + filtered.length) % filtered.length);
449
+ } else if (event.key === "Enter" && !event.nativeEvent.isComposing) {
450
+ event.preventDefault();
451
+ select(filtered[current]);
452
+ }
453
+ };
454
+ return /* @__PURE__ */ jsxs(Popover, {
455
+ open,
456
+ onOpenChange,
457
+ children: [handle ? /* @__PURE__ */ jsx(TooltipTrigger, {
458
+ handle,
459
+ payload: { title: TITLE },
460
+ render: /* @__PURE__ */ jsx(PopoverTrigger, {
461
+ "aria-label": TITLE,
462
+ className: toolbarButton,
463
+ render: /* @__PURE__ */ jsx("button", { type: "button" })
464
+ }),
465
+ children: /* @__PURE__ */ jsx(Plus, { size: 16 })
466
+ }) : /* @__PURE__ */ jsx(PopoverTrigger, {
467
+ "aria-label": TITLE,
468
+ className: toolbarButton,
469
+ render: /* @__PURE__ */ jsx("button", { type: "button" }),
470
+ children: /* @__PURE__ */ jsx(Plus, { size: 16 })
471
+ }), /* @__PURE__ */ jsxs(PopoverPanel, {
472
+ flush: true,
473
+ align: "start",
474
+ side: "bottom",
475
+ size: "md",
476
+ children: [/* @__PURE__ */ jsxs("div", {
477
+ className: insertSearch,
478
+ children: [/* @__PURE__ */ jsx(Search, {
479
+ "aria-hidden": true,
480
+ className: insertSearchIcon,
481
+ size: 14
482
+ }), /* @__PURE__ */ jsx(TextInput, {
483
+ "aria-controls": listId,
484
+ "aria-label": "Search blocks",
485
+ className: insertSearchInput,
486
+ placeholder: "Search blocks",
487
+ type: "search",
488
+ value: query,
489
+ "aria-activedescendant": activeIndex === null ? void 0 : slashCommandItemId(listId, activeIndex),
490
+ onKeyDown,
491
+ onChange: (event) => {
492
+ setQuery(event.target.value);
493
+ setHighlighted(0);
494
+ }
495
+ })]
496
+ }), /* @__PURE__ */ jsx(SlashCommandList, {
497
+ id: listId,
498
+ items: filtered,
499
+ query,
500
+ selectedIndex: activeIndex,
501
+ variant: "embedded",
502
+ onHighlight: setHighlighted,
503
+ onSelect: select
504
+ })]
505
+ })]
506
+ });
507
+ }
508
+ //#endregion
509
+ //#region src/overflow.ts
510
+ function runWidth(entries, count, gap) {
511
+ let total = 0;
512
+ for (let i = 0; i < count; i++) total += entries[i].width + (i > 0 ? gap : 0);
513
+ return total;
514
+ }
515
+ function fitCount(entries, { available, gap, reserve }) {
516
+ if (available === null || runWidth(entries, entries.length, gap) <= available) return entries.length;
517
+ let count = 0;
518
+ while (count < entries.length && runWidth(entries, count + 1, gap) + gap + reserve <= available) count++;
519
+ while (count > 0 && entries[count - 1].separator) count--;
520
+ return count;
521
+ }
522
+ //#endregion
523
+ //#region src/ToolbarOverflowMenu.tsx
524
+ function ToolbarMenuButton({ children, handle, title }) {
525
+ return /* @__PURE__ */ jsx(TooltipTrigger, {
526
+ handle,
527
+ payload: { title },
528
+ render: /* @__PURE__ */ jsx(DropdownMenuTrigger, {
529
+ "aria-label": title,
530
+ className: toolbarButton,
531
+ render: /* @__PURE__ */ jsx("button", { type: "button" })
532
+ }),
533
+ children
534
+ });
535
+ }
536
+ function ToolbarMenuItems({ items }) {
537
+ return /* @__PURE__ */ jsx(Fragment, { children: items.map((item) => item.active === void 0 ? /* @__PURE__ */ jsxs(DropdownMenuItem, {
538
+ disabled: item.disabled,
539
+ shortcut: item.shortcut,
540
+ onClick: item.onSelect,
541
+ children: [item.icon, item.label]
542
+ }, item.label) : /* @__PURE__ */ jsxs(DropdownMenuCheckboxItem, {
543
+ closeOnClick: true,
544
+ checked: item.active,
545
+ disabled: item.disabled,
546
+ onClick: item.onSelect,
547
+ children: [item.icon, item.label]
548
+ }, item.label)) });
549
+ }
550
+ function ToolbarOverflowMenu({ entries, handle, icon }) {
551
+ const sections = entries.filter((entry) => entry.separator || entry.menu);
552
+ return /* @__PURE__ */ jsxs(DropdownMenu, {
553
+ modal: false,
554
+ children: [/* @__PURE__ */ jsx(ToolbarMenuButton, {
555
+ handle,
556
+ title: "More tools",
557
+ children: icon
558
+ }), /* @__PURE__ */ jsx(DropdownMenuContent, {
559
+ align: "end",
560
+ positionMethod: "fixed",
561
+ sideOffset: 4,
562
+ children: sections.map((entry, index) => {
563
+ if (entry.separator) {
564
+ const prev = sections[index - 1];
565
+ return prev && !prev.separator && index < sections.length - 1 ? /* @__PURE__ */ jsx(DropdownMenuSeparator, {}, entry.key) : null;
566
+ }
567
+ const menu = entry.menu;
568
+ if (!menu.label) return /* @__PURE__ */ jsx(ToolbarMenuItems, { items: menu.items }, entry.key);
569
+ return /* @__PURE__ */ jsxs(DropdownMenuGroup, { children: [/* @__PURE__ */ jsx(DropdownMenuLabel, { children: menu.label }), /* @__PURE__ */ jsx(ToolbarMenuItems, { items: menu.items })] }, entry.key);
570
+ })
571
+ })]
572
+ });
573
+ }
213
574
  //#endregion
214
575
  //#region src/use-toolbar-state.ts
215
576
  var INITIAL_TOOLBAR_STATE = {
@@ -300,334 +661,125 @@ function useToolbarState() {
300
661
  }
301
662
  //#endregion
302
663
  //#region src/ToolbarPlugin.tsx
303
- var ICON_SIZE = 15;
304
- var ICON_STROKE = 2;
305
664
  var DEFAULT_MAX_VISIBLE_INSERT_ITEMS = 5;
665
+ function useAvailableWidth() {
666
+ const ref = useRef(null);
667
+ const [width, setWidth] = useState(null);
668
+ useLayoutEffect(() => {
669
+ const el = ref.current;
670
+ if (!el) return;
671
+ const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));
672
+ observer.observe(el);
673
+ return () => observer.disconnect();
674
+ }, []);
675
+ return [ref, width];
676
+ }
306
677
  function ToolbarPlugin({ className, maxVisibleInsertItems = DEFAULT_MAX_VISIBLE_INSERT_ITEMS, insertItemOrder }) {
307
678
  const [editor] = useLexicalComposerContext();
308
679
  const state = useToolbarState();
309
- const [tooltipHandle] = useState(() => createTooltipHandle());
310
- const toolbarItems = useMemo(() => {
311
- const items = collectCommandItems(editor).filter((item) => item.placement?.includes("toolbar") && item.group === "insert");
680
+ const [handle] = useState(() => createTooltipHandle());
681
+ const [rowRef, available] = useAvailableWidth();
682
+ const insertItems = useMemo(() => {
683
+ const items = normalizeCommandItems(collectCommandItems(editor).filter((item) => item.placement?.includes("toolbar") && item.group === "insert"));
312
684
  if (!insertItemOrder || insertItemOrder.length === 0) return items;
685
+ const order = insertItemOrder.map((title) => getCommandMeta(title)?.title ?? title);
313
686
  return items.sort((a, b) => {
314
- const ai = insertItemOrder.indexOf(a.title);
315
- const bi = insertItemOrder.indexOf(b.title);
687
+ const ai = order.indexOf(a.title);
688
+ const bi = order.indexOf(b.title);
316
689
  return (ai === -1 ? Infinity : ai) - (bi === -1 ? Infinity : bi);
317
690
  });
318
691
  }, [editor, insertItemOrder]);
319
- const fontFamilyItems = useMemo(() => FONT_FAMILIES.map((def) => ({
320
- label: def.label,
321
- active: state.fontFamily === def.value,
322
- style: def.value ? { fontFamily: def.value } : void 0,
323
- onSelect: () => applyFontFamily(editor, def.value)
324
- })), [editor, state.fontFamily]);
325
- const headingItems = useMemo(() => [
326
- {
327
- label: "Text",
328
- icon: /* @__PURE__ */ jsx(Pilcrow, {
329
- size: ICON_SIZE,
330
- strokeWidth: ICON_STROKE
331
- }),
332
- active: state.blockType === "paragraph",
333
- onSelect: () => {
334
- editor.update(() => {
335
- const selection = $getSelection();
336
- if ($isRangeSelection(selection)) $setBlocksType(selection, () => $createParagraphNode());
337
- });
338
- }
339
- },
340
- {
341
- label: "Heading 1",
342
- icon: /* @__PURE__ */ jsx(Heading1, {
343
- size: ICON_SIZE,
344
- strokeWidth: ICON_STROKE
345
- }),
346
- active: state.blockType === "h1",
347
- onSelect: () => {
348
- editor.update(() => {
349
- const selection = $getSelection();
350
- if ($isRangeSelection(selection)) $setBlocksType(selection, () => $createHeadingNode("h1"));
351
- });
352
- }
353
- },
354
- {
355
- label: "Heading 2",
356
- icon: /* @__PURE__ */ jsx(Heading2, {
357
- size: ICON_SIZE,
358
- strokeWidth: ICON_STROKE
359
- }),
360
- active: state.blockType === "h2",
361
- onSelect: () => {
362
- editor.update(() => {
363
- const selection = $getSelection();
364
- if ($isRangeSelection(selection)) $setBlocksType(selection, () => $createHeadingNode("h2"));
365
- });
366
- }
367
- },
368
- {
369
- label: "Heading 3",
370
- icon: /* @__PURE__ */ jsx(Heading3, {
371
- size: ICON_SIZE,
372
- strokeWidth: ICON_STROKE
373
- }),
374
- active: state.blockType === "h3",
375
- onSelect: () => {
376
- editor.update(() => {
377
- const selection = $getSelection();
378
- if ($isRangeSelection(selection)) $setBlocksType(selection, () => $createHeadingNode("h3"));
379
- });
380
- }
692
+ const slashItems = useMemo(() => getDefaultSlashItems(editor), [editor]);
693
+ const entries = useMemo(() => {
694
+ const list = [
695
+ dropdownEntry("font", {
696
+ title: "Font family",
697
+ label: getFontLabel(state.fontFamily),
698
+ width: 76,
699
+ items: FONT_FAMILIES.map((def) => ({
700
+ label: def.label,
701
+ active: state.fontFamily === def.value,
702
+ style: def.value ? { fontFamily: def.value } : void 0,
703
+ onSelect: () => applyFontFamily(editor, def.value)
704
+ }))
705
+ }, handle),
706
+ separatorEntry("sep-font"),
707
+ dropdownEntry("block", {
708
+ title: "Block type",
709
+ label: BLOCK_TYPE_LABELS[state.blockType] ?? "Text",
710
+ width: 112,
711
+ items: blockTypeItems(editor, state.blockType)
712
+ }, handle)
713
+ ];
714
+ for (const [index, group] of formattingButtons(editor, state).entries()) {
715
+ list.push(separatorEntry(`sep-${index}`));
716
+ for (const spec of group) list.push(buttonEntry(spec, handle));
381
717
  }
382
- ], [editor, state.blockType]);
383
- const containerClassName = className ? `${toolbarContainer} ${className}` : toolbarContainer;
384
- const h = tooltipHandle;
718
+ list.push(separatorEntry("sep-insert"));
719
+ for (const item of insertItems.slice(0, maxVisibleInsertItems)) list.push(buttonEntry({
720
+ key: `insert-${item.title}`,
721
+ label: item.title,
722
+ icon: item.icon,
723
+ shortcut: item.shortcut,
724
+ onSelect: () => item.onSelect(editor, "")
725
+ }, handle));
726
+ const rest = insertItems.slice(maxVisibleInsertItems).map((item) => ({
727
+ label: item.title,
728
+ icon: item.icon,
729
+ shortcut: item.shortcut,
730
+ onSelect: () => item.onSelect(editor, "")
731
+ }));
732
+ list.push({
733
+ key: "insert-menu",
734
+ width: 28,
735
+ menu: rest.length > 0 ? {
736
+ label: "Insert",
737
+ items: rest
738
+ } : void 0,
739
+ node: /* @__PURE__ */ jsx(InsertMenu, {
740
+ handle,
741
+ items: slashItems,
742
+ onSelect: (item) => item.onSelect(editor, "")
743
+ }, "insert-menu")
744
+ });
745
+ return list;
746
+ }, [
747
+ editor,
748
+ state,
749
+ handle,
750
+ insertItems,
751
+ maxVisibleInsertItems,
752
+ slashItems
753
+ ]);
754
+ const visibleCount = fitCount(entries, {
755
+ available,
756
+ gap: 2,
757
+ reserve: 28
758
+ });
759
+ const hidden = entries.slice(visibleCount);
385
760
  return /* @__PURE__ */ jsxs(TooltipProvider, {
386
761
  delay: 300,
387
762
  children: [/* @__PURE__ */ jsx(TooltipRoot, {
388
763
  disableHoverablePopup: true,
389
- handle: tooltipHandle,
390
- children: ((props) => props.payload !== void 0 ? /* @__PURE__ */ jsxs(TooltipContent, {
764
+ handle,
765
+ children: ((props) => props.payload !== void 0 ? /* @__PURE__ */ jsx(TooltipContent, {
766
+ shortcut: props.payload.shortcut,
391
767
  side: "bottom",
392
768
  sideOffset: 4,
393
- children: [props.payload.title, props.payload.shortcut && /* @__PURE__ */ jsx("span", {
394
- className: "_1e1ersc8",
395
- children: props.payload.shortcut
396
- })]
769
+ children: props.payload.title
397
770
  }) : null)
398
771
  }), /* @__PURE__ */ jsx("div", {
399
772
  "aria-label": "Editor toolbar",
400
- className: containerClassName,
773
+ className: className ? `${toolbarContainer} ${className}` : toolbarContainer,
401
774
  role: "toolbar",
402
775
  children: /* @__PURE__ */ jsxs("div", {
403
776
  className: toolbarRow,
404
- children: [
405
- /* @__PURE__ */ jsx(ToolbarDropdown, {
406
- items: fontFamilyItems,
407
- label: getFontLabel(state.fontFamily),
408
- title: "Font family",
409
- tooltipHandle: h,
410
- triggerWidth: 76
411
- }),
412
- /* @__PURE__ */ jsx(ToolbarSeparator, {}),
413
- /* @__PURE__ */ jsx(ToolbarDropdown, {
414
- items: headingItems,
415
- label: BLOCK_TYPE_LABELS[state.blockType] ?? "Text",
416
- title: "Block type",
417
- tooltipHandle: h,
418
- triggerWidth: 120
419
- }),
420
- /* @__PURE__ */ jsx(ToolbarSeparator, {}),
421
- /* @__PURE__ */ jsx(ToolbarButton, {
422
- disabled: !state.canUndo,
423
- icon: /* @__PURE__ */ jsx(Undo, {
424
- size: ICON_SIZE,
425
- strokeWidth: ICON_STROKE
426
- }),
427
- shortcut: "Ctrl+Z",
428
- title: "Undo",
429
- tooltipHandle: h,
430
- onClick: () => editor.dispatchCommand(UNDO_COMMAND, void 0)
431
- }),
432
- /* @__PURE__ */ jsx(ToolbarButton, {
433
- disabled: !state.canRedo,
434
- icon: /* @__PURE__ */ jsx(Redo, {
435
- size: ICON_SIZE,
436
- strokeWidth: ICON_STROKE
437
- }),
438
- shortcut: "Ctrl+Y",
439
- title: "Redo",
440
- tooltipHandle: h,
441
- onClick: () => editor.dispatchCommand(REDO_COMMAND, void 0)
442
- }),
443
- /* @__PURE__ */ jsx(ToolbarSeparator, {}),
444
- /* @__PURE__ */ jsx(ToolbarButton, {
445
- active: state.isBold,
446
- icon: /* @__PURE__ */ jsx(Bold, {
447
- size: ICON_SIZE,
448
- strokeWidth: ICON_STROKE
449
- }),
450
- shortcut: "Ctrl+B",
451
- title: "Bold",
452
- tooltipHandle: h,
453
- onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")
454
- }),
455
- /* @__PURE__ */ jsx(ToolbarButton, {
456
- active: state.isItalic,
457
- icon: /* @__PURE__ */ jsx(Italic, {
458
- size: ICON_SIZE,
459
- strokeWidth: ICON_STROKE
460
- }),
461
- shortcut: "Ctrl+I",
462
- title: "Italic",
463
- tooltipHandle: h,
464
- onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic")
465
- }),
466
- /* @__PURE__ */ jsx(ToolbarButton, {
467
- active: state.isUnderline,
468
- icon: /* @__PURE__ */ jsx(Underline, {
469
- size: ICON_SIZE,
470
- strokeWidth: ICON_STROKE
471
- }),
472
- shortcut: "Ctrl+U",
473
- title: "Underline",
474
- tooltipHandle: h,
475
- onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline")
476
- }),
477
- /* @__PURE__ */ jsx(ToolbarButton, {
478
- active: state.isStrikethrough,
479
- icon: /* @__PURE__ */ jsx(Strikethrough, {
480
- size: ICON_SIZE,
481
- strokeWidth: ICON_STROKE
482
- }),
483
- title: "Strikethrough",
484
- tooltipHandle: h,
485
- onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough")
486
- }),
487
- /* @__PURE__ */ jsx(ToolbarButton, {
488
- active: state.isCode,
489
- icon: /* @__PURE__ */ jsx(Code, {
490
- size: ICON_SIZE,
491
- strokeWidth: ICON_STROKE
492
- }),
493
- title: "Inline Code",
494
- tooltipHandle: h,
495
- onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code")
496
- }),
497
- /* @__PURE__ */ jsx(ToolbarSeparator, {}),
498
- /* @__PURE__ */ jsx(ToolbarButton, {
499
- active: state.isHighlight,
500
- icon: /* @__PURE__ */ jsx(Highlighter, {
501
- size: ICON_SIZE,
502
- strokeWidth: ICON_STROKE
503
- }),
504
- title: "Highlight",
505
- tooltipHandle: h,
506
- onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "highlight")
507
- }),
508
- /* @__PURE__ */ jsx(ToolbarButton, {
509
- active: state.isSpoiler,
510
- icon: /* @__PURE__ */ jsx(EyeOff, {
511
- size: ICON_SIZE,
512
- strokeWidth: ICON_STROKE
513
- }),
514
- title: "Spoiler",
515
- tooltipHandle: h,
516
- onClick: () => editor.update($toggleSpoilerSelection)
517
- }),
518
- /* @__PURE__ */ jsx(ToolbarSeparator, {}),
519
- /* @__PURE__ */ jsx(ToolbarButton, {
520
- active: state.blockType === "bullet",
521
- icon: /* @__PURE__ */ jsx(List, {
522
- size: ICON_SIZE,
523
- strokeWidth: ICON_STROKE
524
- }),
525
- title: "Bulleted List",
526
- tooltipHandle: h,
527
- onClick: () => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0)
528
- }),
529
- /* @__PURE__ */ jsx(ToolbarButton, {
530
- active: state.blockType === "number",
531
- icon: /* @__PURE__ */ jsx(ListOrdered, {
532
- size: ICON_SIZE,
533
- strokeWidth: ICON_STROKE
534
- }),
535
- title: "Numbered List",
536
- tooltipHandle: h,
537
- onClick: () => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0)
538
- }),
539
- /* @__PURE__ */ jsx(ToolbarButton, {
540
- active: state.blockType === "check",
541
- icon: /* @__PURE__ */ jsx(ListChecks, {
542
- size: ICON_SIZE,
543
- strokeWidth: ICON_STROKE
544
- }),
545
- title: "Checklist",
546
- tooltipHandle: h,
547
- onClick: () => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, void 0)
548
- }),
549
- /* @__PURE__ */ jsx(ToolbarSeparator, {}),
550
- /* @__PURE__ */ jsx(ToolbarButton, {
551
- active: state.elementFormat === "left" || state.elementFormat === "",
552
- icon: /* @__PURE__ */ jsx(AlignLeft, {
553
- size: ICON_SIZE,
554
- strokeWidth: ICON_STROKE
555
- }),
556
- title: "Align Left",
557
- tooltipHandle: h,
558
- onClick: () => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "left")
559
- }),
560
- /* @__PURE__ */ jsx(ToolbarButton, {
561
- active: state.elementFormat === "center",
562
- icon: /* @__PURE__ */ jsx(AlignCenter, {
563
- size: ICON_SIZE,
564
- strokeWidth: ICON_STROKE
565
- }),
566
- title: "Align Center",
567
- tooltipHandle: h,
568
- onClick: () => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "center")
569
- }),
570
- /* @__PURE__ */ jsx(ToolbarButton, {
571
- active: state.elementFormat === "right",
572
- icon: /* @__PURE__ */ jsx(AlignRight, {
573
- size: ICON_SIZE,
574
- strokeWidth: ICON_STROKE
575
- }),
576
- title: "Align Right",
577
- tooltipHandle: h,
578
- onClick: () => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "right")
579
- }),
580
- /* @__PURE__ */ jsx(ToolbarButton, {
581
- active: state.elementFormat === "justify",
582
- icon: /* @__PURE__ */ jsx(AlignJustify, {
583
- size: ICON_SIZE,
584
- strokeWidth: ICON_STROKE
585
- }),
586
- title: "Justify",
587
- tooltipHandle: h,
588
- onClick: () => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "justify")
589
- }),
590
- toolbarItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
591
- /* @__PURE__ */ jsx(ToolbarSeparator, {}),
592
- toolbarItems.slice(0, maxVisibleInsertItems).map((item) => /* @__PURE__ */ jsx(ToolbarButton, {
593
- icon: item.icon,
594
- shortcut: item.shortcut,
595
- title: item.title,
596
- tooltipHandle: h,
597
- onClick: () => item.onSelect(editor, "")
598
- }, item.title)),
599
- toolbarItems.length > maxVisibleInsertItems && /* @__PURE__ */ jsxs(DropdownMenu, {
600
- modal: false,
601
- children: [/* @__PURE__ */ jsx(TooltipTrigger, {
602
- handle: h,
603
- payload: { title: "More" },
604
- render: /* @__PURE__ */ jsx(DropdownMenuTrigger, {
605
- className: "_1e1ersc2",
606
- render: /* @__PURE__ */ jsx("button", { type: "button" })
607
- }),
608
- children: /* @__PURE__ */ jsx(Ellipsis, {
609
- size: ICON_SIZE,
610
- strokeWidth: ICON_STROKE
611
- })
612
- }), /* @__PURE__ */ jsx(DropdownMenuContent, {
613
- positionMethod: "fixed",
614
- sideOffset: 4,
615
- children: toolbarItems.slice(maxVisibleInsertItems).map((item) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
616
- onClick: () => item.onSelect(editor, ""),
617
- children: [item.icon && /* @__PURE__ */ jsx("span", {
618
- style: {
619
- marginRight: 8,
620
- display: "inline-flex",
621
- transform: "scale(0.8)",
622
- transformOrigin: "left center"
623
- },
624
- children: item.icon
625
- }), item.title]
626
- }, item.title))
627
- })]
628
- })
629
- ] })
630
- ]
777
+ ref: rowRef,
778
+ children: [entries.slice(0, visibleCount).map((entry) => entry.node), hidden.length > 0 && /* @__PURE__ */ jsx(ToolbarOverflowMenu, {
779
+ entries: hidden,
780
+ handle,
781
+ icon: /* @__PURE__ */ jsx(Ellipsis, { size: 16 })
782
+ })]
631
783
  })
632
784
  })]
633
785
  });