@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
@@ -468,7 +468,7 @@ export class UI extends Module<UINodes> {
468
468
  };
469
469
 
470
470
  const handleBlockHovered = (event: Event): void => {
471
- if (!(event instanceof MouseEvent)) {
471
+ if (typeof MouseEvent === 'undefined' || !(event instanceof MouseEvent)) {
472
472
  return;
473
473
  }
474
474
 
@@ -789,6 +789,16 @@ export class UI extends Module<UINodes> {
789
789
  return;
790
790
  }
791
791
 
792
+ /**
793
+ * Close BlockSettings first if it's open, regardless of selection state.
794
+ * This prevents navigation mode from being enabled when the user closes block tunes with Escape.
795
+ */
796
+ if (this.Blok.BlockSettings.opened) {
797
+ this.Blok.BlockSettings.close();
798
+
799
+ return;
800
+ }
801
+
792
802
  /**
793
803
  * Clear blocks selection by ESC (but not when entering navigation mode)
794
804
  */
@@ -806,12 +816,6 @@ export class UI extends Module<UINodes> {
806
816
  return;
807
817
  }
808
818
 
809
- if (this.Blok.BlockSettings.opened) {
810
- this.Blok.BlockSettings.close();
811
-
812
- return;
813
- }
814
-
815
819
  /**
816
820
  * If a nested popover is open (like convert-to dropdown),
817
821
  * close only the nested popover, not the entire inline toolbar.
@@ -145,6 +145,12 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
145
145
  */
146
146
  private currentBlockForSearch: HTMLElement | null = null;
147
147
 
148
+ /**
149
+ * Cached contentEditable element for the current block being searched.
150
+ * Avoids repeated DOM queries on each input event.
151
+ */
152
+ private currentContentEditable: Element | null = null;
153
+
148
154
  /**
149
155
  * Toolbox constructor
150
156
  * @param options - available parameters
@@ -383,6 +389,9 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
383
389
  const userSearchTerms = tool.searchTerms ?? [];
384
390
  const mergedSearchTerms = [...new Set([...librarySearchTerms, ...userSearchTerms])];
385
391
 
392
+ // Use entry-level shortcut if available, otherwise fall back to tool-level shortcut (for first entry only)
393
+ const shortcut = toolboxItem.shortcut ?? (displaySecondaryLabel ? tool.shortcut : undefined);
394
+
386
395
  return {
387
396
  icon: toolboxItem.icon,
388
397
  title: translateToolTitle(this.i18n, toolboxItem, capitalize(tool.name)),
@@ -390,7 +399,7 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
390
399
  onActivate: (): void => {
391
400
  void this.toolButtonActivated(tool.name, toolboxItem.data);
392
401
  },
393
- secondaryLabel: (tool.shortcut && displaySecondaryLabel) ? beautifyShortcut(tool.shortcut) : '',
402
+ secondaryLabel: shortcut ? beautifyShortcut(shortcut) : '',
394
403
  englishTitle,
395
404
  searchTerms: mergedSearchTerms,
396
405
  };
@@ -545,6 +554,7 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
545
554
  }
546
555
 
547
556
  this.currentBlockForSearch = currentBlock.holder;
557
+ this.currentContentEditable = this.currentBlockForSearch.querySelector('[contenteditable="true"]');
548
558
  this.listeners.on(this.currentBlockForSearch, 'input', this.handleBlockInput);
549
559
  }
550
560
 
@@ -555,6 +565,7 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
555
565
  if (this.currentBlockForSearch !== null) {
556
566
  this.listeners.off(this.currentBlockForSearch, 'input', this.handleBlockInput);
557
567
  this.currentBlockForSearch = null;
568
+ this.currentContentEditable = null;
558
569
  }
559
570
 
560
571
  this.popover?.filterItems('');
@@ -565,13 +576,11 @@ export class Toolbox extends EventsDispatcher<ToolboxEventMap> {
565
576
  * Extracts text after "/" and applies it as a filter query.
566
577
  */
567
578
  private handleBlockInput = (): void => {
568
- if (this.currentBlockForSearch === null) {
579
+ if (this.currentContentEditable === null) {
569
580
  return;
570
581
  }
571
582
 
572
- // Get text from the contenteditable element inside the block
573
- const contentEditable = this.currentBlockForSearch.querySelector('[contenteditable="true"]');
574
- const text = contentEditable?.textContent || '';
583
+ const text = this.currentContentEditable.textContent || '';
575
584
  const slashIndex = text.lastIndexOf('/');
576
585
 
577
586
  if (slashIndex === -1) {
@@ -109,37 +109,49 @@ const expandListItems = (
109
109
 
110
110
  childIds.push(itemId);
111
111
 
112
- // Recursively expand nested items first to get their IDs
113
- const nestedChildIds = item.items && item.items.length > 0
114
- ? expandListItems(item.items, itemId, depth + 1, style, undefined, tunes, blocks)
115
- : [];
116
-
117
112
  // Determine if we should include start (only for first root item of ordered lists)
118
113
  const includeStart = style === 'ordered' && depth === 0 && index === 0 && start !== undefined && start !== 1;
119
114
 
120
- // Create the list_item block
115
+ // Check if there are nested items (we'll get their IDs after pushing the parent)
116
+ const hasNestedItems = item.items && item.items.length > 0;
117
+
118
+ // Create the list block (flat model - each item is a separate 'list' block)
119
+ // We'll update with content IDs after processing children
121
120
  const itemBlock: OutputBlockData = {
122
121
  id: itemId,
123
- type: 'list_item',
122
+ type: 'list',
124
123
  data: {
125
124
  text: item.content,
126
125
  checked: item.checked,
127
126
  style,
127
+ ...(depth > 0 ? { depth } : {}),
128
128
  ...(includeStart ? { start } : {}),
129
129
  },
130
130
  ...(tunes !== undefined ? { tunes } : {}),
131
131
  ...(parentId !== undefined ? { parent: parentId } : {}),
132
- ...(nestedChildIds.length > 0 ? { content: nestedChildIds } : {}),
133
132
  };
134
133
 
134
+ // Push parent block first to maintain correct order (parent before children)
135
135
  blocks.push(itemBlock);
136
+
137
+ // Now recursively expand nested items (they will be added after the parent)
138
+ if (!hasNestedItems) {
139
+ return;
140
+ }
141
+
142
+ const nestedChildIds = expandListItems(item.items!, itemId, depth + 1, style, undefined, tunes, blocks);
143
+
144
+ // Update the parent block with content IDs (only if children exist)
145
+ if (nestedChildIds.length > 0) {
146
+ itemBlock.content = nestedChildIds;
147
+ }
136
148
  });
137
149
 
138
150
  return childIds;
139
151
  };
140
152
 
141
153
  /**
142
- * Expand a List block with nested items into flat list_item blocks
154
+ * Expand a List block with nested items into flat list blocks
143
155
  */
144
156
  const expandListToHierarchical = (
145
157
  listData: LegacyListData,
@@ -217,7 +229,7 @@ const collectChildItems = (
217
229
 
218
230
  for (const childId of contentIds) {
219
231
  const childBlock = blockMap.get(childId);
220
- const isListItem = childBlock && childBlock.type === 'list_item';
232
+ const isListItem = childBlock && childBlock.type === 'list';
221
233
 
222
234
  if (isListItem) {
223
235
  const items = collectListItems(childBlock, blockMap, processedIds);
@@ -269,7 +281,7 @@ const collectListItems = (
269
281
  };
270
282
 
271
283
  /**
272
- * Process a root list_item block and convert to a legacy List block
284
+ * Process a root list block (flat model) and convert to a legacy List block
273
285
  */
274
286
  const processRootListItem = (
275
287
  block: OutputBlockData,
@@ -294,6 +306,13 @@ const processRootListItem = (
294
306
  return listBlock;
295
307
  };
296
308
 
309
+ /**
310
+ * Check if a block is a flat-model list block (has 'text' field instead of 'items')
311
+ */
312
+ const isFlatModelListBlock = (block: OutputBlockData): boolean => {
313
+ return block.type === 'list' && block.data?.text !== undefined && block.data?.items === undefined;
314
+ };
315
+
297
316
  /**
298
317
  * Collapse hierarchical flat-with-references format back to legacy nested format
299
318
  * @param blocks - array of flat blocks with parent/content references
@@ -302,24 +321,21 @@ const processRootListItem = (
302
321
  export const collapseToLegacy = (blocks: OutputBlockData[]): OutputBlockData[] => {
303
322
  // Build a map of blocks by ID for quick lookup
304
323
  const blockMap = new Map<BlockId, OutputBlockData>();
305
- const listItemBlocks: OutputBlockData[] = [];
306
324
 
307
325
  for (const block of blocks) {
308
326
  if (block.id) {
309
327
  blockMap.set(block.id, block);
310
328
  }
311
-
312
- if (block.type === 'list_item') {
313
- listItemBlocks.push(block);
314
- }
315
329
  }
316
330
 
317
- // If no list_item blocks, just strip hierarchy fields and return
318
- if (listItemBlocks.length === 0) {
331
+ // If no flat-model list blocks, just strip hierarchy fields and return
332
+ const hasFlatListBlocks = blocks.some(isFlatModelListBlock);
333
+
334
+ if (!hasFlatListBlocks) {
319
335
  return blocks.map(stripHierarchyFields);
320
336
  }
321
337
 
322
- // Process blocks, converting root list_items to List blocks
338
+ // Process blocks, converting root flat-model list blocks to legacy List blocks
323
339
  const result: OutputBlockData[] = [];
324
340
  const processedIds = new Set<BlockId>();
325
341
 
@@ -330,8 +346,9 @@ export const collapseToLegacy = (blocks: OutputBlockData[]): OutputBlockData[] =
330
346
  continue;
331
347
  }
332
348
 
333
- const isRootListItem = block.type === 'list_item' && !block.parent;
334
- const isNonListItem = block.type !== 'list_item';
349
+ const isFlatListBlock = isFlatModelListBlock(block);
350
+ const isRootListItem = isFlatListBlock && !block.parent;
351
+ const isNonListItem = !isFlatListBlock;
335
352
 
336
353
  if (isRootListItem) {
337
354
  const listBlock = processRootListItem(block, blockMap, processedIds);
@@ -265,7 +265,7 @@ export class PopoverItemDefault extends PopoverItem {
265
265
  if (params.secondaryLabel) {
266
266
  const secondaryEl = document.createElement('div');
267
267
 
268
- secondaryEl.className = 'whitespace-nowrap pr-1.5 text-xs -tracking-widest text-text-secondary opacity-60';
268
+ secondaryEl.className = 'whitespace-nowrap pr-1.5 text-xs font-light tracking-[0.25px] text-text-secondary opacity-60';
269
269
  secondaryEl.setAttribute(DATA_ATTR.popoverItemSecondaryTitle, '');
270
270
  secondaryEl.setAttribute('data-blok-testid', 'popover-item-secondary-title');
271
271
  secondaryEl.textContent = params.secondaryLabel;
@@ -158,6 +158,17 @@ export abstract class PopoverItem {
158
158
  return this.params !== undefined && 'children' in this.params && this.params.children?.searchable === true;
159
159
  }
160
160
 
161
+ /**
162
+ * Returns the width for children popover, if specified
163
+ */
164
+ public get childrenWidth(): string | undefined {
165
+ if (this.params === undefined || !('children' in this.params)) {
166
+ return undefined;
167
+ }
168
+
169
+ return this.params.children?.width;
170
+ }
171
+
161
172
  /**
162
173
  * True if popover should close once item is activated
163
174
  */
@@ -399,7 +399,7 @@ export abstract class PopoverAbstract<Nodes extends PopoverNodes = PopoverNodes>
399
399
  popover.setAttribute('data-blok-testid', 'popover');
400
400
 
401
401
  // Set CSS variables
402
- popover.style.setProperty('--width', '200px');
402
+ popover.style.setProperty('--width', this.params.width ?? '280px');
403
403
  popover.style.setProperty('--item-padding', '3px');
404
404
  popover.style.setProperty('--item-height', 'calc(1.25rem + 2 * var(--item-padding))');
405
405
  popover.style.setProperty('--popover-top', 'calc(100% + 0.5rem)');
@@ -401,6 +401,7 @@ export class PopoverDesktop extends PopoverAbstract {
401
401
  flippable: item.isChildrenFlippable,
402
402
  messages: this.messages,
403
403
  onNavigateBack: this.destroyNestedPopoverIfExists.bind(this),
404
+ width: item.childrenWidth,
404
405
  });
405
406
 
406
407
  item.onChildrenOpen();
@@ -452,14 +453,19 @@ export class PopoverDesktop extends PopoverAbstract {
452
453
  // Apply position: absolute for nested container
453
454
  nestedContainer.style.position = 'absolute';
454
455
 
456
+ // Get parent width - use computed width if --width is 'auto'
457
+ const parentWidth = this.params.width === 'auto'
458
+ ? `${this.nodes.popoverContainer.offsetWidth}px`
459
+ : 'var(--width)';
460
+
455
461
  // Calculate --popover-left based on nesting level and parent open direction
456
462
  // Set on the actual popover element to override its default value
457
463
  if (isParentOpenLeft) {
458
464
  // Position to the left
459
- actualPopoverEl.style.setProperty(CSSVariables.PopoverLeft, 'calc(-1 * (var(--nesting-level) + 1) * var(--width) + 100%)');
465
+ actualPopoverEl.style.setProperty(CSSVariables.PopoverLeft, `calc(-1 * (var(--nesting-level) + 1) * ${parentWidth} + 100%)`);
460
466
  } else {
461
467
  // Position to the right
462
- actualPopoverEl.style.setProperty(CSSVariables.PopoverLeft, 'calc(var(--nesting-level) * (var(--width) - var(--nested-popover-overlap)))');
468
+ actualPopoverEl.style.setProperty(CSSVariables.PopoverLeft, `calc(var(--nesting-level) * (${parentWidth} - var(--nested-popover-overlap)))`);
463
469
  }
464
470
 
465
471
  // Calculate top position based on parent open direction
@@ -21,7 +21,6 @@ const POPOVER_ITEM_TESTID = '[data-blok-testid="popover-item"]';
21
21
  const POPOVER_OPENED_SELECTOR = '[data-blok-popover-opened="true"]';
22
22
  const ITEM_FOCUSED_SELECTOR = '[data-blok-focused=\"true\"]';
23
23
  const CONFIRMATION_SELECTOR = '[data-blok-popover-item-confirmation="true"]';
24
- const NOTHING_FOUND_SELECTOR = '[data-blok-nothing-found-displayed="true"]';
25
24
  const DELETE_BUTTON_SELECTOR = '[data-blok-item-name="delete"]';
26
25
  const CONVERT_TO_SELECTOR = '[data-blok-item-name="convert-to"]';
27
26
  const NESTED_POPOVER_SELECTOR = '[data-blok-nested="true"]';
@@ -451,90 +450,6 @@ export const SearchFiltering: Story = {
451
450
  },
452
451
  };
453
452
 
454
- /**
455
- * Popover nothing found message (in block settings).
456
- */
457
- export const NothingFoundMessage: Story = {
458
- args: {
459
- data: sampleData,
460
- },
461
- play: async ({ canvasElement, step }) => {
462
- await step('Wait for editor and toolbar to initialize', async () => {
463
- await waitFor(
464
- () => {
465
- const block = canvasElement.querySelector(BLOCK_TESTID);
466
-
467
- expect(block).toBeInTheDocument();
468
- },
469
- TIMEOUT_INIT
470
- );
471
- // Wait for toolbar to be created (happens in requestIdleCallback)
472
- await waitForToolbar(canvasElement);
473
- });
474
-
475
- await step('Click block to show toolbar', async () => {
476
- const block = canvasElement.querySelector(BLOCK_TESTID);
477
-
478
- if (block) {
479
- simulateClick(block);
480
- }
481
-
482
- await waitFor(
483
- () => {
484
- const toolbar = canvasElement.querySelector(TOOLBAR_TESTID);
485
-
486
- expect(toolbar).toHaveAttribute('data-blok-opened', 'true');
487
- },
488
- TIMEOUT_ACTION
489
- );
490
- });
491
-
492
- await step('Open block settings', async () => {
493
- const settingsButton = canvasElement.querySelector(SETTINGS_BUTTON_TESTID);
494
-
495
- if (settingsButton) {
496
- await userEvent.click(settingsButton);
497
- }
498
-
499
- await waitFor(
500
- () => {
501
- // Block tunes popover is appended to document.body
502
- const blockTunesPopover = document.querySelector(BLOCK_TUNES_POPOVER_TESTID);
503
-
504
- expect(blockTunesPopover).toBeInTheDocument();
505
- },
506
- TIMEOUT_ACTION
507
- );
508
- });
509
-
510
- await step('Search for non-existent item', async () => {
511
- // Wait for popover to be fully rendered and search input to be available
512
- await new Promise((resolve) => setTimeout(resolve, 100));
513
-
514
- // Find and focus the search input inside the popover
515
- const searchInput = document.querySelector('[data-blok-testid="popover-search-input"]') as HTMLInputElement;
516
-
517
- if (searchInput) {
518
- searchInput.focus();
519
- // Type directly into the input
520
- searchInput.value = 'xyznonexistent';
521
- // Trigger input event to notify the search handler
522
- searchInput.dispatchEvent(new Event('input', { bubbles: true }));
523
- }
524
-
525
- await waitFor(
526
- () => {
527
- // Nothing found message is inside the popover which is in document.body
528
- const nothingFound = document.querySelector(NOTHING_FOUND_SELECTOR);
529
-
530
- expect(nothingFound).toBeInTheDocument();
531
- },
532
- TIMEOUT_ACTION
533
- );
534
- });
535
- },
536
- };
537
-
538
453
  /**
539
454
  * Popover item in disabled state.
540
455
  */
@@ -84,17 +84,20 @@
84
84
  }
85
85
 
86
86
  [data-drop-indicator]::before {
87
- @apply content-[''] absolute -translate-x-1/2 w-full max-w-content h-1.5 rounded-sm bg-[#d4e3fc] pointer-events-none z-10;
88
- left: calc(50% + var(--drop-indicator-depth, 0) * 12px);
87
+ @apply content-[''] absolute w-full max-w-content h-1.5 rounded-sm bg-[#d4e3fc] pointer-events-none z-10;
88
+ left: 50%;
89
+ margin-left: calc(var(--drop-indicator-depth, 0) * 12px);
89
90
  max-width: calc(650px - var(--drop-indicator-depth, 0) * 24px);
90
91
  }
91
92
 
92
93
  [data-drop-indicator="bottom"]::before {
93
- @apply translate-y-1/2 bottom-0;
94
+ @apply bottom-0;
95
+ transform: translateX(-50%) translateY(50%);
94
96
  }
95
97
 
96
98
  [data-drop-indicator="top"]::before {
97
- @apply -translate-y-1/2 top-0;
99
+ @apply top-0;
100
+ transform: translateX(-50%) translateY(-50%);
98
101
  }
99
102
 
100
103
  /**
@@ -641,6 +641,7 @@ export class Header implements BlockTool {
641
641
  name: `header-${level.number}`,
642
642
  data: { level: level.number },
643
643
  searchTerms: [`h${level.number}`, 'title', 'header', 'heading'],
644
+ shortcut: '#'.repeat(level.number),
644
645
  }));
645
646
  }
646
647
  }
@@ -17,7 +17,7 @@ import type {
17
17
  PasteEvent,
18
18
  ToolboxConfig,
19
19
  ConversionConfig,
20
- SanitizerConfig,
20
+ ToolSanitizerConfig,
21
21
  PasteConfig,
22
22
  } from '../../../types';
23
23
  import type { MenuConfig } from '../../../types/tools/menu-config';
@@ -183,7 +183,14 @@ export class ListItem implements BlockTool {
183
183
  */
184
184
  private static pendingMarkerUpdate = false;
185
185
 
186
- sanitize?: SanitizerConfig | undefined;
186
+ sanitize?: ToolSanitizerConfig | undefined;
187
+
188
+ /**
189
+ * Legacy list item structure for backward compatibility
190
+ */
191
+ private static isLegacyFormat(data: unknown): data is { items: Array<{ content: string; checked?: boolean }>, style?: ListItemStyle, start?: number } {
192
+ return typeof data === 'object' && data !== null && 'items' in data && Array.isArray((data as { items: unknown }).items);
193
+ }
187
194
 
188
195
  private normalizeData(data: ListItemData | Record<string, never>): ListItemData {
189
196
  const defaultStyle = this._settings.defaultStyle || 'unordered';
@@ -197,6 +204,22 @@ export class ListItem implements BlockTool {
197
204
  };
198
205
  }
199
206
 
207
+ // Handle legacy format with items[] array - extract first item's content
208
+ // This provides backward compatibility when legacy data is passed directly to the tool
209
+ if (ListItem.isLegacyFormat(data)) {
210
+ const firstItem = data.items[0];
211
+ const text = firstItem?.content || '';
212
+ const checked = firstItem?.checked || false;
213
+
214
+ return {
215
+ text,
216
+ style: data.style || defaultStyle,
217
+ checked: Boolean(checked),
218
+ depth: 0,
219
+ ...(data.start !== undefined && data.start !== 1 ? { start: data.start } : {}),
220
+ };
221
+ }
222
+
200
223
  return {
201
224
  text: data.text || '',
202
225
  style: data.style || defaultStyle,
@@ -1665,11 +1688,15 @@ export class ListItem implements BlockTool {
1665
1688
  };
1666
1689
  }
1667
1690
 
1668
- public static get sanitize(): SanitizerConfig {
1691
+ public static get sanitize(): ToolSanitizerConfig {
1669
1692
  return {
1670
1693
  text: {
1671
1694
  br: true,
1672
- a: true,
1695
+ a: {
1696
+ href: true,
1697
+ target: '_blank',
1698
+ rel: 'nofollow',
1699
+ },
1673
1700
  b: true,
1674
1701
  i: true,
1675
1702
  mark: true,
@@ -1797,6 +1824,7 @@ export class ListItem implements BlockTool {
1797
1824
  data: { style: 'unordered' },
1798
1825
  name: 'bulleted-list',
1799
1826
  searchTerms: ['ul', 'bullet', 'unordered', 'list'],
1827
+ shortcut: '-',
1800
1828
  },
1801
1829
  {
1802
1830
  icon: IconListNumbered,
@@ -1805,6 +1833,7 @@ export class ListItem implements BlockTool {
1805
1833
  data: { style: 'ordered' },
1806
1834
  name: 'numbered-list',
1807
1835
  searchTerms: ['ol', 'ordered', 'number', 'list'],
1836
+ shortcut: '1.',
1808
1837
  },
1809
1838
  {
1810
1839
  icon: IconListChecklist,
@@ -1813,6 +1842,7 @@ export class ListItem implements BlockTool {
1813
1842
  data: { style: 'checklist' },
1814
1843
  name: 'check-list',
1815
1844
  searchTerms: ['checkbox', 'task', 'todo', 'check', 'list'],
1845
+ shortcut: '[]',
1816
1846
  },
1817
1847
  ];
1818
1848
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export type TagConfig = boolean | { [attr: string]: boolean | string };
6
6
 
7
- export type SanitizerRule = TagConfig | ((el: Element) => TagConfig)
7
+ export type SanitizerRule = TagConfig | ((el: Element) => TagConfig);
8
8
 
9
9
  export interface SanitizerConfig {
10
10
  /**
@@ -41,3 +41,27 @@ export interface SanitizerConfig {
41
41
  */
42
42
  [key: string]: SanitizerRule;
43
43
  }
44
+
45
+ /**
46
+ * Sanitizer config for Block Tools that supports field-specific tag rules.
47
+ * Use this when your tool's data has multiple fields that each need different sanitization.
48
+ *
49
+ * @example Field-specific sanitization
50
+ * ```typescript
51
+ * static get sanitize(): ToolSanitizerConfig {
52
+ * return {
53
+ * text: {
54
+ * br: true,
55
+ * a: { href: true, target: '_blank', rel: 'nofollow' }
56
+ * },
57
+ * caption: {
58
+ * b: true,
59
+ * i: true
60
+ * }
61
+ * };
62
+ * }
63
+ * ```
64
+ */
65
+ export interface ToolSanitizerConfig {
66
+ [key: string]: SanitizerRule | SanitizerConfig;
67
+ }
package/types/index.d.ts CHANGED
@@ -72,6 +72,7 @@ export {
72
72
  BlokConfig,
73
73
  SanitizerConfig,
74
74
  SanitizerRule,
75
+ ToolSanitizerConfig,
75
76
  PasteConfig,
76
77
  LogLevels,
77
78
  ConversionConfig,
@@ -1,4 +1,4 @@
1
- import { ConversionConfig, PasteConfig, SanitizerConfig } from '../configs';
1
+ import { ConversionConfig, PasteConfig, ToolSanitizerConfig } from '../configs';
2
2
  import { BlockToolData } from './block-tool-data';
3
3
  import { BaseTool, BaseToolConstructable, BaseToolConstructorOptions } from './tool';
4
4
  import { ToolConfig } from './tool-config';
@@ -15,7 +15,7 @@ export interface BlockTool extends BaseTool {
15
15
  /**
16
16
  * Sanitizer rules description
17
17
  */
18
- sanitize?: SanitizerConfig;
18
+ sanitize?: ToolSanitizerConfig;
19
19
 
20
20
  /**
21
21
  * Process Tool's element in DOM and return raw data
@@ -52,6 +52,13 @@ export interface ToolboxConfigEntry {
52
52
  * Terms are matched case-insensitively.
53
53
  */
54
54
  searchTerms?: string[];
55
+
56
+ /**
57
+ * Shortcut hint to display in the toolbox (e.g., '#', '##', '-', '1.', '[]').
58
+ * This is displayed as a secondary label next to the tool title.
59
+ * Unlike tool-level shortcuts, these are per-entry hints for tools with multiple variants.
60
+ */
61
+ shortcut?: string;
55
62
  }
56
63
 
57
64
  /**
@@ -45,6 +45,12 @@ export interface PopoverItemChildren {
45
45
  * Useful for items like link tool that render custom content instead of a dropdown list.
46
46
  */
47
47
  hideChevron?: boolean;
48
+
49
+ /**
50
+ * Width of the nested popover. Defaults to '280px'.
51
+ * Use 'auto' to fit content width.
52
+ */
53
+ width?: string;
48
54
  }
49
55
 
50
56
  /**
@@ -65,6 +65,12 @@ export interface PopoverParams {
65
65
  * Used to close nested popover and return focus to parent.
66
66
  */
67
67
  onNavigateBack?: () => void;
68
+
69
+ /**
70
+ * Width of the popover. Defaults to '280px'.
71
+ * Use 'auto' to fit content width.
72
+ */
73
+ width?: string;
68
74
  }
69
75
 
70
76