@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,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,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";
|