@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.
- package/dist/blok.mjs +2 -2
- package/dist/chunks/{blok-oNSQ3HA6.mjs → blok-Xfgk2kCJ.mjs} +665 -637
- package/dist/chunks/{i18next-loader-BdNRw4n4.mjs → i18next-loader-BMO6Rg_l.mjs} +1 -1
- package/dist/chunks/{index-DHgXmfki.mjs → index-DyPp5v5e.mjs} +1 -1
- package/dist/chunks/{inline-tool-convert-CRqgjRim.mjs → inline-tool-convert-DhHW7EYl.mjs} +2 -1
- package/dist/full.mjs +2 -2
- package/dist/tools.mjs +57 -31
- package/package.json +25 -7
- package/src/components/inline-tools/inline-tool-convert.ts +1 -0
- package/src/components/inline-tools/inline-tool-link.ts +1 -0
- package/src/components/modules/toolbar/blockSettings.ts +2 -1
- package/src/components/modules/toolbar/index.ts +97 -116
- package/src/components/modules/ui.ts +11 -7
- package/src/components/ui/toolbox.ts +14 -5
- package/src/components/utils/data-model-transform.ts +38 -21
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +1 -1
- package/src/components/utils/popover/components/popover-item/popover-item.ts +11 -0
- package/src/components/utils/popover/popover-abstract.ts +1 -1
- package/src/components/utils/popover/popover-desktop.ts +8 -2
- package/src/stories/Popover.stories.ts +0 -85
- package/src/styles/main.css +7 -4
- package/src/tools/header/index.ts +1 -0
- package/src/tools/list/index.ts +34 -4
- package/types/configs/sanitizer-config.d.ts +25 -1
- package/types/index.d.ts +1 -0
- package/types/tools/block-tool.d.ts +2 -2
- package/types/tools/tool-settings.d.ts +7 -0
- package/types/utils/popover/popover-item.d.ts +6 -0
- 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:
|
|
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.
|
|
579
|
+
if (this.currentContentEditable === null) {
|
|
569
580
|
return;
|
|
570
581
|
}
|
|
571
582
|
|
|
572
|
-
|
|
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
|
-
//
|
|
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: '
|
|
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
|
|
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 === '
|
|
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
|
|
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
|
|
318
|
-
|
|
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
|
|
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
|
|
334
|
-
const
|
|
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-
|
|
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', '
|
|
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,
|
|
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,
|
|
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
|
*/
|
package/src/styles/main.css
CHANGED
|
@@ -84,17 +84,20 @@
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
[data-drop-indicator]::before {
|
|
87
|
-
@apply content-[''] absolute
|
|
88
|
-
left:
|
|
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
|
|
94
|
+
@apply bottom-0;
|
|
95
|
+
transform: translateX(-50%) translateY(50%);
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
[data-drop-indicator="top"]::before {
|
|
97
|
-
@apply
|
|
99
|
+
@apply top-0;
|
|
100
|
+
transform: translateX(-50%) translateY(-50%);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/**
|
package/src/tools/list/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
PasteEvent,
|
|
18
18
|
ToolboxConfig,
|
|
19
19
|
ConversionConfig,
|
|
20
|
-
|
|
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?:
|
|
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():
|
|
1691
|
+
public static get sanitize(): ToolSanitizerConfig {
|
|
1669
1692
|
return {
|
|
1670
1693
|
text: {
|
|
1671
1694
|
br: true,
|
|
1672
|
-
a:
|
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConversionConfig, PasteConfig,
|
|
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?:
|
|
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
|
|