@jackuait/blok 0.7.3-beta.4 → 0.7.3
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-CdxHhr5i.mjs → blok-BmlbETK7.mjs} +2119 -2013
- package/dist/chunks/{constants-C_H9o9Ao.mjs → constants-WhLyFkza.mjs} +260 -223
- package/dist/chunks/{i18next-loader-D5HxE5ZQ.mjs → i18next-loader-CZARkla1.mjs} +1 -1
- package/dist/chunks/{lightweight-i18n-Safdy0ua.mjs → lightweight-i18n-BQa0F2X6.mjs} +9 -0
- package/dist/chunks/{tools-B0YXCZFW.mjs → tools-BCb5bMO3.mjs} +973 -843
- package/dist/full.mjs +3 -3
- package/dist/locales.mjs +9 -0
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +2 -2
- package/src/components/block/style-manager.ts +1 -1
- package/src/components/blocks.ts +26 -54
- package/src/components/constants/data-attributes.ts +0 -2
- package/src/components/i18n/locales/en/messages.json +9 -0
- package/src/components/icons/index.ts +34 -6
- package/src/components/inline-tools/inline-tool-link.ts +202 -5
- package/src/components/inline-tools/inline-tool-marker.ts +166 -23
- package/src/components/inline-tools/utils/formatting-range-utils.ts +10 -1
- package/src/components/modules/blockManager/blockManager.ts +2 -2
- package/src/components/modules/blockManager/operations.ts +2 -2
- package/src/components/modules/blockManager/repository.ts +1 -9
- package/src/components/modules/blockManager/types.ts +1 -1
- package/src/components/modules/drag/operations/DragOperations.ts +45 -6
- package/src/components/modules/paste/google-docs-preprocessor.ts +69 -2
- package/src/components/modules/paste/handlers/blok-data-handler.ts +96 -19
- package/src/components/modules/renderer.ts +2 -0
- package/src/components/modules/toolbar/blockSettings.ts +1 -1
- package/src/components/modules/toolbar/index.ts +21 -0
- package/src/components/modules/toolbar/plus-button.ts +15 -5
- package/src/components/selection/fake-background/index.ts +9 -10
- package/src/components/shared/color-picker.ts +108 -95
- package/src/components/shared/color-presets.ts +30 -2
- package/src/components/ui/toolbox.ts +36 -7
- package/src/components/utils/color-mapping.ts +43 -1
- package/src/components/utils/color-migration.ts +37 -0
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.const.ts +4 -3
- package/src/components/utils/popover/components/popover-item/popover-item-default/popover-item-default.ts +5 -39
- package/src/components/utils/popover/components/popover-item/popover-item-separator/popover-item-separator.const.ts +2 -2
- package/src/components/utils/popover/components/popover-item/popover-item.ts +11 -0
- package/src/components/utils/popover/components/search-input/search-input.const.ts +2 -3
- package/src/components/utils/popover/components/search-input/search-input.ts +1 -32
- package/src/components/utils/popover/popover-abstract.ts +2 -4
- package/src/components/utils/popover/popover-desktop.ts +1 -16
- package/src/components/utils/popover/popover-inline.ts +1 -2
- package/src/components/utils/popover/popover-mobile.ts +2 -2
- package/src/components/utils/popover/popover.const.ts +1 -1
- package/src/stories/Table.stories.ts +15 -9
- package/src/styles/main.css +312 -14
- package/src/tools/header/index.ts +5 -5
- package/src/tools/list/constants.ts +11 -4
- package/src/tools/list/depth-validator.ts +13 -1
- package/src/tools/list/dom-builder.ts +5 -3
- package/src/tools/list/index.ts +3 -2
- package/src/tools/paragraph/index.ts +2 -2
- package/src/tools/table/table-cell-color-picker.ts +1 -1
- package/src/tools/table/table-cell-selection.ts +1 -2
- package/src/tools/table/table-core.ts +2 -2
- package/src/tools/table/table-grip-visuals.ts +13 -5
- package/src/tools/table/table-heading-toggle.ts +15 -9
- package/src/tools/table/table-row-col-controls.ts +17 -11
- package/src/tools/table/table-row-col-drag.ts +26 -3
- package/src/tools/toggle/constants.ts +5 -5
- package/src/tools/toggle/index.ts +1 -1
- package/types/tools/hook-events.d.ts +6 -0
- package/types/utils/popover/popover-item.d.ts +6 -0
- package/CHANGELOG.md +0 -119
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
} from './utils/formatting-range-utils';
|
|
16
16
|
import { createColorPicker } from '../shared/color-picker';
|
|
17
17
|
import type { ColorPickerHandle } from '../shared/color-picker';
|
|
18
|
+
import { colorVarName } from '../shared/color-presets';
|
|
19
|
+
import { mapToNearestPresetName } from '../utils/color-mapping';
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Color mode type — either text color or background color
|
|
@@ -122,6 +124,16 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
122
124
|
*/
|
|
123
125
|
private colorMode: ColorMode = 'color';
|
|
124
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Active text color for bar indicator (null = default/none)
|
|
129
|
+
*/
|
|
130
|
+
private activeTextColor: string | null = null;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Active background color for bar indicator (null = default/none)
|
|
134
|
+
*/
|
|
135
|
+
private activeBgColor: string | null = null;
|
|
136
|
+
|
|
125
137
|
/**
|
|
126
138
|
* The color picker handle with element and control methods
|
|
127
139
|
*/
|
|
@@ -138,7 +150,6 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
138
150
|
this.picker = createColorPicker({
|
|
139
151
|
i18n: this.i18n,
|
|
140
152
|
testIdPrefix: 'marker',
|
|
141
|
-
defaultModeIndex: 0,
|
|
142
153
|
modes: [
|
|
143
154
|
{ key: 'color', labelKey: 'tools.marker.textColor', presetField: 'text' },
|
|
144
155
|
{ key: 'background-color', labelKey: 'tools.marker.background', presetField: 'bg' },
|
|
@@ -151,6 +162,16 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
151
162
|
} else {
|
|
152
163
|
this.removeColor(this.colorMode);
|
|
153
164
|
}
|
|
165
|
+
|
|
166
|
+
if (modeKey === 'color') {
|
|
167
|
+
this.activeTextColor = color;
|
|
168
|
+
} else {
|
|
169
|
+
this.activeBgColor = color;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.picker.setActiveColor(color, modeKey);
|
|
173
|
+
this.updateToolbarColors(this.activeTextColor, this.activeBgColor);
|
|
174
|
+
|
|
154
175
|
this.selection.setFakeBackground();
|
|
155
176
|
this.selection.save();
|
|
156
177
|
},
|
|
@@ -172,12 +193,18 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
172
193
|
}
|
|
173
194
|
|
|
174
195
|
const range = selection.getRangeAt(0);
|
|
196
|
+
const formatted = isRangeFormatted(range, isMarkTag, { ignoreWhitespace: true });
|
|
197
|
+
|
|
198
|
+
if (formatted) {
|
|
199
|
+
const colors = this.detectBothSelectionColors(range);
|
|
200
|
+
|
|
201
|
+
this.updateToolbarColors(colors.text, colors.bg);
|
|
202
|
+
}
|
|
175
203
|
|
|
176
|
-
return
|
|
204
|
+
return formatted;
|
|
177
205
|
},
|
|
178
206
|
children: {
|
|
179
207
|
hideChevron: true,
|
|
180
|
-
width: '200px',
|
|
181
208
|
items: [
|
|
182
209
|
{
|
|
183
210
|
type: PopoverItemType.Html,
|
|
@@ -214,6 +241,8 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
214
241
|
return;
|
|
215
242
|
}
|
|
216
243
|
|
|
244
|
+
const resolvedValue = this.resolveToVar(value, mode);
|
|
245
|
+
|
|
217
246
|
/**
|
|
218
247
|
* Check if the entire selection is already inside a single mark element
|
|
219
248
|
*/
|
|
@@ -232,7 +261,7 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
232
261
|
range.compareBoundaryPoints(Range.END_TO_END, markRange) >= 0;
|
|
233
262
|
|
|
234
263
|
if (coversAll) {
|
|
235
|
-
existingMark.style.setProperty(mode,
|
|
264
|
+
existingMark.style.setProperty(mode, resolvedValue);
|
|
236
265
|
this.ensureTransparentBg(existingMark);
|
|
237
266
|
|
|
238
267
|
return;
|
|
@@ -242,7 +271,7 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
242
271
|
* Partial selection: split the mark around the selection
|
|
243
272
|
* so the new color applies only to the selected text
|
|
244
273
|
*/
|
|
245
|
-
this.splitMarkAroundRange(existingMark, range, mode,
|
|
274
|
+
this.splitMarkAroundRange(existingMark, range, mode, resolvedValue);
|
|
246
275
|
|
|
247
276
|
return;
|
|
248
277
|
}
|
|
@@ -260,7 +289,7 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
260
289
|
|
|
261
290
|
const mark = document.createElement('mark');
|
|
262
291
|
|
|
263
|
-
mark.style.setProperty(mode,
|
|
292
|
+
mark.style.setProperty(mode, resolvedValue);
|
|
264
293
|
this.ensureTransparentBg(mark);
|
|
265
294
|
mark.appendChild(range.extractContents());
|
|
266
295
|
range.insertNode(mark);
|
|
@@ -314,6 +343,13 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
314
343
|
? ancestorEl.closest('mark')?.parentElement ?? ancestorEl
|
|
315
344
|
: ancestorEl;
|
|
316
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Split any marks that extend beyond the selection boundaries so that
|
|
348
|
+
* the removal only affects the selected portion, not the entire mark.
|
|
349
|
+
* (Mirrors the same call in applyColor().)
|
|
350
|
+
*/
|
|
351
|
+
this.splitMarksAtBoundaries(range);
|
|
352
|
+
|
|
317
353
|
const markAncestors = collectFormattingAncestors(range, isMarkTag);
|
|
318
354
|
|
|
319
355
|
for (const mark of markAncestors) {
|
|
@@ -362,18 +398,77 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
362
398
|
* and detect the current selection's color to highlight the active swatch.
|
|
363
399
|
*/
|
|
364
400
|
private onPickerOpen(): void {
|
|
401
|
+
this.activeTextColor = null;
|
|
402
|
+
this.activeBgColor = null;
|
|
403
|
+
|
|
365
404
|
this.picker.reset();
|
|
366
405
|
|
|
367
|
-
const
|
|
406
|
+
const colors = this.detectBothSelectionComputedColors();
|
|
368
407
|
|
|
369
|
-
if (
|
|
370
|
-
this.picker.setActiveColor(
|
|
408
|
+
if (colors.text !== null) {
|
|
409
|
+
this.picker.setActiveColor(colors.text, 'color');
|
|
410
|
+
this.activeTextColor = colors.text;
|
|
371
411
|
}
|
|
372
412
|
|
|
413
|
+
if (colors.bg !== null) {
|
|
414
|
+
this.picker.setActiveColor(colors.bg, 'background-color');
|
|
415
|
+
this.activeBgColor = colors.bg;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
this.updateToolbarColors(this.activeTextColor, this.activeBgColor);
|
|
419
|
+
|
|
373
420
|
this.selection.setFakeBackground();
|
|
374
421
|
this.selection.save();
|
|
375
422
|
}
|
|
376
423
|
|
|
424
|
+
/**
|
|
425
|
+
* Update the color indicator on the inline toolbar marker button.
|
|
426
|
+
* Background color is applied via SVG rect fill so it stays clipped to the rounded square.
|
|
427
|
+
* Button background-color is set to transparent to suppress active-state Tailwind selectors.
|
|
428
|
+
* @param textColor - CSS color value for the icon/text, or null to reset
|
|
429
|
+
* @param bgColor - CSS color value for the button background, or null to reset
|
|
430
|
+
*/
|
|
431
|
+
private updateToolbarColors(textColor: string | null, bgColor: string | null): void {
|
|
432
|
+
const btn = document.querySelector<HTMLElement>('[data-blok-item-name="marker"]');
|
|
433
|
+
|
|
434
|
+
if (!btn) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (textColor !== null) {
|
|
439
|
+
btn.style.setProperty('color', textColor);
|
|
440
|
+
} else if (bgColor !== null) {
|
|
441
|
+
// Suppress active-state blue (text-icon-active-text) when only a background color is set.
|
|
442
|
+
btn.style.setProperty('color', 'var(--blok-text-primary)');
|
|
443
|
+
} else {
|
|
444
|
+
btn.style.removeProperty('color');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const rect = btn.querySelector<SVGRectElement>('svg rect');
|
|
448
|
+
|
|
449
|
+
if (bgColor !== null) {
|
|
450
|
+
// Fill the SVG rect so the color is clipped to the rounded square shape.
|
|
451
|
+
// Transparent on the button suppresses active-state bg-icon-active-bg.
|
|
452
|
+
if (rect) {
|
|
453
|
+
rect.style.fill = bgColor;
|
|
454
|
+
}
|
|
455
|
+
btn.style.setProperty('background-color', 'transparent');
|
|
456
|
+
} else if (textColor !== null) {
|
|
457
|
+
// Use a neutral fill when only text color is applied so that:
|
|
458
|
+
// (a) the active-state blue (data-blok-popover-item-active:bg-icon-active-bg) is suppressed, and
|
|
459
|
+
// (b) light text colors remain visible regardless of the toolbar's own background.
|
|
460
|
+
if (rect) {
|
|
461
|
+
rect.style.fill = 'var(--blok-swatch-neutral-bg)';
|
|
462
|
+
}
|
|
463
|
+
btn.style.setProperty('background-color', 'transparent');
|
|
464
|
+
} else {
|
|
465
|
+
if (rect) {
|
|
466
|
+
rect.style.fill = '';
|
|
467
|
+
}
|
|
468
|
+
btn.style.removeProperty('background-color');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
377
472
|
/**
|
|
378
473
|
* Called when the picker popover closes — clean up selection state
|
|
379
474
|
*/
|
|
@@ -388,36 +483,67 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
388
483
|
}
|
|
389
484
|
|
|
390
485
|
/**
|
|
391
|
-
* Detect the
|
|
392
|
-
*
|
|
486
|
+
* Detect both text and background colors from the mark at the start of a range.
|
|
487
|
+
* Uses the raw inline style value directly. CSS variables are passed through and
|
|
488
|
+
* resolve correctly in a real browser; resolved rgb values are used as-is.
|
|
489
|
+
* @param range - The selection range to inspect
|
|
393
490
|
*/
|
|
394
|
-
private
|
|
491
|
+
private detectBothSelectionColors(range: Range): { text: string | null; bg: string | null } {
|
|
492
|
+
const mark = findMarkElement(range.startContainer);
|
|
493
|
+
|
|
494
|
+
if (!mark) {
|
|
495
|
+
return { text: null, bg: null };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const pickRaw = (prop: string): string | null => {
|
|
499
|
+
const raw = mark.style.getPropertyValue(prop);
|
|
500
|
+
|
|
501
|
+
return raw && raw !== 'transparent' ? raw : null;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
text: pickRaw('color'),
|
|
506
|
+
bg: pickRaw('background-color'),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Detect both text and background colors from the current selection's mark element
|
|
512
|
+
* using computed values (resolves CSS variables via getComputedStyle).
|
|
513
|
+
* Used by onPickerOpen() to highlight the correct swatches in both picker sections.
|
|
514
|
+
*/
|
|
515
|
+
private detectBothSelectionComputedColors(): { text: string | null; bg: string | null } {
|
|
395
516
|
const selection = window.getSelection();
|
|
396
517
|
|
|
397
518
|
if (!selection || selection.rangeCount === 0) {
|
|
398
|
-
return null;
|
|
519
|
+
return { text: null, bg: null };
|
|
399
520
|
}
|
|
400
521
|
|
|
401
522
|
const range = selection.getRangeAt(0);
|
|
402
523
|
const mark = findMarkElement(range.startContainer);
|
|
403
524
|
|
|
404
525
|
if (!mark) {
|
|
405
|
-
return null;
|
|
526
|
+
return { text: null, bg: null };
|
|
406
527
|
}
|
|
407
528
|
|
|
408
|
-
const
|
|
529
|
+
const computedStyle = window.getComputedStyle(mark);
|
|
409
530
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
531
|
+
const resolveColor = (prop: string): string | null => {
|
|
532
|
+
const raw = mark.style.getPropertyValue(prop);
|
|
413
533
|
|
|
414
|
-
|
|
534
|
+
if (!raw || raw === 'transparent') {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
415
537
|
|
|
416
|
-
|
|
417
|
-
return { value: bgColor, mode: 'background-color' };
|
|
418
|
-
}
|
|
538
|
+
const computed = computedStyle.getPropertyValue(prop);
|
|
419
539
|
|
|
420
|
-
|
|
540
|
+
return computed && computed !== 'transparent' ? computed : null;
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
text: resolveColor('color'),
|
|
545
|
+
bg: resolveColor('background-color'),
|
|
546
|
+
};
|
|
421
547
|
}
|
|
422
548
|
|
|
423
549
|
/**
|
|
@@ -745,4 +871,21 @@ export class MarkerInlineTool implements InlineTool {
|
|
|
745
871
|
mark.style.setProperty('background-color', 'transparent');
|
|
746
872
|
}
|
|
747
873
|
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Translate a raw hex color value to its CSS custom property equivalent.
|
|
877
|
+
* If the value is already a CSS var or cannot be mapped, returns it unchanged.
|
|
878
|
+
* @param value - CSS color value from the picker
|
|
879
|
+
* @param mode - 'color' or 'background-color'
|
|
880
|
+
*/
|
|
881
|
+
private resolveToVar(value: string, mode: ColorMode): string {
|
|
882
|
+
if (value.startsWith('var(')) {
|
|
883
|
+
return value;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const presetMode = mode === 'color' ? 'text' : 'bg';
|
|
887
|
+
const name = mapToNearestPresetName(value, presetMode);
|
|
888
|
+
|
|
889
|
+
return name !== null ? colorVarName(name, presetMode) : value;
|
|
890
|
+
}
|
|
748
891
|
}
|
|
@@ -27,8 +27,17 @@ const nodeIntersectsRange = (range: Range, node: Node): boolean => {
|
|
|
27
27
|
* @param range - The range to iterate within
|
|
28
28
|
*/
|
|
29
29
|
export const createRangeTextWalker = (range: Range): TreeWalker => {
|
|
30
|
+
// When commonAncestorContainer is a text node, it has no descendants —
|
|
31
|
+
// the TreeWalker would find nothing. Use the parent element as root so
|
|
32
|
+
// the text node itself is reachable as a child. The nodeIntersectsRange
|
|
33
|
+
// filter still restricts results to nodes within the range.
|
|
34
|
+
const container = range.commonAncestorContainer;
|
|
35
|
+
const root = container.nodeType === Node.TEXT_NODE
|
|
36
|
+
? (container.parentNode ?? container)
|
|
37
|
+
: container;
|
|
38
|
+
|
|
30
39
|
return document.createTreeWalker(
|
|
31
|
-
|
|
40
|
+
root,
|
|
32
41
|
NodeFilter.SHOW_TEXT,
|
|
33
42
|
{
|
|
34
43
|
acceptNode: (node) => {
|
|
@@ -922,9 +922,9 @@ export class BlockManager extends Module {
|
|
|
922
922
|
/**
|
|
923
923
|
* Move a block to a new index
|
|
924
924
|
*/
|
|
925
|
-
public move(toIndex: number, fromIndex: number = this.currentBlockIndex, skipDOM = false): void {
|
|
925
|
+
public move(toIndex: number, fromIndex: number = this.currentBlockIndex, skipDOM = false, skipMovedHook = false): void {
|
|
926
926
|
this._currentBlockIndex = this.operations.currentBlockIndexValue;
|
|
927
|
-
this.operations.move(toIndex, fromIndex, skipDOM, this.blocksStore);
|
|
927
|
+
this.operations.move(toIndex, fromIndex, skipDOM, this.blocksStore, skipMovedHook);
|
|
928
928
|
this._currentBlockIndex = this.operations.currentBlockIndexValue;
|
|
929
929
|
}
|
|
930
930
|
|
|
@@ -613,7 +613,7 @@ export class BlockOperations {
|
|
|
613
613
|
* @param skipDOM - If true, do not manipulate DOM
|
|
614
614
|
* @param blocksStore - The blocks store to modify
|
|
615
615
|
*/
|
|
616
|
-
public move(toIndex: number, fromIndex: number, skipDOM: boolean, blocksStore: BlocksStore): void {
|
|
616
|
+
public move(toIndex: number, fromIndex: number, skipDOM: boolean, blocksStore: BlocksStore, skipMovedHook = false): void {
|
|
617
617
|
// Make sure indexes are valid and within a valid range
|
|
618
618
|
if (isNaN(toIndex) || isNaN(fromIndex)) {
|
|
619
619
|
log(`Warning during 'move' call: incorrect indices provided.`, 'warn');
|
|
@@ -642,7 +642,7 @@ export class BlockOperations {
|
|
|
642
642
|
this.suppressStopCapturing = true;
|
|
643
643
|
try {
|
|
644
644
|
/** Move up current Block */
|
|
645
|
-
blocksStore.move(toIndex, fromIndex, skipDOM);
|
|
645
|
+
blocksStore.move(toIndex, fromIndex, skipDOM, skipMovedHook);
|
|
646
646
|
|
|
647
647
|
/**
|
|
648
648
|
* After the move, the moved block may be at a different index than toIndex
|
|
@@ -117,21 +117,13 @@ export class BlockRepository {
|
|
|
117
117
|
return undefined;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const nodes = this.blocksStore.nodes;
|
|
121
|
-
|
|
122
120
|
const firstLevelBlock = normalizedElement.closest(createSelector(DATA_ATTR.element));
|
|
123
121
|
|
|
124
122
|
if (!firstLevelBlock) {
|
|
125
123
|
return undefined;
|
|
126
124
|
}
|
|
127
125
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (index >= 0) {
|
|
131
|
-
return this.blocksStore[index];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return undefined;
|
|
126
|
+
return this.blocks.find((block) => block.holder === firstLevelBlock);
|
|
135
127
|
}
|
|
136
128
|
|
|
137
129
|
/**
|
|
@@ -109,5 +109,5 @@ export type BlocksStore = Blocks & {
|
|
|
109
109
|
[index: number]: Block | undefined;
|
|
110
110
|
insert(index: number, block: Block, replace?: boolean, appendToWorkingArea?: boolean): void;
|
|
111
111
|
remove(index: number): void;
|
|
112
|
-
move(toIndex: number, fromIndex: number, skipDOM?: boolean): void;
|
|
112
|
+
move(toIndex: number, fromIndex: number, skipDOM?: boolean, skipMovedHook?: boolean): void;
|
|
113
113
|
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* DragOperations - Handles move and duplicate operations for drag and drop
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { BlockToolAPI } from '../../../block';
|
|
5
6
|
import type { Block } from '../../../block';
|
|
6
7
|
|
|
7
8
|
export interface SavedBlockData {
|
|
@@ -22,7 +23,7 @@ export interface DuplicateResult {
|
|
|
22
23
|
export interface BlockManagerAdapter {
|
|
23
24
|
getBlockIndex(block: Block): number;
|
|
24
25
|
getBlockByIndex(index: number): Block | undefined;
|
|
25
|
-
move(toIndex: number, fromIndex: number, needToFocus: boolean): void;
|
|
26
|
+
move(toIndex: number, fromIndex: number, needToFocus: boolean, skipMovedHook?: boolean): void;
|
|
26
27
|
insert(config: {
|
|
27
28
|
tool: string;
|
|
28
29
|
data: Record<string, unknown>;
|
|
@@ -269,25 +270,55 @@ export class DragOperations {
|
|
|
269
270
|
|
|
270
271
|
/**
|
|
271
272
|
* Moves blocks down (to a higher index)
|
|
273
|
+
*
|
|
274
|
+
* Blocks are processed in reverse order so each lands at its exact final position
|
|
275
|
+
* without index-shifting side-effects. The `moved()` lifecycle hook is suppressed
|
|
276
|
+
* during the loop because depth-validators in list items read neighbour depths from
|
|
277
|
+
* the DOM — in intermediate states those neighbours haven't arrived yet, causing
|
|
278
|
+
* depths to be incorrectly capped. After all blocks are in place the hooks are
|
|
279
|
+
* re-triggered in document order (parent → children) so each block sees correct
|
|
280
|
+
* neighbours when it validates.
|
|
272
281
|
*/
|
|
273
282
|
private moveBlocksDown(sortedBlocks: Block[], insertIndex: number): void {
|
|
274
|
-
|
|
275
|
-
|
|
283
|
+
const originalIndices = new Map<Block, number>();
|
|
284
|
+
|
|
285
|
+
sortedBlocks.forEach(block => {
|
|
286
|
+
originalIndices.set(block, this.blockManager.getBlockIndex(block));
|
|
287
|
+
});
|
|
288
|
+
|
|
276
289
|
const reversedBlocks = [...sortedBlocks].reverse();
|
|
277
290
|
|
|
278
291
|
reversedBlocks.forEach((block, index) => {
|
|
279
292
|
const currentIndex = this.blockManager.getBlockIndex(block);
|
|
280
293
|
const targetPosition = insertIndex - 1 - index;
|
|
281
294
|
|
|
282
|
-
this.blockManager.move(targetPosition, currentIndex, false);
|
|
295
|
+
this.blockManager.move(targetPosition, currentIndex, false, true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
sortedBlocks.forEach(block => {
|
|
299
|
+
block.call(BlockToolAPI.MOVED, {
|
|
300
|
+
fromIndex: originalIndices.get(block) ?? 0,
|
|
301
|
+
toIndex: this.blockManager.getBlockIndex(block),
|
|
302
|
+
isGroupMove: true,
|
|
303
|
+
});
|
|
283
304
|
});
|
|
284
305
|
}
|
|
285
306
|
|
|
286
307
|
/**
|
|
287
308
|
* Moves blocks up (to a lower index)
|
|
309
|
+
*
|
|
310
|
+
* Forward order is used so parent blocks arrive at their target before children,
|
|
311
|
+
* giving depth-validators the correct predecessor. The `moved()` hook is still
|
|
312
|
+
* suppressed during the loop and re-triggered afterward for symmetry with
|
|
313
|
+
* `moveBlocksDown` and to guard against any edge-cases in future reordering.
|
|
288
314
|
*/
|
|
289
315
|
private moveBlocksUp(sortedBlocks: Block[], baseInsertIndex: number): void {
|
|
290
|
-
|
|
316
|
+
const originalIndices = new Map<Block, number>();
|
|
317
|
+
|
|
318
|
+
sortedBlocks.forEach(block => {
|
|
319
|
+
originalIndices.set(block, this.blockManager.getBlockIndex(block));
|
|
320
|
+
});
|
|
321
|
+
|
|
291
322
|
sortedBlocks.forEach((block, index) => {
|
|
292
323
|
const currentIndex = this.blockManager.getBlockIndex(block);
|
|
293
324
|
const targetIndex = baseInsertIndex + index;
|
|
@@ -296,7 +327,15 @@ export class DragOperations {
|
|
|
296
327
|
return;
|
|
297
328
|
}
|
|
298
329
|
|
|
299
|
-
this.blockManager.move(targetIndex, currentIndex, false);
|
|
330
|
+
this.blockManager.move(targetIndex, currentIndex, false, true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
sortedBlocks.forEach(block => {
|
|
334
|
+
block.call(BlockToolAPI.MOVED, {
|
|
335
|
+
fromIndex: originalIndices.get(block) ?? 0,
|
|
336
|
+
toIndex: this.blockManager.getBlockIndex(block),
|
|
337
|
+
isGroupMove: true,
|
|
338
|
+
});
|
|
300
339
|
});
|
|
301
340
|
}
|
|
302
341
|
}
|
|
@@ -84,6 +84,42 @@ function isDefaultBlack(color: string): boolean {
|
|
|
84
84
|
return normalized === 'rgb(0,0,0)' || normalized === '#000000';
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Compute the relative luminance of a CSS color value (rgb() or hex format).
|
|
89
|
+
* Returns a value in [0, 1], or -1 if the format is unrecognized.
|
|
90
|
+
* Uses simplified linear luminance (no gamma correction), adequate for
|
|
91
|
+
* threshold comparisons at this scale.
|
|
92
|
+
*/
|
|
93
|
+
function computeRelativeLuminance(color: string): number {
|
|
94
|
+
const normalized = color.replace(/\s/g, '').toLowerCase();
|
|
95
|
+
|
|
96
|
+
const rgbMatch = /^rgb\((\d+),(\d+),(\d+)\)$/.exec(normalized);
|
|
97
|
+
|
|
98
|
+
if (rgbMatch) {
|
|
99
|
+
const r = parseInt(rgbMatch[1], 10) / 255;
|
|
100
|
+
const g = parseInt(rgbMatch[2], 10) / 255;
|
|
101
|
+
const b = parseInt(rgbMatch[3], 10) / 255;
|
|
102
|
+
|
|
103
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const hexMatch = /^#([0-9a-f]{6}|[0-9a-f]{3})$/.exec(normalized);
|
|
107
|
+
|
|
108
|
+
if (hexMatch) {
|
|
109
|
+
const hex = hexMatch[1];
|
|
110
|
+
const expand = hex.length === 3
|
|
111
|
+
? [hex[0] + hex[0], hex[1] + hex[1], hex[2] + hex[2]]
|
|
112
|
+
: [hex.substring(0, 2), hex.substring(2, 4), hex.substring(4, 6)];
|
|
113
|
+
const r = parseInt(expand[0], 16) / 255;
|
|
114
|
+
const g = parseInt(expand[1], 16) / 255;
|
|
115
|
+
const b = parseInt(expand[2], 16) / 255;
|
|
116
|
+
|
|
117
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return -1;
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
/**
|
|
88
124
|
* Check whether a CSS background-color value is the default white page background.
|
|
89
125
|
* When the browser natively copies from a contenteditable, it adds computed styles
|
|
@@ -96,6 +132,35 @@ function isDefaultWhiteBackground(bgColor: string): boolean {
|
|
|
96
132
|
return normalized === 'rgb(255,255,255)' || normalized === '#ffffff' || normalized === 'white';
|
|
97
133
|
}
|
|
98
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Check whether a CSS background-color value is a near-black (dark mode page) background.
|
|
137
|
+
* When the browser natively copies from a contenteditable in dark mode, it adds computed
|
|
138
|
+
* styles including the resolved dark page background (e.g. rgb(25, 25, 24) for Blok's
|
|
139
|
+
* #191918 dark background). These should not be treated as intentional marker formatting.
|
|
140
|
+
*
|
|
141
|
+
* Uses relative luminance < 0.12, which is below all Blok dark background presets
|
|
142
|
+
* (~18% minimum lightness for #2f2f2f) while catching typical dark page backgrounds.
|
|
143
|
+
*/
|
|
144
|
+
function isDefaultDarkBackground(bgColor: string): boolean {
|
|
145
|
+
const luminance = computeRelativeLuminance(bgColor);
|
|
146
|
+
|
|
147
|
+
return luminance >= 0 && luminance < 0.12;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check whether a CSS color value is a near-white (dark mode default text) color.
|
|
152
|
+
* When the browser natively copies from a contenteditable in dark mode, it includes
|
|
153
|
+
* the resolved light page text color (e.g. rgb(226, 224, 220) for Blok's #e2e0dc
|
|
154
|
+
* default text). These should not be treated as intentional marker formatting for
|
|
155
|
+
* non-Google-Docs content.
|
|
156
|
+
*
|
|
157
|
+
* Uses relative luminance > 0.75, which is above all Blok text presets while
|
|
158
|
+
* catching typical dark mode default text colors.
|
|
159
|
+
*/
|
|
160
|
+
function isDefaultLightText(color: string): boolean {
|
|
161
|
+
return computeRelativeLuminance(color) > 0.75;
|
|
162
|
+
}
|
|
163
|
+
|
|
99
164
|
/**
|
|
100
165
|
* Optionally wrap innerHTML in a `<mark>` with mapped color styles.
|
|
101
166
|
* Returns the original content unchanged when no color formatting is needed.
|
|
@@ -145,10 +210,12 @@ function convertSpanToSemanticHtml(span: Element, isGoogleDocs: boolean): string
|
|
|
145
210
|
const color = colorMatch?.[1]?.trim();
|
|
146
211
|
const bgColor = bgMatch?.[1]?.trim();
|
|
147
212
|
|
|
148
|
-
const hasColor =
|
|
213
|
+
const hasColor = isGoogleDocs
|
|
214
|
+
? color !== undefined && !isDefaultBlack(color)
|
|
215
|
+
: color !== undefined && !isDefaultBlack(color) && !isDefaultLightText(color);
|
|
149
216
|
const hasBgColor = isGoogleDocs
|
|
150
217
|
? bgColor !== undefined && bgColor !== 'transparent'
|
|
151
|
-
: bgColor !== undefined && bgColor !== 'transparent' && !isDefaultWhiteBackground(bgColor);
|
|
218
|
+
: bgColor !== undefined && bgColor !== 'transparent' && !isDefaultWhiteBackground(bgColor) && !isDefaultDarkBackground(bgColor);
|
|
152
219
|
|
|
153
220
|
if (!isBold && !isItalic && !hasColor && !hasBgColor) {
|
|
154
221
|
return null;
|