@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.
Files changed (62) hide show
  1. package/README.md +131 -0
  2. package/dist/behaviors/index.d.ts +1 -0
  3. package/dist/behaviors/index.js +1 -0
  4. package/dist/behaviors/text/RichText.svelte +33 -0
  5. package/dist/behaviors/text/RichText.svelte.d.ts +11 -0
  6. package/dist/behaviors/text/index.d.ts +3 -0
  7. package/dist/behaviors/text/index.js +3 -0
  8. package/dist/behaviors/text/rich-text.extension.d.ts +2 -0
  9. package/dist/behaviors/text/rich-text.extension.js +75 -0
  10. package/dist/behaviors/text/text.behavior.svelte.d.ts +103 -0
  11. package/dist/behaviors/text/text.behavior.svelte.js +346 -0
  12. package/dist/blocks/Block.svelte +18 -0
  13. package/dist/blocks/Block.svelte.d.ts +11 -0
  14. package/dist/blocks/Nabu.svelte +31 -0
  15. package/dist/blocks/Nabu.svelte.d.ts +12 -0
  16. package/dist/blocks/block.svelte.d.ts +143 -0
  17. package/dist/blocks/block.svelte.js +364 -0
  18. package/dist/blocks/container.utils.d.ts +28 -0
  19. package/dist/blocks/container.utils.js +114 -0
  20. package/dist/blocks/heading/Heading.svelte +42 -0
  21. package/dist/blocks/heading/Heading.svelte.d.ts +11 -0
  22. package/dist/blocks/heading/heading.svelte.d.ts +45 -0
  23. package/dist/blocks/heading/heading.svelte.js +94 -0
  24. package/dist/blocks/heading/hooks/onBeforeInput.hook.d.ts +3 -0
  25. package/dist/blocks/heading/hooks/onBeforeInput.hook.js +58 -0
  26. package/dist/blocks/heading/index.d.ts +7 -0
  27. package/dist/blocks/heading/index.js +41 -0
  28. package/dist/blocks/index.d.ts +10 -0
  29. package/dist/blocks/index.js +12 -0
  30. package/dist/blocks/list/List.svelte +25 -0
  31. package/dist/blocks/list/List.svelte.d.ts +11 -0
  32. package/dist/blocks/list/ListItem.svelte +45 -0
  33. package/dist/blocks/list/ListItem.svelte.d.ts +11 -0
  34. package/dist/blocks/list/index.d.ts +10 -0
  35. package/dist/blocks/list/index.js +41 -0
  36. package/dist/blocks/list/list-item.svelte.d.ts +50 -0
  37. package/dist/blocks/list/list-item.svelte.js +213 -0
  38. package/dist/blocks/list/list.behavior.svelte.d.ts +23 -0
  39. package/dist/blocks/list/list.behavior.svelte.js +61 -0
  40. package/dist/blocks/list/list.svelte.d.ts +39 -0
  41. package/dist/blocks/list/list.svelte.js +139 -0
  42. package/dist/blocks/megablock.svelte.d.ts +13 -0
  43. package/dist/blocks/megablock.svelte.js +64 -0
  44. package/dist/blocks/nabu.svelte.d.ts +121 -0
  45. package/dist/blocks/nabu.svelte.js +395 -0
  46. package/dist/blocks/paragraph/Paragraph.svelte +38 -0
  47. package/dist/blocks/paragraph/Paragraph.svelte.d.ts +11 -0
  48. package/dist/blocks/paragraph/index.d.ts +7 -0
  49. package/dist/blocks/paragraph/index.js +44 -0
  50. package/dist/blocks/paragraph/paragraph.svelte.d.ts +41 -0
  51. package/dist/blocks/paragraph/paragraph.svelte.js +86 -0
  52. package/dist/blocks/selection.svelte.d.ts +38 -0
  53. package/dist/blocks/selection.svelte.js +143 -0
  54. package/dist/index.d.ts +4 -0
  55. package/dist/index.js +5 -0
  56. package/dist/utils/extensions.d.ts +69 -0
  57. package/dist/utils/extensions.js +43 -0
  58. package/dist/utils/index.d.ts +2 -0
  59. package/dist/utils/index.js +2 -0
  60. package/dist/utils/selection.svelte.d.ts +219 -0
  61. package/dist/utils/selection.svelte.js +611 -0
  62. package/package.json +74 -0
@@ -0,0 +1,395 @@
1
+ import { LoroDoc, UndoManager } from 'loro-crdt';
2
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
3
+ import { Block } from './block.svelte';
4
+ import { NabuSelection } from './selection.svelte';
5
+ import { handleContainerBeforeInput } from './container.utils.js';
6
+ import { tick } from 'svelte';
7
+
8
+
9
+ /**
10
+ * @import {Component} from "svelte";
11
+ * @import { LoroTreeNode, LoroTree } from "loro-crdt";
12
+ * @import {Extension} from '../utils/extensions.js';
13
+ */
14
+
15
+ /**
16
+ * @template {Object<string, any>} [T={}]
17
+ * @typedef {LoroTreeNode<{type: string} & T>} NabuNode
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} NabuInit
22
+ * @property {Extension[]} [extensions]
23
+ * @property {Uint8Array<ArrayBufferLike>} [snapshot]
24
+ */
25
+
26
+ export class Nabu {
27
+ /** @param {NabuInit} init */
28
+ constructor(init = {}) {
29
+ this.doc = new LoroDoc();
30
+ if (init.snapshot) {
31
+ this.doc.import(init.snapshot);
32
+ }
33
+
34
+ this.selection = new NabuSelection(this);
35
+ this.tree = /** @type {LoroTree<Record<string, NabuNode>>} */ (this.doc.getTree("blocks"));
36
+ this.content = this.doc.getMap("content");
37
+
38
+ // Initialize Undo/Redo Manager
39
+ this.undoManager = new UndoManager(this.doc, {
40
+ maxUndoSteps: 100,
41
+ mergeInterval: 1000,
42
+ onPush: () => {
43
+ // Save cursor position for restoration on undo/redo
44
+ const sel = this.selection;
45
+ if (!sel || !sel.anchorBlock) return { value: null, cursors: [] };
46
+
47
+ return {
48
+ value: {
49
+ blockId: sel.anchorBlock.id,
50
+ offset: sel.startOffset
51
+ },
52
+ cursors: []
53
+ };
54
+ },
55
+ onPop: (_, storedValue) => {
56
+ // Restore cursor position after undo/redo
57
+ const value = /** @type {{blockId: string, offset: number} | null} */ (storedValue?.value);
58
+ if (value && typeof value === 'object' && 'blockId' in value) {
59
+ tick().then(() => {
60
+ const block = this.blocks.get(value.blockId);
61
+ if (block && block.behaviors?.has('text')) {
62
+ this.selection.setCursor(block, value.offset);
63
+ }
64
+ });
65
+ }
66
+ }
67
+ });
68
+
69
+ this.extensions = init.extensions || [];
70
+
71
+ if (this.extensions?.length) {
72
+ for (const ext of this.extensions) {
73
+ if (ext.block) this.registry.set(ext.name, ext.block);
74
+ if (ext.component) this.components.set(ext.name, ext.component);
75
+ if (ext.hooks) {
76
+ for (const [hookName, hookFn] of Object.entries(ext.hooks)) {
77
+ if (!this.hooks.has(hookName)) {
78
+ this.hooks.set(hookName, []);
79
+ }
80
+ this.hooks.get(hookName).push(hookFn);
81
+ }
82
+ }
83
+
84
+ if (ext.serializers) {
85
+ for (const [format, fn] of Object.entries(ext.serializers)) {
86
+ this.serializers.set(format, fn);
87
+ }
88
+ }
89
+
90
+ }
91
+ }
92
+
93
+ const roots = /** @type {NabuNode[]} */ (this.tree.roots());
94
+ if (roots?.length) {
95
+ for (const root of roots) {
96
+ const block = Block.load(this, root);
97
+ this.children.push(block);
98
+ }
99
+ }
100
+
101
+ this.tree.subscribe((event) => {
102
+ event.events.forEach(e => {
103
+ if (e.diff.type === "tree") {
104
+ e.diff.diff.forEach(action => {
105
+ if (action.action === 'create' || action.action === 'move' || action.action === 'delete') {
106
+ if (action.parent) {
107
+ const parentId = action.parent.toString();
108
+ const parentBlock = this.blocks.get(parentId);
109
+ if (parentBlock) parentBlock.updateChildren();
110
+ } else {
111
+ this.updateRoots();
112
+ }
113
+
114
+ if (action.oldParent) {
115
+ const oldParentId = action.oldParent.toString();
116
+ const oldParentBlock = this.blocks.get(oldParentId);
117
+ if (oldParentBlock) oldParentBlock.updateChildren();
118
+ }
119
+ }
120
+ })
121
+ }
122
+
123
+ if (e.diff.type === "map") {
124
+
125
+ const newType = e.diff.updated["type"];
126
+ if (newType !== undefined) {
127
+ const nodeId = e.path[1].toString();
128
+ const block = this.blocks.get(nodeId);
129
+ if (block && block.type !== newType) {
130
+ if (block.parent) block.parent.updateChildren();
131
+ else this.updateRoots();
132
+ }
133
+ }
134
+ }
135
+ })
136
+ });
137
+
138
+ this.init();
139
+ }
140
+
141
+ static BREAK = Symbol("BREAK");
142
+ static CONTINUE = Symbol("CONTINUE");
143
+ BREAK = Nabu.BREAK;
144
+ CONTINUE = Nabu.CONTINUE;
145
+
146
+ /** @type {LoroDoc} */
147
+ doc;
148
+ /** @type {SvelteMap<string, typeof Block>} */
149
+ registry = new SvelteMap();
150
+ /** @type {SvelteMap<string, Component>} */
151
+ components = new SvelteMap();
152
+ /** @type {SvelteMap<string, Block>} */
153
+ blocks = new SvelteMap();
154
+
155
+ /** @type {SvelteMap<string, SvelteSet<Block>>} */
156
+ blocksByType = new SvelteMap();
157
+
158
+ /** @type {SvelteMap<string, any>} */
159
+ systems = new SvelteMap();
160
+
161
+ hooks = new Map();
162
+
163
+ /**
164
+ * Root-level serializers. Each function receives the Nabu instance and returns the serialized document.
165
+ * @type {Map<string, (nabu: Nabu) => any>}
166
+ */
167
+ serializers = new Map([
168
+ ['markdown', (nabu) =>
169
+ nabu.children
170
+ .map(b => b.serialize('markdown'))
171
+ .filter(Boolean)
172
+ .join('\n\n')
173
+ ],
174
+ ['json', (nabu) => ({
175
+ version: '1',
176
+ blocks: nabu.children.map(b => b.serialize('json')).filter(Boolean)
177
+ })]
178
+ ]);
179
+
180
+ /** @type {Block[]} */
181
+ children = $state([]);
182
+
183
+ get isEmpty() {
184
+ return this.children.length === 0;
185
+ }
186
+
187
+ /** @param {string} format */
188
+ serialize(format) {
189
+ const fn = this.serializers.get(format);
190
+ if (!fn) {
191
+ console.warn(`No serializer registered for format "${format}" on Nabu`);
192
+ return null;
193
+ }
194
+ return fn(this);
195
+ }
196
+
197
+ init() {
198
+ this.hooks.get("onInit")?.forEach(hook => hook(this));
199
+ }
200
+
201
+ /** Rafraîchit les blocs racines de l'éditeur */
202
+ updateRoots() {
203
+ const roots = /** @type {NabuNode[]} */ (this.tree.roots());
204
+ this.children = roots.map((root, i) => {
205
+ const id = root.id.toString();
206
+ let block = this.blocks.get(id);
207
+ const currentType = root.data.get("type");
208
+
209
+ if (!block || block.type !== currentType) {
210
+ if (block) {
211
+ this.blocks.delete(id);
212
+ this.blocksByType.get(block.type)?.delete(block);
213
+ }
214
+
215
+ block = Block.load(this, root);
216
+ }
217
+ block.index = i;
218
+ block.parent = null;
219
+ return block;
220
+ });
221
+ }
222
+
223
+ trigger(hookName, ...args) {
224
+ const hooks = this.hooks.get(hookName) || [];
225
+ for (const hook of hooks) {
226
+ const result = hook(this, ...args);
227
+ if (result === this.BREAK) {
228
+ break;
229
+ }
230
+
231
+ }
232
+ }
233
+
234
+
235
+ commit() {
236
+ this.trigger("onBeforeTransaction", this);
237
+ this.doc.commit();
238
+ }
239
+
240
+ /**
241
+ * Undo the last operation
242
+ */
243
+ undo() {
244
+ if (this.undoManager.canUndo()) {
245
+ this.undoManager.undo();
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Redo the last undone operation
251
+ */
252
+ redo() {
253
+ if (this.undoManager.canRedo()) {
254
+ this.undoManager.redo();
255
+ }
256
+ }
257
+
258
+
259
+
260
+ /** @param {{from: {offset: number, block: Block}, to: {offset: number, block: Block}}} [options={}] */
261
+ focus(options) {
262
+ tick().then(() => {
263
+ const sel = this.selection;
264
+ const fromBlock = options?.from?.block || sel.anchorBlock;
265
+ const toBlock = options?.to?.block || sel.focusBlock;
266
+ const fromOffset = options?.from?.offset ?? sel.startOffset ?? 0;
267
+ const toOffset = options?.to?.offset ?? sel.endOffset ?? 0;
268
+
269
+ if (fromBlock && toBlock) {
270
+ const fromPoint = fromBlock.getDOMPoint(fromOffset);
271
+ const toPoint = toBlock.getDOMPoint(toOffset);
272
+ if (fromPoint && toPoint) this.selection.setBaseAndExtent(fromPoint.node, fromPoint.offset, toPoint.node, toPoint.offset);
273
+ }
274
+ })
275
+
276
+ }
277
+
278
+ /**
279
+ * Insère un nouveau bloc dans le document
280
+ * @param {string} type - Le type du bloc (ex: 'paragraph')
281
+ * @param {Object} [props={}] - Les propriétés initiales
282
+ * @param {string|null} [parentId=null] - ID du parent (null pour racine)
283
+ * @param {number|null} [index=null] - Position dans la liste des enfants
284
+ */
285
+ insert(type, props = {}, parentId = null, index = null) {
286
+
287
+ const BlockClass = this.registry.get(type);
288
+ if (!BlockClass) {
289
+ throw new Error(`Block type "${type}" not registered.`);
290
+ }
291
+
292
+ const block = BlockClass.create(this, type, props, parentId, index);
293
+
294
+
295
+ this.commit();
296
+ return block;
297
+ }
298
+
299
+
300
+
301
+ /** @param {Block} block */
302
+ delete(block) {
303
+ this.deleteNode(block.node.id);
304
+ }
305
+
306
+
307
+
308
+
309
+ /**
310
+ * @param {string} nodeId
311
+ * @returns
312
+ */
313
+ deleteNode(nodeId) {
314
+ const block = this.blocks.get(nodeId);
315
+ if (!block) return;
316
+ this.tree.delete(block.node.id);
317
+ }
318
+
319
+ // EVENT HANDLING
320
+
321
+ /**
322
+ * Route un événement vers le bon bloc en fonction de la sélection courante.
323
+ * @param {string} handlerName - Le nom de la méthode à appeler sur le bloc (ex: 'beforeinput', 'keydown')
324
+ * @param {Event} e - L'événement natif
325
+ * @param {string} [hookName] - Optionnel : Le nom du hook d'extension à vérifier en premier
326
+ */
327
+ dispatchEventToSelection(handlerName, e, hookName) {
328
+ const sel = this.selection;
329
+ if (!sel.anchorBlock || !sel.focusBlock) return;
330
+
331
+ // 1. Essayer les hooks globaux d'extension d'abord
332
+ if (hookName) {
333
+ const hooks = this.hooks.get(hookName) || [];
334
+ for (const hook of hooks) {
335
+ const handled = hook(this, e, sel.anchorBlock);
336
+ if (handled === this.BREAK) {
337
+ e.preventDefault();
338
+ return;
339
+ }
340
+ }
341
+ }
342
+
343
+ // 2. Trouver la cible et lui déléguer
344
+ let targetBlock = null;
345
+ if (sel.anchorBlock === sel.focusBlock) {
346
+ targetBlock = sel.anchorBlock;
347
+ } else {
348
+ const anchorParents = sel.anchorBlock.parents || [];
349
+ const focusParents = sel.focusBlock.parents || [];
350
+ targetBlock = anchorParents.find(ancestor => focusParents.includes(ancestor)) || this;
351
+ }
352
+
353
+ // 3. Appeler la méthode sur le bloc cible s'il la possède
354
+ if (targetBlock && typeof targetBlock[handlerName] === 'function') {
355
+ const handled = targetBlock[handlerName](e);
356
+ if (handled) {
357
+ e.preventDefault();
358
+ }
359
+ }
360
+ }
361
+
362
+ /** @param {InputEvent} e */
363
+ handleBeforeinput(e) {
364
+ e.preventDefault(); // Toujours bloquer les mutations natives
365
+ this.dispatchEventToSelection('beforeinput', e, 'onBeforeInput');
366
+ }
367
+
368
+ /** @param {KeyboardEvent} e */
369
+ handleKeydown(e) {
370
+ // Undo: Ctrl+Z or Cmd+Z (without Shift)
371
+ if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
372
+ e.preventDefault();
373
+ this.undo();
374
+ return;
375
+ }
376
+
377
+ // Redo: Ctrl+Y, Cmd+Y, or Ctrl+Shift+Z, Cmd+Shift+Z
378
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
379
+ e.preventDefault();
380
+ this.redo();
381
+ return;
382
+ }
383
+
384
+ this.dispatchEventToSelection('keydown', e, 'onKeyDown');
385
+ }
386
+
387
+ /** @param {InputEvent} e */
388
+ beforeinput(e) {
389
+ return handleContainerBeforeInput(this, this, e);
390
+ }
391
+
392
+
393
+ }
394
+
395
+
@@ -0,0 +1,38 @@
1
+ <script>
2
+ import RichText from '../../behaviors/text/RichText.svelte';
3
+ /** @type {{block: import('./paragraph.svelte').Paragraph}}*/
4
+ let {block} = $props();
5
+ </script>
6
+
7
+ <div
8
+ bind:this={block.element}
9
+ data-block-id={block.id}
10
+ data-block-type="paragraph"
11
+ class="nabu-paragraph"
12
+ class:selected={block.selected}
13
+ class:first={block.isSelectionStart}
14
+ class:last={block.isSelectionEnd}
15
+ ><RichText delta={block.delta} /></div>
16
+
17
+ <style>
18
+ .nabu-paragraph {
19
+ white-space: pre-wrap;
20
+ margin-bottom: 0.5em;
21
+ &.selected {
22
+ background-color: rgba(59, 130, 246, 0.25);
23
+ }
24
+ &.first {
25
+ border-top: 1px solid rgba(59, 130, 246, 0.5);
26
+ }
27
+ &.last {
28
+ border-bottom: 1px solid rgba(59, 130, 246, 0.5);
29
+ }
30
+
31
+
32
+ /* &::before {
33
+ content: " ";
34
+ display: inline-block;
35
+ width: 0;
36
+ } */
37
+ }
38
+ </style>
@@ -0,0 +1,11 @@
1
+ export default Paragraph;
2
+ type Paragraph = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ declare const Paragraph: import("svelte").Component<{
7
+ block: import("./paragraph.svelte").Paragraph;
8
+ }, {}, "">;
9
+ type $$ComponentProps = {
10
+ block: import("./paragraph.svelte").Paragraph;
11
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @import { Nabu } from "../nabu.svelte";
3
+ */
4
+ export const ParagraphExtension: import("../..").Extension;
5
+ import ParagraphComponent from "./Paragraph.svelte";
6
+ import { Paragraph } from "./paragraph.svelte";
7
+ export { ParagraphComponent, Paragraph };
@@ -0,0 +1,44 @@
1
+ import { extension } from "../../utils/extensions";
2
+ import { Paragraph } from "./paragraph.svelte";
3
+ import ParagraphComponent from "./Paragraph.svelte";
4
+
5
+ /**
6
+ * @import { Nabu } from "../nabu.svelte";
7
+ */
8
+
9
+ const ParagraphExtension = extension("paragraph", {
10
+ block: Paragraph,
11
+ component: ParagraphComponent,
12
+ hooks: {
13
+ onInit: (nabu) => {
14
+ if (nabu.children.length === 0) {
15
+ nabu.insert("paragraph", {
16
+ text: ""
17
+ });
18
+
19
+ }
20
+ },
21
+ /** @param {Nabu} nabu @param {Paragraph} block @param {Event} event @param {{offset: number, delta: import('loro-crdt').Delta<string>}} data */
22
+ onSplit: (nabu, block, event, data) => {
23
+ const { offset, delta } = data;
24
+
25
+ block.delete({from: offset, to: -1});
26
+
27
+ const currentIndex = block.node.index();
28
+ const parent = block.node.parent();
29
+ const parentId = parent?.id.toString() || null;
30
+
31
+ const newBlock = nabu.insert("paragraph", { delta }, parentId, currentIndex + 1);
32
+
33
+ block.commit();
34
+
35
+ setTimeout(() => {
36
+ nabu.selection.setCursor(newBlock, 0);
37
+ }, 0);
38
+
39
+ return { block: newBlock };
40
+ }
41
+ }
42
+ })
43
+
44
+ export {ParagraphExtension, ParagraphComponent, Paragraph };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @import { Nabu, NabuNode } from "../nabu.svelte";
3
+ */
4
+ /**
5
+ * @typedef {NabuNode<{type: "paragraph", text: LoroText}>} ParagraphNode
6
+ */
7
+ export class Paragraph extends Block {
8
+ /** @param {Nabu} nabu @param {string} type @param {Object} [props={}] @param {string|null} [parentId=null] @param {number|null} [index=null] */
9
+ static create(nabu: Nabu, type: string, props?: Object, parentId?: string | null, index?: number | null): Paragraph;
10
+ /** @param {Nabu} nabu @param {ParagraphNode} node */
11
+ constructor(nabu: Nabu, node: ParagraphNode);
12
+ container: LoroText;
13
+ /** @type {TextBehavior} */
14
+ behavior: TextBehavior;
15
+ component: import("svelte").Component<any, any, string> | import("svelte").Component<{
16
+ block: import("./paragraph.svelte").Paragraph;
17
+ }, {}, "">;
18
+ get text(): string;
19
+ get delta(): import("loro-crdt").Delta<string>[];
20
+ selection: {
21
+ from: number;
22
+ to: number;
23
+ isCollapsed: boolean;
24
+ direction: "forward" | "backward" | "none";
25
+ } | null;
26
+ /** @param {Block} block */
27
+ absorbs(block: Block): boolean;
28
+ /** @param {import('loro-crdt').Delta<string>[]} data */
29
+ applyDelta(data?: import("loro-crdt").Delta<string>[]): void;
30
+ /** @param {Parameters<Block["split"]>[0]} [options] @returns {ReturnType<Block["split"]>} */
31
+ split(options?: Parameters<Block["split"]>[0]): ReturnType<Block["split"]>;
32
+ }
33
+ export type ParagraphNode = NabuNode<{
34
+ type: "paragraph";
35
+ text: LoroText;
36
+ }>;
37
+ import { Block } from "../block.svelte";
38
+ import { LoroText } from "loro-crdt";
39
+ import { TextBehavior } from "../../behaviors/text";
40
+ import type { Nabu } from "../nabu.svelte";
41
+ import type { NabuNode } from "../nabu.svelte";
@@ -0,0 +1,86 @@
1
+ import { Block } from "../block.svelte";
2
+ import { LoroText } from "loro-crdt";
3
+ import ParagraphComponent from "./Paragraph.svelte";
4
+ import { TextBehavior } from "../../behaviors/text";
5
+
6
+ /**
7
+ * @import { Nabu, NabuNode } from "../nabu.svelte";
8
+ */
9
+
10
+ /**
11
+ * @typedef {NabuNode<{type: "paragraph", text: LoroText}>} ParagraphNode
12
+ */
13
+
14
+ export class Paragraph extends Block {
15
+ /** @param {Nabu} nabu @param {ParagraphNode} node */
16
+ constructor(nabu, node) {
17
+ super(nabu, node);
18
+
19
+ const data = node.data;
20
+ this.container = data.get("text") ?? data.setContainer("text", new LoroText());
21
+
22
+ /** @type {TextBehavior} */
23
+ this.behavior = new TextBehavior(this, this.container);
24
+ this.behaviors.set("text", this.behavior);
25
+
26
+ this.serializers.set('markdown', () => this.behavior.toMarkdown());
27
+ this.serializers.set('json', () => ({
28
+ id: this.id,
29
+ type: 'paragraph',
30
+ content: this.behavior.toJSON()
31
+ }));
32
+ }
33
+
34
+ component = $derived(this.nabu.components.get("paragraph") || ParagraphComponent);
35
+
36
+ get text() {
37
+ return this.behavior.text;
38
+ }
39
+
40
+ get delta() {
41
+ return this.behavior.delta;
42
+ }
43
+
44
+ selection = $derived(this.behavior.selection);
45
+
46
+ /** @param {InputEvent} event */
47
+ beforeinput(event) {
48
+ return this.behavior.handleBeforeInput(event);
49
+ }
50
+
51
+ /**
52
+ * Retrouve le nœud texte et l'offset DOM pour un offset Modèle donné
53
+ * @param {number} targetOffset
54
+ * @returns {{node: Node, offset: number} | null}
55
+ */
56
+ getDOMPoint(targetOffset) {
57
+ if (!this.element) return null;
58
+ return this.behavior.getDOMPoint(targetOffset);
59
+ }
60
+
61
+ /** @param {Block} block */
62
+ absorbs(block) { return this.behavior.absorbs(block); }
63
+
64
+ /** @param {number} index @param {string} text */
65
+ insert(index, text) { return this.behavior.insert(index, text); }
66
+
67
+ /** @param {Parameters<Block["delete"]>[0]} [deletion] */
68
+ delete(deletion) { return this.behavior.delete(deletion); }
69
+
70
+ /** @param {import('loro-crdt').Delta<string>[]} data */
71
+ applyDelta(data = []) { return this.behavior.applyDelta(data); }
72
+
73
+ /** @param {Parameters<Block["split"]>[0]} [options] @returns {ReturnType<Block["split"]>} */
74
+ split(options) { return this.behavior.split(options); }
75
+
76
+ /** @param {Nabu} nabu @param {string} type @param {Object} [props={}] @param {string|null} [parentId=null] @param {number|null} [index=null] */
77
+ static create(nabu, type, props = {}, parentId = null, index = null) {
78
+ const node = nabu.tree.createNode(parentId || undefined, index || undefined);
79
+ node.data.set("type", "paragraph");
80
+ const container = node.data.setContainer("text", new LoroText());
81
+ if (props.text) container.insert(0, props.text || "Start writing...");
82
+ if (props.delta) container.applyDelta([...props.delta]);
83
+ const block = new Paragraph(nabu, node);
84
+ return block;
85
+ }
86
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @import { Nabu } from "./nabu.svelte";
3
+ * @import { Block } from "./block.svelte";
4
+ * @import { LoroTree, LoroTreeNode } from "loro-crdt";
5
+ */
6
+ export class NabuSelection extends SvelteSelection {
7
+ /** @param {Nabu} nabu */
8
+ constructor(nabu: Nabu);
9
+ nabu: Nabu;
10
+ /** @type {Set<Block>} */
11
+ previous: Set<Block>;
12
+ anchorBlock: Block | null | undefined;
13
+ focusBlock: Block | null | undefined;
14
+ startBlock: Block | null | undefined;
15
+ endBlock: Block | null | undefined;
16
+ start: {
17
+ from: number;
18
+ to: number;
19
+ direction: "forward" | "backward" | "none";
20
+ block: Block;
21
+ } | null | undefined;
22
+ end: {
23
+ from: number;
24
+ to: number;
25
+ direction: "forward" | "backward" | "none";
26
+ block: Block;
27
+ } | null | undefined;
28
+ /**
29
+ * Définit le curseur à un endroit précis du document (Modèle -> DOM)
30
+ * @param {Block} block
31
+ * @param {number} offset
32
+ */
33
+ setCursor(block: Block, offset: number): void;
34
+ blocks: Set<Block>;
35
+ }
36
+ import { SvelteSelection } from "../utils/selection.svelte";
37
+ import type { Nabu } from "./nabu.svelte";
38
+ import type { Block } from "./block.svelte";