@aionbuilders/nabu 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -0
- package/dist/behaviors/index.d.ts +1 -0
- package/dist/behaviors/index.js +1 -0
- package/dist/behaviors/text/RichText.svelte +33 -0
- package/dist/behaviors/text/RichText.svelte.d.ts +11 -0
- package/dist/behaviors/text/index.d.ts +3 -0
- package/dist/behaviors/text/index.js +3 -0
- package/dist/behaviors/text/rich-text.extension.d.ts +2 -0
- package/dist/behaviors/text/rich-text.extension.js +75 -0
- package/dist/behaviors/text/text.behavior.svelte.d.ts +103 -0
- package/dist/behaviors/text/text.behavior.svelte.js +346 -0
- package/dist/blocks/Block.svelte +18 -0
- package/dist/blocks/Block.svelte.d.ts +11 -0
- package/dist/blocks/Nabu.svelte +31 -0
- package/dist/blocks/Nabu.svelte.d.ts +12 -0
- package/dist/blocks/block.svelte.d.ts +143 -0
- package/dist/blocks/block.svelte.js +364 -0
- package/dist/blocks/container.utils.d.ts +28 -0
- package/dist/blocks/container.utils.js +114 -0
- package/dist/blocks/heading/Heading.svelte +42 -0
- package/dist/blocks/heading/Heading.svelte.d.ts +11 -0
- package/dist/blocks/heading/heading.svelte.d.ts +45 -0
- package/dist/blocks/heading/heading.svelte.js +94 -0
- package/dist/blocks/heading/hooks/onBeforeInput.hook.d.ts +3 -0
- package/dist/blocks/heading/hooks/onBeforeInput.hook.js +58 -0
- package/dist/blocks/heading/index.d.ts +7 -0
- package/dist/blocks/heading/index.js +41 -0
- package/dist/blocks/index.d.ts +10 -0
- package/dist/blocks/index.js +12 -0
- package/dist/blocks/list/List.svelte +25 -0
- package/dist/blocks/list/List.svelte.d.ts +11 -0
- package/dist/blocks/list/ListItem.svelte +45 -0
- package/dist/blocks/list/ListItem.svelte.d.ts +11 -0
- package/dist/blocks/list/index.d.ts +10 -0
- package/dist/blocks/list/index.js +41 -0
- package/dist/blocks/list/list-item.svelte.d.ts +50 -0
- package/dist/blocks/list/list-item.svelte.js +213 -0
- package/dist/blocks/list/list.behavior.svelte.d.ts +23 -0
- package/dist/blocks/list/list.behavior.svelte.js +61 -0
- package/dist/blocks/list/list.svelte.d.ts +39 -0
- package/dist/blocks/list/list.svelte.js +139 -0
- package/dist/blocks/megablock.svelte.d.ts +13 -0
- package/dist/blocks/megablock.svelte.js +64 -0
- package/dist/blocks/nabu.svelte.d.ts +121 -0
- package/dist/blocks/nabu.svelte.js +395 -0
- package/dist/blocks/paragraph/Paragraph.svelte +38 -0
- package/dist/blocks/paragraph/Paragraph.svelte.d.ts +11 -0
- package/dist/blocks/paragraph/index.d.ts +7 -0
- package/dist/blocks/paragraph/index.js +44 -0
- package/dist/blocks/paragraph/paragraph.svelte.d.ts +41 -0
- package/dist/blocks/paragraph/paragraph.svelte.js +86 -0
- package/dist/blocks/selection.svelte.d.ts +38 -0
- package/dist/blocks/selection.svelte.js +143 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/utils/extensions.d.ts +69 -0
- package/dist/utils/extensions.js +43 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/selection.svelte.d.ts +219 -0
- package/dist/utils/selection.svelte.js +611 -0
- package/package.json +74 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Nabu, NabuNode } from "./nabu.svelte";
|
|
3
|
+
* @import { MegaBlock } from "./megablock.svelte";
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { tick } from "svelte";
|
|
7
|
+
import { SvelteMap, SvelteSet } from "svelte/reactivity";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export class Block {
|
|
11
|
+
/** @param {Nabu} nabu @param {NabuNode} node */
|
|
12
|
+
constructor(nabu, node) {
|
|
13
|
+
this.nabu = nabu;
|
|
14
|
+
this.node = node;
|
|
15
|
+
const metadata = node.data;
|
|
16
|
+
this.id = node.id.toString()
|
|
17
|
+
this.type = metadata.get("type") || "block";
|
|
18
|
+
this.nabu.blocks.set(this.id, this);
|
|
19
|
+
const blocksOfType = this.nabu.blocksByType.get(this.type) || new SvelteSet();
|
|
20
|
+
blocksOfType.add(this);
|
|
21
|
+
this.nabu.blocksByType.set(this.type, blocksOfType);
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const parent = node.parent();
|
|
26
|
+
if (parent) {
|
|
27
|
+
this.parent = this.nabu.blocks.get(parent.id.toString()) || null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
serializers = new SvelteMap();
|
|
32
|
+
behaviors = new SvelteMap();
|
|
33
|
+
selected = $state(false);
|
|
34
|
+
isSelectionStart = $state(false);
|
|
35
|
+
isSelectionEnd = $state(false);
|
|
36
|
+
isIntermediate = $derived(this.selected && !this.isSelectionStart && !this.isSelectionEnd);
|
|
37
|
+
|
|
38
|
+
clearSelection() {
|
|
39
|
+
this.selected = false;
|
|
40
|
+
this.isSelectionStart = false;
|
|
41
|
+
this.isSelectionEnd = false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @type {{from: number, to: number, direction: "forward" | "backward" | "none"} | null} */
|
|
45
|
+
selection = $derived(null);
|
|
46
|
+
|
|
47
|
+
/** @type {MegaBlock | null} */
|
|
48
|
+
parent = $state(null);
|
|
49
|
+
index = $state(0);
|
|
50
|
+
/** @type {Block?} */
|
|
51
|
+
previous = $derived(this.index > 0 && this.parent ? this.parent.children[this.index - 1] : null);
|
|
52
|
+
/** @type {Block?} */
|
|
53
|
+
next = $derived(this.parent && this.index < this.parent.children.length - 1 ? this.parent.children[this.index + 1] : null);
|
|
54
|
+
|
|
55
|
+
parents = $derived.by(() => {
|
|
56
|
+
const parents = [];
|
|
57
|
+
let current = this.parent;
|
|
58
|
+
while (current) {
|
|
59
|
+
parents.push(current);
|
|
60
|
+
current = current.parent;
|
|
61
|
+
}
|
|
62
|
+
return parents;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
component = $derived(this.nabu.components.get(this.type) || null);
|
|
66
|
+
|
|
67
|
+
/** @type {HTMLElement | null} */
|
|
68
|
+
element = $state(null);
|
|
69
|
+
|
|
70
|
+
/** @param {(block: Block) => boolean} predicate @returns {Block | null} */
|
|
71
|
+
findForward(predicate) {
|
|
72
|
+
// 1. Chercher dans les enfants (descendre)
|
|
73
|
+
// @ts-ignore
|
|
74
|
+
if (this.children?.length) {
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
for (const child of this.children) {
|
|
77
|
+
if (predicate(child)) return child;
|
|
78
|
+
const found = child.findForward(predicate);
|
|
79
|
+
if (found) return found;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Chercher dans les frères suivants et remonter
|
|
84
|
+
let current = this;
|
|
85
|
+
while (current) {
|
|
86
|
+
const parent = current.parent || current.nabu;
|
|
87
|
+
const i = parent.children.indexOf(current);
|
|
88
|
+
if (i !== -1) {
|
|
89
|
+
for (let j = i + 1; j < parent.children.length; j++) {
|
|
90
|
+
const sibling = parent.children[j];
|
|
91
|
+
if (predicate(sibling)) return sibling;
|
|
92
|
+
const found = sibling.findForward(predicate);
|
|
93
|
+
if (found) return found;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
current = current.parent;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @param {(block: Block) => boolean} predicate @returns {Block | null} */
|
|
102
|
+
findBackward(predicate) {
|
|
103
|
+
let current = this;
|
|
104
|
+
while (current) {
|
|
105
|
+
const parent = current.parent || current.nabu;
|
|
106
|
+
const i = parent.children.indexOf(current);
|
|
107
|
+
|
|
108
|
+
if (i !== -1) {
|
|
109
|
+
// On parcourt les frères précédents de bas en haut
|
|
110
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
111
|
+
const sibling = parent.children[j];
|
|
112
|
+
|
|
113
|
+
// Si le frère a des enfants, le "précédent" est le DERNIER de ses descendants
|
|
114
|
+
// @ts-ignore
|
|
115
|
+
if (sibling.children?.length) {
|
|
116
|
+
// @ts-ignore
|
|
117
|
+
const lastDescendant = sibling.findLastDescendant(predicate);
|
|
118
|
+
if (lastDescendant) return lastDescendant;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (predicate(sibling)) return sibling;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Si aucun frère précédent ne match, on teste le parent lui-même
|
|
126
|
+
if (current.parent) {
|
|
127
|
+
if (predicate(current.parent)) return current.parent;
|
|
128
|
+
current = current.parent;
|
|
129
|
+
} else {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns real, uncommitted siblings, which is useful for extensions that want to check the document structure before the transaction is committed.
|
|
138
|
+
*/
|
|
139
|
+
getAdjacentSiblings() {
|
|
140
|
+
const parent = this.node.parent();
|
|
141
|
+
const siblings = parent ? parent.children() : this.nabu.tree.roots();
|
|
142
|
+
if (!siblings) return { previous: null, next: null };
|
|
143
|
+
|
|
144
|
+
const index = this.node.index();
|
|
145
|
+
if (index === null || index === undefined) return { previous: null, next: null };
|
|
146
|
+
|
|
147
|
+
const previousNode = index > 0 ? siblings[index - 1] : null;
|
|
148
|
+
const nextNode = index < siblings.length - 1 ? siblings[index + 1] : null;
|
|
149
|
+
const previous = previousNode ? this.nabu.blocks.get(previousNode.id.toString()) : null;
|
|
150
|
+
const next = nextNode ? this.nabu.blocks.get(nextNode.id.toString()) : null;
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
previous,
|
|
154
|
+
next,
|
|
155
|
+
previousNode,
|
|
156
|
+
nextNode
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Helper pour trouver le dernier descendant profond qui matche
|
|
162
|
+
* @param {(block: Block) => boolean} predicate
|
|
163
|
+
* @returns {Block | null}
|
|
164
|
+
*/
|
|
165
|
+
findLastDescendant(predicate) {
|
|
166
|
+
// @ts-ignore
|
|
167
|
+
const children = this.children || [];
|
|
168
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
169
|
+
const child = children[i];
|
|
170
|
+
const found = child.findLastDescendant(predicate);
|
|
171
|
+
if (found) return found;
|
|
172
|
+
if (predicate(child)) return child;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
destroy() {
|
|
178
|
+
this.nabu.delete(this);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Transforme ce bloc en un autre type de bloc.
|
|
183
|
+
* @param {string} newType
|
|
184
|
+
* @param {Object} [props={}]
|
|
185
|
+
*/
|
|
186
|
+
transformTo(newType, props = {}) {
|
|
187
|
+
const data = this.node.data;
|
|
188
|
+
data.set("type", newType);
|
|
189
|
+
for (const [key, value] of Object.entries(props)) {
|
|
190
|
+
data.set(key, value);
|
|
191
|
+
}
|
|
192
|
+
this.commit();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @param {number} index @param {string} text */
|
|
196
|
+
insert(index, text) {
|
|
197
|
+
console.warn("Not implemented: insert text", text, "at index", index, "in block", this.id);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** @param {{from?: number, to?: number, index?: number, length?: number}} [deletion] */
|
|
201
|
+
delete(deletion) {
|
|
202
|
+
console.warn("Not implemented: delete block", this.id, "with deletion range", deletion);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @param {Block} block @returns {any} */
|
|
206
|
+
absorbs(block) {
|
|
207
|
+
console.warn("Not implemented: check if block", this.id, "absorbs block", block.id);
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
/** @param {Block} block @returns {any} */
|
|
213
|
+
mergeWith(block) {
|
|
214
|
+
const success = this.absorbs(block);
|
|
215
|
+
if (success) {
|
|
216
|
+
block.destroy();
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Consumes another block, handling children relocation intelligently.
|
|
224
|
+
* @param {Block} otherBlock - The block to consume or be consumed by
|
|
225
|
+
* @param {'into' | 'from'} direction - 'into' = this merges into other, 'from' = other merges into this
|
|
226
|
+
* @returns {Block} The surviving block
|
|
227
|
+
*/
|
|
228
|
+
consume(otherBlock, direction = 'from') {
|
|
229
|
+
const [survivor, victim] = direction === 'into'
|
|
230
|
+
? [otherBlock, this]
|
|
231
|
+
: [this, otherBlock];
|
|
232
|
+
|
|
233
|
+
const absorbed = survivor.absorbs(victim);
|
|
234
|
+
|
|
235
|
+
// Handle children relocation if victim has any
|
|
236
|
+
// @ts-ignore - MegaBlock has children
|
|
237
|
+
if (victim.children?.length) {
|
|
238
|
+
if (absorbed && survivor.adoptChildren) {
|
|
239
|
+
// MegaBlock survivor adopts children
|
|
240
|
+
// @ts-ignore
|
|
241
|
+
survivor.adoptChildren(victim.children);
|
|
242
|
+
} else {
|
|
243
|
+
// Non-MegaBlock survivor: promote children as siblings
|
|
244
|
+
// @ts-ignore
|
|
245
|
+
victim.children.forEach(child => {
|
|
246
|
+
child.node.moveAfter(survivor.node);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Handle the victim block itself if not absorbed
|
|
252
|
+
if (!absorbed) {
|
|
253
|
+
victim.node.moveAfter(survivor.node);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return survivor;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** @param {Block[]} children @param {number | null} [index] */
|
|
260
|
+
adoptChildren(children, index = null) {
|
|
261
|
+
console.warn("Not implemented: adopt children", children.map(c => c.id), "into block", this.id, "at index", index);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** @param {{from?: number, to?: number, index?: number, length?: number, offset?: number}} options @returns {{block: Block} | null} */
|
|
265
|
+
split(options) {
|
|
266
|
+
console.warn("Not implemented: split block", this.id, "with options", options);
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** @param {{start?: number, end?: number, offset?: number} | null } options @param {boolean} [passive=false] */
|
|
271
|
+
focus(options = {}, passive = false) {
|
|
272
|
+
let start = options?.start ?? options?.offset ?? this.selection?.from ?? 0;
|
|
273
|
+
let end = options?.end ?? options?.offset ?? this.selection?.to ?? 0;
|
|
274
|
+
const startPoint = this.getDOMPoint(start);
|
|
275
|
+
const endPoint = this.getDOMPoint(end);
|
|
276
|
+
if (!passive && startPoint && endPoint) {
|
|
277
|
+
tick().then(() => {
|
|
278
|
+
this.nabu.selection.setBaseAndExtent(startPoint.node, startPoint.offset || 0, endPoint.node || null, endPoint.offset || 0)
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {start: startPoint, end: endPoint, options: {startOffset: start, endOffset: end}};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @param {number} offset
|
|
287
|
+
* @returns {{node: Node, offset: number} | null}
|
|
288
|
+
*/
|
|
289
|
+
getDOMPoint(offset) {
|
|
290
|
+
console.warn("getDOMPoint not implemented for block", this.type, this.id);
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// -- Event Handling --
|
|
295
|
+
|
|
296
|
+
/** @param {string} eventName @param {Event} event @param {Object} [data={}] */
|
|
297
|
+
ascend(eventName, event, data = {}) {
|
|
298
|
+
//@ts-ignore
|
|
299
|
+
if (this.parent && typeof this.parent[eventName] === "function") {
|
|
300
|
+
//@ts-ignore
|
|
301
|
+
return this.parent[eventName](event, { ...data, from: this });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 2. On essaye les hooks d'extensions enregistrés dans Nabu
|
|
305
|
+
const hooks = this.nabu.hooks.get(eventName);
|
|
306
|
+
if (hooks) {
|
|
307
|
+
for (const hook of hooks) {
|
|
308
|
+
|
|
309
|
+
// Si un hook retourne 'true', on considère l'événement géré
|
|
310
|
+
const result = hook(this.nabu, this, event, data);
|
|
311
|
+
if (result) return result;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** @param {InputEvent} event @returns {any} */
|
|
319
|
+
beforeinput(event) {
|
|
320
|
+
// On peut intercepter les événements d'input ici pour faire des choses comme :
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** @param {KeyboardEvent} event @returns {any} */
|
|
324
|
+
keydown(event) {
|
|
325
|
+
// Intercepter les touches spéciales (Tab, Enter, flèches)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// -- UTILS --
|
|
329
|
+
|
|
330
|
+
commit() {
|
|
331
|
+
this.nabu.commit();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
/** @param {string} format */
|
|
336
|
+
serialize(format) {
|
|
337
|
+
const serializer = this.serializers.get(format);
|
|
338
|
+
if (serializer) {
|
|
339
|
+
return serializer(this);
|
|
340
|
+
} else {
|
|
341
|
+
console.warn(`No serializer found for format "${format}" on block type "${this.type}"`);
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
/** @param {Nabu} nabu @param {NabuNode} node */
|
|
348
|
+
static load(nabu, node) {
|
|
349
|
+
const metadata = node.data;
|
|
350
|
+
const type = metadata.get("type") || "block";
|
|
351
|
+
const BlockClass = nabu.registry.get(type) || Block;
|
|
352
|
+
const block = new BlockClass(nabu, node);
|
|
353
|
+
return block;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** @param {Nabu} nabu @param {string} type @param {Object} [props={}] @param {string|null} [parentId=null] @param {number|null} [index=null] */
|
|
357
|
+
static create(nabu, type, props = {}, parentId = null, index = null) {
|
|
358
|
+
const node = nabu.tree.createNode(parentId || undefined, index || undefined);
|
|
359
|
+
node.data.set("type", type);
|
|
360
|
+
const block = Block.load(nabu, node);
|
|
361
|
+
return block;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Block } from './block.svelte';
|
|
3
|
+
* @import { Nabu } from './nabu.svelte';
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Shared beforeinput logic for block containers (Nabu root and MegaBlock).
|
|
7
|
+
*
|
|
8
|
+
* Uses a "spine" approach: for each selection boundary, we walk up the parent
|
|
9
|
+
* chain until we hit the container, collecting intermediate blocks to clean up
|
|
10
|
+
* along the way. This correctly handles arbitrarily nested structures.
|
|
11
|
+
*
|
|
12
|
+
* Spine termination differs between the two callers:
|
|
13
|
+
* - MegaBlock: the container IS pushed into the spine (loop exits on === container),
|
|
14
|
+
* so the direct child of container is at spine.at(-2).
|
|
15
|
+
* - Nabu: root blocks have parent = null, so the loop breaks before Nabu is ever
|
|
16
|
+
* pushed, and the direct child of container is at spine.at(-1).
|
|
17
|
+
* Both cases are unified by the directChild() helper below.
|
|
18
|
+
*
|
|
19
|
+
* @param {{ children: Block[], commit: () => void }} container
|
|
20
|
+
* @param {Nabu} nabu
|
|
21
|
+
* @param {InputEvent} event
|
|
22
|
+
*/
|
|
23
|
+
export function handleContainerBeforeInput(container: {
|
|
24
|
+
children: Block[];
|
|
25
|
+
commit: () => void;
|
|
26
|
+
}, nabu: Nabu, event: InputEvent): any;
|
|
27
|
+
import type { Block } from './block.svelte';
|
|
28
|
+
import type { Nabu } from './nabu.svelte';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Block } from './block.svelte';
|
|
3
|
+
* @import { Nabu } from './nabu.svelte';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared beforeinput logic for block containers (Nabu root and MegaBlock).
|
|
8
|
+
*
|
|
9
|
+
* Uses a "spine" approach: for each selection boundary, we walk up the parent
|
|
10
|
+
* chain until we hit the container, collecting intermediate blocks to clean up
|
|
11
|
+
* along the way. This correctly handles arbitrarily nested structures.
|
|
12
|
+
*
|
|
13
|
+
* Spine termination differs between the two callers:
|
|
14
|
+
* - MegaBlock: the container IS pushed into the spine (loop exits on === container),
|
|
15
|
+
* so the direct child of container is at spine.at(-2).
|
|
16
|
+
* - Nabu: root blocks have parent = null, so the loop breaks before Nabu is ever
|
|
17
|
+
* pushed, and the direct child of container is at spine.at(-1).
|
|
18
|
+
* Both cases are unified by the directChild() helper below.
|
|
19
|
+
*
|
|
20
|
+
* @param {{ children: Block[], commit: () => void }} container
|
|
21
|
+
* @param {Nabu} nabu
|
|
22
|
+
* @param {InputEvent} event
|
|
23
|
+
*/
|
|
24
|
+
export function handleContainerBeforeInput(container, nabu, event) {
|
|
25
|
+
const startBlock = /** @type {Block} */ (nabu.selection.startBlock);
|
|
26
|
+
const endBlock = /** @type {Block} */ (nabu.selection.endBlock);
|
|
27
|
+
if (!startBlock || !endBlock) return false;
|
|
28
|
+
|
|
29
|
+
const focusData = startBlock.focus(undefined, true);
|
|
30
|
+
const inputType = event.inputType;
|
|
31
|
+
|
|
32
|
+
// Build start spine: walk from startBlock up to (but not past) container,
|
|
33
|
+
// pruning next-siblings of each intermediate node along the way.
|
|
34
|
+
const startSpine = [startBlock];
|
|
35
|
+
while (startSpine.at(-1) && startSpine.at(-1) !== container) {
|
|
36
|
+
const current = startSpine.at(-1);
|
|
37
|
+
const parent = current?.parent;
|
|
38
|
+
if (!current || !parent) break;
|
|
39
|
+
startSpine.push(parent);
|
|
40
|
+
if (parent !== container) {
|
|
41
|
+
const nextSiblings = parent.children.slice(current.index + 1);
|
|
42
|
+
if (nextSiblings.length) nextSiblings.forEach(block => block.destroy());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Build end spine: walk from endBlock up to (but not past) container,
|
|
47
|
+
// pruning prev-siblings of each intermediate node along the way.
|
|
48
|
+
const endSpine = [endBlock];
|
|
49
|
+
while (endSpine.at(-1) && endSpine.at(-1) !== container) {
|
|
50
|
+
const current = endSpine.at(-1);
|
|
51
|
+
const parent = current?.parent;
|
|
52
|
+
if (!current || !parent) break;
|
|
53
|
+
endSpine.push(parent);
|
|
54
|
+
if (parent !== container) {
|
|
55
|
+
const previousSiblings = parent.children.slice(0, current.index);
|
|
56
|
+
if (previousSiblings.length) previousSiblings.forEach(block => block.destroy());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Resolve the direct children of container from each spine.
|
|
61
|
+
const directChild = (spine) => spine.at(-1) === container ? spine.at(-2) : spine.at(-1);
|
|
62
|
+
const startOfStartSpine = directChild(startSpine);
|
|
63
|
+
const startOfEndSpine = directChild(endSpine);
|
|
64
|
+
if (!startOfStartSpine || !startOfEndSpine) return false;
|
|
65
|
+
|
|
66
|
+
// Delete intermediate direct children of container between the two spines.
|
|
67
|
+
const intermediates = container.children.slice(startOfStartSpine.index + 1, startOfEndSpine.index);
|
|
68
|
+
intermediates.forEach(block => block.destroy());
|
|
69
|
+
|
|
70
|
+
// Single-block selection: delegate directly to that block.
|
|
71
|
+
if (startBlock === endBlock) {
|
|
72
|
+
return startBlock.beforeinput(event);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Delete the selected portions of both boundary blocks.
|
|
76
|
+
startBlock.delete();
|
|
77
|
+
endBlock.delete();
|
|
78
|
+
|
|
79
|
+
let focusBlock = startBlock;
|
|
80
|
+
|
|
81
|
+
if (inputType === 'deleteContentBackward' || inputType === 'deleteContentForward') {
|
|
82
|
+
// Text deletion already handled above.
|
|
83
|
+
} else if (inputType === 'insertParagraph') {
|
|
84
|
+
const { block: newBlock } = startBlock.split({ offset: focusData.options.startOffset }) || {};
|
|
85
|
+
if (newBlock) focusBlock = newBlock;
|
|
86
|
+
} else if (inputType === 'insertText' || inputType === 'insertLineBreak') {
|
|
87
|
+
const text = event.data || (inputType === 'insertLineBreak' ? '\n' : '');
|
|
88
|
+
startBlock.insert(focusData.options.startOffset, text);
|
|
89
|
+
} else {
|
|
90
|
+
console.warn('Unhandled input type in container:', inputType);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Merge the end block into focusBlock and relocate orphaned siblings.
|
|
95
|
+
focusBlock.consume(endBlock);
|
|
96
|
+
|
|
97
|
+
endSpine.forEach(block => {
|
|
98
|
+
if (block === container) return;
|
|
99
|
+
const brotherhood = block.parent?.children || [];
|
|
100
|
+
const index = brotherhood.indexOf(block);
|
|
101
|
+
if (index !== -1) {
|
|
102
|
+
const nextSiblings = brotherhood.slice(index + 1);
|
|
103
|
+
if (nextSiblings.length) nextSiblings.forEach(sibling => sibling.node.moveAfter(focusBlock.node));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
startOfEndSpine?.destroy();
|
|
108
|
+
|
|
109
|
+
container.commit();
|
|
110
|
+
startBlock.focus({
|
|
111
|
+
offset: focusData.options.startOffset + (inputType === 'insertText' ? (event.data?.length || 0) : 0)
|
|
112
|
+
});
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import RichText from '../../behaviors/text/RichText.svelte';
|
|
3
|
+
/** @type {{block: import('./heading.svelte.js').Heading}}*/
|
|
4
|
+
let {block} = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<svelte:element
|
|
8
|
+
this={"h" + block.level}
|
|
9
|
+
bind:this={block.element}
|
|
10
|
+
data-block-id={block.id}
|
|
11
|
+
data-block-type="heading"
|
|
12
|
+
class="nabu-heading"
|
|
13
|
+
class:selected={block.selected}
|
|
14
|
+
class:first={block.isSelectionStart}
|
|
15
|
+
class:last={block.isSelectionEnd}
|
|
16
|
+
><RichText delta={block.delta} /></svelte:element>
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
.nabu-heading {
|
|
20
|
+
margin-top: 1.5rem;
|
|
21
|
+
margin-bottom: 0.5rem;
|
|
22
|
+
white-space: pre-wrap;
|
|
23
|
+
outline: none;
|
|
24
|
+
|
|
25
|
+
&.selected {
|
|
26
|
+
background-color: rgba(59, 130, 246, 0.25);
|
|
27
|
+
}
|
|
28
|
+
&.first {
|
|
29
|
+
border-top: 1px solid rgba(59, 130, 246, 0.5);
|
|
30
|
+
}
|
|
31
|
+
&.last {
|
|
32
|
+
border-bottom: 1px solid rgba(59, 130, 246, 0.5);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
h1 { font-size: 2.25rem; font-weight: 800; }
|
|
37
|
+
h2 { font-size: 1.875rem; font-weight: 700; }
|
|
38
|
+
h3 { font-size: 1.5rem; font-weight: 700; }
|
|
39
|
+
h4 { font-size: 1.25rem; font-weight: 600; }
|
|
40
|
+
h5 { font-size: 1.125rem; font-weight: 600; }
|
|
41
|
+
h6 { font-size: 1rem; font-weight: 600; }
|
|
42
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default Heading;
|
|
2
|
+
type Heading = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const Heading: import("svelte").Component<{
|
|
7
|
+
block: import("./heading.svelte.js").Heading;
|
|
8
|
+
}, {}, "">;
|
|
9
|
+
type $$ComponentProps = {
|
|
10
|
+
block: import("./heading.svelte.js").Heading;
|
|
11
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Nabu, NabuNode } from "../nabu.svelte";
|
|
3
|
+
* @import { TextNode } from "../../behaviors/text";
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {NabuNode<{type: "heading", text: LoroText, level: number}>} HeadingNode
|
|
7
|
+
*/
|
|
8
|
+
export class Heading extends Block {
|
|
9
|
+
/** @param {Nabu} nabu @param {string} type @param {Object} [props={}] @param {string|null} [parentId=null] @param {number|null} [index=null] */
|
|
10
|
+
static create(nabu: Nabu, type: string, props?: Object, parentId?: string | null, index?: number | null): Heading;
|
|
11
|
+
/** @param {Nabu} nabu @param {HeadingNode} node */
|
|
12
|
+
constructor(nabu: Nabu, node: HeadingNode);
|
|
13
|
+
container: LoroText;
|
|
14
|
+
/** @type {TextBehavior} */
|
|
15
|
+
behavior: TextBehavior;
|
|
16
|
+
/** @type {number} */
|
|
17
|
+
level: number;
|
|
18
|
+
component: import("svelte").Component<any, any, string> | import("svelte").Component<{
|
|
19
|
+
block: import("./heading.svelte").Heading;
|
|
20
|
+
}, {}, "">;
|
|
21
|
+
get text(): string;
|
|
22
|
+
get delta(): import("loro-crdt").Delta<string>[];
|
|
23
|
+
selection: {
|
|
24
|
+
from: number;
|
|
25
|
+
to: number;
|
|
26
|
+
isCollapsed: boolean;
|
|
27
|
+
direction: "forward" | "backward" | "none";
|
|
28
|
+
} | null;
|
|
29
|
+
/** @param {Block} block */
|
|
30
|
+
absorbs(block: Block): boolean;
|
|
31
|
+
/** @param {import('loro-crdt').Delta<string>[]} data */
|
|
32
|
+
applyDelta(data?: import("loro-crdt").Delta<string>[]): void;
|
|
33
|
+
/** @param {Parameters<Block["split"]>[0]} [options] @returns {ReturnType<Block["split"]>} */
|
|
34
|
+
split(options?: Parameters<Block["split"]>[0]): ReturnType<Block["split"]>;
|
|
35
|
+
}
|
|
36
|
+
export type HeadingNode = NabuNode<{
|
|
37
|
+
type: "heading";
|
|
38
|
+
text: LoroText;
|
|
39
|
+
level: number;
|
|
40
|
+
}>;
|
|
41
|
+
import { Block } from "../block.svelte";
|
|
42
|
+
import { LoroText } from "loro-crdt";
|
|
43
|
+
import { TextBehavior } from "../../behaviors/text";
|
|
44
|
+
import type { Nabu } from "../nabu.svelte";
|
|
45
|
+
import type { NabuNode } from "../nabu.svelte";
|