@jackuait/blok 0.4.1-beta.17 → 0.4.1-beta.19
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-BmQiBq7w.mjs → blok-zaWxnlMM.mjs} +680 -526
- package/dist/chunks/{i18next-loader-CtUJZQir.mjs → i18next-loader-CI8T9PDi.mjs} +1 -1
- package/dist/chunks/{index-CvHTp5IA.mjs → index-D9haze7z.mjs} +1 -1
- package/dist/chunks/{inline-tool-convert-f0-Y0Vcm.mjs → inline-tool-convert-CoQJYHI_.mjs} +1 -1
- package/dist/full.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +1 -1
- package/src/components/block/index.ts +15 -0
- package/src/components/modules/api/blocks.ts +27 -0
- package/src/components/modules/blockManager.ts +4 -0
- package/src/components/modules/history/index.ts +7 -0
- package/src/components/modules/history/smart-grouping.ts +98 -0
- package/src/components/modules/history/types.ts +56 -0
- package/src/components/modules/history.ts +145 -6
- package/src/components/modules/paste.ts +47 -7
- package/src/components/modules/renderer.ts +3 -1
- package/types/api/blocks.d.ts +15 -0
- package/types/data-formats/output-data.d.ts +7 -0
|
@@ -18,7 +18,7 @@ let nt = (o = 21) => {
|
|
|
18
18
|
return t;
|
|
19
19
|
};
|
|
20
20
|
var ot = /* @__PURE__ */ ((o) => (o.VERBOSE = "VERBOSE", o.INFO = "INFO", o.WARN = "WARN", o.ERROR = "ERROR", o))(ot || {});
|
|
21
|
-
const rt = () => "0.4.1-beta.
|
|
21
|
+
const rt = () => "0.4.1-beta.19", Ct = {
|
|
22
22
|
BACKSPACE: 8,
|
|
23
23
|
TAB: 9,
|
|
24
24
|
ENTER: 13,
|
package/dist/full.mjs
CHANGED
|
@@ -10,10 +10,10 @@ var e = (a, l, o) => l in a ? n(a, l, { enumerable: !0, configurable: !0, writab
|
|
|
10
10
|
d.call(l, o) && e(a, o, l[o]);
|
|
11
11
|
return a;
|
|
12
12
|
}, r = (a, l) => t(a, c(l));
|
|
13
|
-
import { B as v, v as A } from "./chunks/blok-
|
|
13
|
+
import { B as v, v as A } from "./chunks/blok-zaWxnlMM.mjs";
|
|
14
14
|
import { List as p, Header as f, Paragraph as I, Link as k, Italic as u, Bold as B } from "./tools.mjs";
|
|
15
15
|
import { defaultBlockTools as H, defaultInlineTools as P } from "./tools.mjs";
|
|
16
|
-
import { D as _ } from "./chunks/inline-tool-convert-
|
|
16
|
+
import { D as _ } from "./chunks/inline-tool-convert-CoQJYHI_.mjs";
|
|
17
17
|
const m = {
|
|
18
18
|
paragraph: {
|
|
19
19
|
class: I,
|
package/dist/tools.mjs
CHANGED
|
@@ -10,8 +10,8 @@ var U = (f, t, e) => t in f ? nt(f, t, { enumerable: !0, configurable: !0, writa
|
|
|
10
10
|
it.call(t, e) && U(f, e, t[e]);
|
|
11
11
|
return f;
|
|
12
12
|
}, P = (f, t) => rt(f, st(t));
|
|
13
|
-
import { t as x, D as m, a9 as et, aa as at, ab as lt, A as ct, ac as dt, ad as ut, ae as ht, af as ft, ag as pt, ah as mt, ai as G, aj as j, ak as $, f as A, al as gt, am as Et, S as H, P as Tt, an as Ct, l as At, J as yt } from "./chunks/inline-tool-convert-
|
|
14
|
-
import { a0 as Dt } from "./chunks/inline-tool-convert-
|
|
13
|
+
import { t as x, D as m, a9 as et, aa as at, ab as lt, A as ct, ac as dt, ad as ut, ae as ht, af as ft, ag as pt, ah as mt, ai as G, aj as j, ak as $, f as A, al as gt, am as Et, S as H, P as Tt, an as Ct, l as At, J as yt } from "./chunks/inline-tool-convert-CoQJYHI_.mjs";
|
|
14
|
+
import { a0 as Dt } from "./chunks/inline-tool-convert-CoQJYHI_.mjs";
|
|
15
15
|
const W = [
|
|
16
16
|
"empty:before:pointer-events-none",
|
|
17
17
|
"empty:before:text-gray-text",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackuait/blok",
|
|
3
|
-
"version": "0.4.1-beta.
|
|
3
|
+
"version": "0.4.1-beta.19",
|
|
4
4
|
"description": "Blok — headless, highly extensible rich text editor built for developers who need to implement a block-based editing experience (similar to Notion) without building it from scratch",
|
|
5
5
|
"module": "dist/blok.mjs",
|
|
6
6
|
"types": "./types/index.d.ts",
|
|
@@ -79,6 +79,12 @@ interface BlockConstructorOptions {
|
|
|
79
79
|
* References blocks that are children of this block.
|
|
80
80
|
*/
|
|
81
81
|
contentIds?: string[];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Slot index within parent container (e.g., which column in a columns block).
|
|
85
|
+
* Used to organize children into specific slots for container blocks.
|
|
86
|
+
*/
|
|
87
|
+
slot?: number;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
/**
|
|
@@ -142,6 +148,12 @@ export class Block extends EventsDispatcher<BlockEvents> {
|
|
|
142
148
|
*/
|
|
143
149
|
public contentIds: string[];
|
|
144
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Slot index within parent container (e.g., which column in a columns block).
|
|
153
|
+
* Null if this block is not assigned to a specific slot.
|
|
154
|
+
*/
|
|
155
|
+
public slot: number | null;
|
|
156
|
+
|
|
145
157
|
/**
|
|
146
158
|
* Block Tool`s name
|
|
147
159
|
*/
|
|
@@ -259,6 +271,7 @@ export class Block extends EventsDispatcher<BlockEvents> {
|
|
|
259
271
|
* @param options.readOnly - Read-Only flag
|
|
260
272
|
* @param [options.parentId] - parent block id for hierarchical structure
|
|
261
273
|
* @param [options.contentIds] - array of child block ids
|
|
274
|
+
* @param [options.slot] - slot index within parent container
|
|
262
275
|
* @param [eventBus] - Blok common event bus. Allows to subscribe on some Blok events. Could be omitted when "virtual" Block is created. See BlocksAPI@composeBlockData.
|
|
263
276
|
*/
|
|
264
277
|
constructor({
|
|
@@ -269,6 +282,7 @@ export class Block extends EventsDispatcher<BlockEvents> {
|
|
|
269
282
|
tunesData,
|
|
270
283
|
parentId,
|
|
271
284
|
contentIds,
|
|
285
|
+
slot,
|
|
272
286
|
}: BlockConstructorOptions, eventBus?: EventsDispatcher<BlokEventMap>) {
|
|
273
287
|
super();
|
|
274
288
|
this.ready = new Promise((resolve) => {
|
|
@@ -278,6 +292,7 @@ export class Block extends EventsDispatcher<BlockEvents> {
|
|
|
278
292
|
this.id = id;
|
|
279
293
|
this.parentId = parentId ?? null;
|
|
280
294
|
this.contentIds = contentIds ?? [];
|
|
295
|
+
this.slot = slot ?? null;
|
|
281
296
|
this.settings = tool.settings;
|
|
282
297
|
this.config = this.settings;
|
|
283
298
|
this.blokEventBus = eventBus || null;
|
|
@@ -29,6 +29,8 @@ export class BlocksAPI extends Module {
|
|
|
29
29
|
getBlockIndex: (id: string): number | undefined => this.getBlockIndex(id),
|
|
30
30
|
getBlocksCount: (): number => this.getBlocksCount(),
|
|
31
31
|
getBlockByElement: (element: HTMLElement) => this.getBlockByElement(element),
|
|
32
|
+
getChildren: (parentId: string): BlockAPIInterface[] => this.getChildren(parentId),
|
|
33
|
+
getChildrenInSlot: (parentId: string, slot: number): BlockAPIInterface[] => this.getChildrenInSlot(parentId, slot),
|
|
32
34
|
insert: this.insert,
|
|
33
35
|
insertMany: this.insertMany,
|
|
34
36
|
update: this.update,
|
|
@@ -118,6 +120,31 @@ export class BlocksAPI extends Module {
|
|
|
118
120
|
return new BlockAPI(block);
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Returns all child blocks of a parent container block
|
|
125
|
+
* @param parentId - id of the parent block
|
|
126
|
+
*/
|
|
127
|
+
public getChildren(parentId: string): BlockAPIInterface[] {
|
|
128
|
+
const children = this.Blok.BlockManager.blocks.filter(
|
|
129
|
+
(block) => block.parentId === parentId
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return children.map((block) => new BlockAPI(block));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Returns child blocks of a parent container block in a specific slot
|
|
137
|
+
* @param parentId - id of the parent block
|
|
138
|
+
* @param slot - slot index (e.g., column index for columns block)
|
|
139
|
+
*/
|
|
140
|
+
public getChildrenInSlot(parentId: string, slot: number): BlockAPIInterface[] {
|
|
141
|
+
const children = this.Blok.BlockManager.blocks.filter(
|
|
142
|
+
(block) => block.parentId === parentId && block.slot === slot
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return children.map((block) => new BlockAPI(block));
|
|
146
|
+
}
|
|
147
|
+
|
|
121
148
|
/**
|
|
122
149
|
* Move block from one index to another
|
|
123
150
|
* @param {number} toIndex - index to move to
|
|
@@ -235,6 +235,7 @@ export class BlockManager extends Module {
|
|
|
235
235
|
* @param {BlockToolData} [options.data] - constructor params
|
|
236
236
|
* @param {string} [options.parentId] - parent block id for hierarchical structure
|
|
237
237
|
* @param {string[]} [options.contentIds] - array of child block ids
|
|
238
|
+
* @param {number} [options.slot] - slot index within parent container
|
|
238
239
|
* @returns {Block}
|
|
239
240
|
*/
|
|
240
241
|
public composeBlock({
|
|
@@ -244,6 +245,7 @@ export class BlockManager extends Module {
|
|
|
244
245
|
tunes: tunesData = {},
|
|
245
246
|
parentId,
|
|
246
247
|
contentIds,
|
|
248
|
+
slot,
|
|
247
249
|
}: {
|
|
248
250
|
tool: string;
|
|
249
251
|
id?: string;
|
|
@@ -251,6 +253,7 @@ export class BlockManager extends Module {
|
|
|
251
253
|
tunes?: {[name: string]: BlockTuneData};
|
|
252
254
|
parentId?: string;
|
|
253
255
|
contentIds?: string[];
|
|
256
|
+
slot?: number;
|
|
254
257
|
}): Block {
|
|
255
258
|
const readOnly = this.Blok.ReadOnly.isEnabled;
|
|
256
259
|
const tool = this.Blok.Tools.blockTools.get(name);
|
|
@@ -268,6 +271,7 @@ export class BlockManager extends Module {
|
|
|
268
271
|
tunesData,
|
|
269
272
|
parentId,
|
|
270
273
|
contentIds,
|
|
274
|
+
slot,
|
|
271
275
|
}, this.eventsDispatcher);
|
|
272
276
|
|
|
273
277
|
if (!readOnly) {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart grouping logic for history checkpoints
|
|
3
|
+
* @module History/SmartGrouping
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ActionContext, ActionType, MutationMetadata } from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Actions that should create an immediate checkpoint (before the action)
|
|
10
|
+
*/
|
|
11
|
+
const IMMEDIATE_CHECKPOINT_ACTIONS: ActionType[] = [
|
|
12
|
+
'format',
|
|
13
|
+
'structural',
|
|
14
|
+
'paste',
|
|
15
|
+
'cut',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Determines when to create history checkpoints based on action patterns
|
|
20
|
+
*
|
|
21
|
+
* Creates checkpoints when:
|
|
22
|
+
* - Action type changes (e.g., typing → deleting)
|
|
23
|
+
* - Block changes
|
|
24
|
+
* - Immediate actions (format, structural, paste, cut)
|
|
25
|
+
*/
|
|
26
|
+
export class SmartGrouping {
|
|
27
|
+
/**
|
|
28
|
+
* Current action context
|
|
29
|
+
*/
|
|
30
|
+
private currentContext: ActionContext | undefined;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Determines if a checkpoint should be created before recording this mutation
|
|
34
|
+
* @param metadata - mutation metadata with action type info
|
|
35
|
+
* @param blockId - ID of the block being mutated
|
|
36
|
+
* @returns true if a checkpoint should be created
|
|
37
|
+
*/
|
|
38
|
+
public shouldCreateCheckpoint(
|
|
39
|
+
metadata: MutationMetadata,
|
|
40
|
+
blockId: string
|
|
41
|
+
): boolean {
|
|
42
|
+
const actionType = metadata.actionType ?? 'insert';
|
|
43
|
+
|
|
44
|
+
// No current context means this is the first action - don't checkpoint
|
|
45
|
+
if (!this.currentContext) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Block changed - create checkpoint
|
|
50
|
+
if (this.currentContext.blockId !== blockId) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Action type changed (e.g., typing → deleting) - create checkpoint
|
|
55
|
+
if (this.currentContext.type !== actionType) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Checks if this action type should trigger an immediate checkpoint
|
|
64
|
+
* @param actionType - the action type to check
|
|
65
|
+
* @returns true if this action should create an immediate checkpoint
|
|
66
|
+
*/
|
|
67
|
+
public isImmediateCheckpoint(actionType: ActionType): boolean {
|
|
68
|
+
return IMMEDIATE_CHECKPOINT_ACTIONS.includes(actionType);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Updates the current action context
|
|
73
|
+
* @param actionType - the new action type
|
|
74
|
+
* @param blockId - the block being edited
|
|
75
|
+
*/
|
|
76
|
+
public updateContext(actionType: ActionType, blockId: string): void {
|
|
77
|
+
this.currentContext = {
|
|
78
|
+
type: actionType,
|
|
79
|
+
blockId,
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gets the current action context
|
|
86
|
+
* @returns the current context or undefined
|
|
87
|
+
*/
|
|
88
|
+
public getCurrentContext(): ActionContext | undefined {
|
|
89
|
+
return this.currentContext;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Clears the current action context
|
|
94
|
+
*/
|
|
95
|
+
public clearContext(): void {
|
|
96
|
+
this.currentContext = undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for smart history grouping
|
|
3
|
+
* @module History/Types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Action types that can be detected from mutations
|
|
8
|
+
*/
|
|
9
|
+
export type ActionType =
|
|
10
|
+
| 'insert' // Typing characters
|
|
11
|
+
| 'delete-back' // Backspace deletion
|
|
12
|
+
| 'delete-fwd' // Forward delete
|
|
13
|
+
| 'format' // Inline formatting (bold, italic, etc.)
|
|
14
|
+
| 'structural' // Block-level changes (split, merge)
|
|
15
|
+
| 'paste' // Paste operation
|
|
16
|
+
| 'cut'; // Cut operation
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Metadata about a mutation for smart grouping decisions
|
|
20
|
+
*/
|
|
21
|
+
export interface MutationMetadata {
|
|
22
|
+
/**
|
|
23
|
+
* Type of action that caused this mutation
|
|
24
|
+
*/
|
|
25
|
+
actionType?: ActionType;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Text that was inserted (captured by MutationDetector)
|
|
29
|
+
*/
|
|
30
|
+
insertedText?: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Text that was deleted (captured by MutationDetector)
|
|
34
|
+
*/
|
|
35
|
+
deletedText?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Context about the current action sequence
|
|
40
|
+
*/
|
|
41
|
+
export interface ActionContext {
|
|
42
|
+
/**
|
|
43
|
+
* The type of action being performed
|
|
44
|
+
*/
|
|
45
|
+
type: ActionType;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* ID of the block being edited
|
|
49
|
+
*/
|
|
50
|
+
blockId: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Timestamp when this context started
|
|
54
|
+
*/
|
|
55
|
+
timestamp: number;
|
|
56
|
+
}
|
|
@@ -9,6 +9,8 @@ import { BlockChanged, HistoryStateChanged } from '../events';
|
|
|
9
9
|
import type { BlockMutationEvent } from '../../../types/events/block';
|
|
10
10
|
import { Shortcuts } from '../utils/shortcuts';
|
|
11
11
|
import type { Block } from '../block';
|
|
12
|
+
import { SmartGrouping } from './history/smart-grouping';
|
|
13
|
+
import type { ActionType } from './history/types';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Default maximum history stack size
|
|
@@ -117,6 +119,21 @@ export class History extends Module {
|
|
|
117
119
|
*/
|
|
118
120
|
private initialStateCaptured = false;
|
|
119
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Smart grouping logic for determining when to create checkpoints
|
|
124
|
+
*/
|
|
125
|
+
private smartGrouping = new SmartGrouping();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Current action type being tracked (for detecting action type changes)
|
|
129
|
+
*/
|
|
130
|
+
private currentActionType: ActionType = 'insert';
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Keydown handler reference for cleanup
|
|
134
|
+
*/
|
|
135
|
+
private keydownHandler: ((e: KeyboardEvent) => void) | null = null;
|
|
136
|
+
|
|
120
137
|
/**
|
|
121
138
|
* Maximum number of entries in history stack
|
|
122
139
|
*/
|
|
@@ -145,6 +162,7 @@ export class History extends Module {
|
|
|
145
162
|
public async prepare(): Promise<void> {
|
|
146
163
|
this.setupEventListeners();
|
|
147
164
|
this.setupKeyboardShortcuts();
|
|
165
|
+
this.setupActionTypeTracking();
|
|
148
166
|
}
|
|
149
167
|
|
|
150
168
|
/**
|
|
@@ -310,6 +328,7 @@ export class History extends Module {
|
|
|
310
328
|
this.undoStack = [];
|
|
311
329
|
this.redoStack = [];
|
|
312
330
|
this.initialStateCaptured = false;
|
|
331
|
+
this.smartGrouping.clearContext();
|
|
313
332
|
this.emitStateChanged();
|
|
314
333
|
}
|
|
315
334
|
|
|
@@ -433,11 +452,47 @@ export class History extends Module {
|
|
|
433
452
|
return false;
|
|
434
453
|
}
|
|
435
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Sets up keydown tracking for action type detection
|
|
457
|
+
*/
|
|
458
|
+
private setupActionTypeTracking(): void {
|
|
459
|
+
// Wait for UI to be ready
|
|
460
|
+
setTimeout(() => {
|
|
461
|
+
const redactor = this.Blok.UI?.nodes?.redactor;
|
|
462
|
+
|
|
463
|
+
if (!redactor) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
this.keydownHandler = (e: KeyboardEvent): void => {
|
|
468
|
+
// Detect action type from key press
|
|
469
|
+
if (e.key === 'Backspace') {
|
|
470
|
+
this.currentActionType = 'delete-back';
|
|
471
|
+
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (e.key === 'Delete') {
|
|
476
|
+
this.currentActionType = 'delete-fwd';
|
|
477
|
+
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Single character key (typing)
|
|
482
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
|
483
|
+
this.currentActionType = 'insert';
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
redactor.addEventListener('keydown', this.keydownHandler);
|
|
488
|
+
}, 0);
|
|
489
|
+
}
|
|
490
|
+
|
|
436
491
|
/**
|
|
437
492
|
* Handles block mutation events
|
|
438
|
-
*
|
|
493
|
+
* Uses smart grouping to create checkpoints when action type changes
|
|
439
494
|
*/
|
|
440
|
-
private handleBlockMutation(
|
|
495
|
+
private handleBlockMutation(event: BlockMutationEvent): void {
|
|
441
496
|
// Mark this instance as active for global shortcuts
|
|
442
497
|
History.activeInstance = this;
|
|
443
498
|
|
|
@@ -453,10 +508,39 @@ export class History extends Module {
|
|
|
453
508
|
return;
|
|
454
509
|
}
|
|
455
510
|
|
|
456
|
-
//
|
|
457
|
-
|
|
511
|
+
// Get block ID from event
|
|
512
|
+
const blockId = event.detail.target.id;
|
|
513
|
+
|
|
514
|
+
// Check if we should create a checkpoint before this action
|
|
515
|
+
const shouldCheckpoint = this.smartGrouping.shouldCreateCheckpoint(
|
|
516
|
+
{ actionType: this.currentActionType },
|
|
517
|
+
blockId
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
// Check if this is an immediate checkpoint action
|
|
521
|
+
const isImmediate = this.smartGrouping.isImmediateCheckpoint(this.currentActionType);
|
|
522
|
+
|
|
523
|
+
if (shouldCheckpoint || isImmediate) {
|
|
524
|
+
// Create checkpoint immediately (flush pending debounce)
|
|
525
|
+
this.clearDebounce();
|
|
526
|
+
void this.recordState().then(() => {
|
|
527
|
+
// Update context after recording
|
|
528
|
+
this.smartGrouping.updateContext(this.currentActionType, blockId);
|
|
529
|
+
// Start new debounce for continued editing
|
|
530
|
+
this.startDebounce();
|
|
531
|
+
});
|
|
532
|
+
} else {
|
|
533
|
+
// Update context and debounce normally
|
|
534
|
+
this.smartGrouping.updateContext(this.currentActionType, blockId);
|
|
535
|
+
this.clearDebounce();
|
|
536
|
+
this.startDebounce();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
458
539
|
|
|
459
|
-
|
|
540
|
+
/**
|
|
541
|
+
* Starts the debounce timer for recording state
|
|
542
|
+
*/
|
|
543
|
+
private startDebounce(): void {
|
|
460
544
|
this.debounceTimeout = setTimeout(() => {
|
|
461
545
|
void this.recordState();
|
|
462
546
|
}, this.debounceTime);
|
|
@@ -491,6 +575,14 @@ export class History extends Module {
|
|
|
491
575
|
// Capture caret position along with state
|
|
492
576
|
const caretPosition = this.getCaretPosition();
|
|
493
577
|
|
|
578
|
+
// Check if this state is identical to the last entry (avoid duplicates)
|
|
579
|
+
const lastEntry = this.undoStack[this.undoStack.length - 1];
|
|
580
|
+
|
|
581
|
+
if (lastEntry && this.areStatesEqual(lastEntry.state, state)) {
|
|
582
|
+
// State hasn't changed, no need to record
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
494
586
|
// Clear redo stack when new changes are made
|
|
495
587
|
this.redoStack = [];
|
|
496
588
|
|
|
@@ -561,6 +653,44 @@ export class History extends Module {
|
|
|
561
653
|
}
|
|
562
654
|
}
|
|
563
655
|
|
|
656
|
+
/**
|
|
657
|
+
* Compares two states for equality (ignoring timestamps)
|
|
658
|
+
* @param a - first state
|
|
659
|
+
* @param b - second state
|
|
660
|
+
* @returns true if the block content is identical
|
|
661
|
+
*/
|
|
662
|
+
private areStatesEqual(a: OutputData, b: OutputData): boolean {
|
|
663
|
+
// Quick check: different number of blocks
|
|
664
|
+
if (a.blocks.length !== b.blocks.length) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Compare each block using every() for functional approach
|
|
669
|
+
return a.blocks.every((blockA, i) => {
|
|
670
|
+
const blockB = b.blocks[i];
|
|
671
|
+
|
|
672
|
+
// Check ID and type
|
|
673
|
+
if (blockA.id !== blockB.id || blockA.type !== blockB.type) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Compare data (deep comparison via JSON)
|
|
678
|
+
if (JSON.stringify(blockA.data) !== JSON.stringify(blockB.data)) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Compare tunes if present
|
|
683
|
+
const tunesA = JSON.stringify(blockA.tunes ?? {});
|
|
684
|
+
const tunesB = JSON.stringify(blockB.tunes ?? {});
|
|
685
|
+
|
|
686
|
+
if (tunesA !== tunesB) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return true;
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
564
694
|
/**
|
|
565
695
|
* Restores document to a given state using smart diffing
|
|
566
696
|
* Only updates blocks that have changed to preserve DOM state
|
|
@@ -1085,14 +1215,23 @@ export class History extends Module {
|
|
|
1085
1215
|
}
|
|
1086
1216
|
this.registeredShortcuts = [];
|
|
1087
1217
|
|
|
1218
|
+
// Remove keydown handler
|
|
1219
|
+
const redactor = this.Blok.UI?.nodes?.redactor;
|
|
1220
|
+
|
|
1221
|
+
if (this.keydownHandler && redactor) {
|
|
1222
|
+
redactor.removeEventListener('keydown', this.keydownHandler);
|
|
1223
|
+
}
|
|
1224
|
+
this.keydownHandler = null;
|
|
1225
|
+
|
|
1088
1226
|
// Clear active instance if it's this one
|
|
1089
1227
|
if (History.activeInstance === this) {
|
|
1090
1228
|
History.activeInstance = null;
|
|
1091
1229
|
}
|
|
1092
1230
|
|
|
1093
|
-
// Clear stacks
|
|
1231
|
+
// Clear stacks and smart grouping
|
|
1094
1232
|
this.undoStack = [];
|
|
1095
1233
|
this.redoStack = [];
|
|
1096
1234
|
this.initialStateCaptured = false;
|
|
1235
|
+
this.smartGrouping.clearContext();
|
|
1097
1236
|
}
|
|
1098
1237
|
}
|
|
@@ -270,14 +270,10 @@ export class Paste extends Module {
|
|
|
270
270
|
const normalizedHtmlData = rawHtmlData;
|
|
271
271
|
|
|
272
272
|
/**
|
|
273
|
-
* If Blok json is passed,
|
|
273
|
+
* If Blok json is passed, check if we should try pattern matching first
|
|
274
274
|
*/
|
|
275
|
-
if (blokData) {
|
|
276
|
-
|
|
277
|
-
this.insertBlokData(JSON.parse(blokData));
|
|
278
|
-
|
|
279
|
-
return;
|
|
280
|
-
} catch (_e) { } // Do nothing and continue execution as usual if error appears
|
|
275
|
+
if (blokData && await this.handleBlokDataPaste(blokData, plainData)) {
|
|
276
|
+
return;
|
|
281
277
|
}
|
|
282
278
|
|
|
283
279
|
/** Add all tags that can be substituted to sanitizer configuration */
|
|
@@ -307,6 +303,50 @@ export class Paste extends Module {
|
|
|
307
303
|
}
|
|
308
304
|
}
|
|
309
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Handles pasting of Blok JSON data, with pattern matching priority.
|
|
308
|
+
* For plain text that might match a pattern (like URLs), tries pattern matching first.
|
|
309
|
+
* This handles the case where text is cut and pasted within the same editor -
|
|
310
|
+
* we want pattern matching to still work for things like embed URLs.
|
|
311
|
+
* @param blokData - serialized Blok JSON data
|
|
312
|
+
* @param plainData - plain text content from clipboard
|
|
313
|
+
* @returns true if paste was handled, false otherwise
|
|
314
|
+
*/
|
|
315
|
+
private async handleBlokDataPaste(blokData: string, plainData: string): Promise<boolean> {
|
|
316
|
+
try {
|
|
317
|
+
const parsedBlokData = JSON.parse(blokData) as Pick<SavedData, 'id' | 'data' | 'tool'>[];
|
|
318
|
+
const shouldTryPatternMatch = plainData && this.toolsPatterns.length > 0;
|
|
319
|
+
const patternResult = shouldTryPatternMatch ? await this.processPattern(plainData) : undefined;
|
|
320
|
+
|
|
321
|
+
if (patternResult) {
|
|
322
|
+
await this.insertPatternMatch(patternResult);
|
|
323
|
+
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.insertBlokData(parsedBlokData);
|
|
328
|
+
|
|
329
|
+
return true;
|
|
330
|
+
} catch (_e) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Inserts a matched pattern as a new block
|
|
337
|
+
* @param patternResult - the matched pattern with tool and event
|
|
338
|
+
*/
|
|
339
|
+
private async insertPatternMatch(patternResult: { event: PasteEvent; tool: string }): Promise<void> {
|
|
340
|
+
const { BlockManager, Caret } = this.Blok;
|
|
341
|
+
const needToReplaceCurrentBlock = BlockManager.currentBlock &&
|
|
342
|
+
BlockManager.currentBlock.tool.isDefault &&
|
|
343
|
+
BlockManager.currentBlock.isEmpty;
|
|
344
|
+
|
|
345
|
+
const insertedBlock = await BlockManager.paste(patternResult.tool, patternResult.event, needToReplaceCurrentBlock);
|
|
346
|
+
|
|
347
|
+
Caret.setToBlock(insertedBlock, Caret.positions.END);
|
|
348
|
+
}
|
|
349
|
+
|
|
310
350
|
/**
|
|
311
351
|
* Process pasted text and divide them into Blocks
|
|
312
352
|
* @param {string} data - text to process. Can be HTML or plain.
|
|
@@ -52,7 +52,7 @@ export class Renderer extends Module {
|
|
|
52
52
|
* Create Blocks instances
|
|
53
53
|
*/
|
|
54
54
|
const blocks = processedBlocks.map((blockData: OutputBlockData) => {
|
|
55
|
-
const { tunes, id, parent, content } = blockData;
|
|
55
|
+
const { tunes, id, parent, content, slot } = blockData;
|
|
56
56
|
const originalTool = blockData.type;
|
|
57
57
|
const availabilityResult = (() => {
|
|
58
58
|
if (Tools.available.has(originalTool)) {
|
|
@@ -79,6 +79,7 @@ export class Renderer extends Module {
|
|
|
79
79
|
tunes,
|
|
80
80
|
parentId: parent,
|
|
81
81
|
contentIds: content,
|
|
82
|
+
slot,
|
|
82
83
|
});
|
|
83
84
|
} catch (error) {
|
|
84
85
|
log(`Block «${tool}» skipped because of plugins error`, 'error', {
|
|
@@ -98,6 +99,7 @@ export class Renderer extends Module {
|
|
|
98
99
|
tunes,
|
|
99
100
|
parentId: parent,
|
|
100
101
|
contentIds: content,
|
|
102
|
+
slot,
|
|
101
103
|
});
|
|
102
104
|
}
|
|
103
105
|
};
|
package/types/api/blocks.d.ts
CHANGED
|
@@ -71,6 +71,21 @@ export interface Blocks {
|
|
|
71
71
|
*/
|
|
72
72
|
getBlockByElement(element: HTMLElement): BlockAPI | undefined;
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Returns all child blocks of a parent container block
|
|
76
|
+
*
|
|
77
|
+
* @param parentId - id of the parent block
|
|
78
|
+
*/
|
|
79
|
+
getChildren(parentId: string): BlockAPI[];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Returns child blocks of a parent container block in a specific slot
|
|
83
|
+
*
|
|
84
|
+
* @param parentId - id of the parent block
|
|
85
|
+
* @param slot - slot index (e.g., column index for columns block)
|
|
86
|
+
*/
|
|
87
|
+
getChildrenInSlot(parentId: string, slot: number): BlockAPI[];
|
|
88
|
+
|
|
74
89
|
/**
|
|
75
90
|
* Returns Blocks count
|
|
76
91
|
* @return {number}
|