@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,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import {NabuNode, Block, NabuSelection} from "../../blocks";
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { LoroText } from "loro-crdt";
|
|
6
|
+
import { tick } from "svelte";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a Loro text delta to inline Markdown.
|
|
10
|
+
* @param {import('loro-crdt').Delta<string>[]} delta
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function deltaToMarkdown(delta) {
|
|
14
|
+
return delta.map(op => {
|
|
15
|
+
if (typeof op.insert !== 'string') return '';
|
|
16
|
+
const text = op.insert;
|
|
17
|
+
const attrs = op.attributes || {};
|
|
18
|
+
if (attrs.code) return `\`${text}\``;
|
|
19
|
+
if (attrs.bold && attrs.italic) return `***${text}***`;
|
|
20
|
+
if (attrs.bold) return `**${text}**`;
|
|
21
|
+
if (attrs.italic) return `*${text}*`;
|
|
22
|
+
if (attrs.strikethrough) return `~~${text}~~`;
|
|
23
|
+
if (attrs.underline) return `<u>${text}</u>`;
|
|
24
|
+
return text;
|
|
25
|
+
}).join('');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {NabuNode<{type: "paragraph", text: LoroText}>} TextNode
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export class TextBehavior {
|
|
33
|
+
/** @param {Block} block @param {LoroText} [container] */
|
|
34
|
+
constructor(block, container) {
|
|
35
|
+
this.block = block;
|
|
36
|
+
this.nabu = block.nabu;
|
|
37
|
+
const node = /** @type {TextNode} */ (block.node);
|
|
38
|
+
/** @type {LoroText} */
|
|
39
|
+
this.container = container ?? node.data.get("text") ?? node.data.setContainer("text", new LoroText());
|
|
40
|
+
this.text = $state(this.container.toString());
|
|
41
|
+
this.delta = $state(this.container.toDelta());
|
|
42
|
+
this.container.subscribe(() => {
|
|
43
|
+
this.text = this.container.toString();
|
|
44
|
+
this.delta = this.container.toDelta();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
selection = $derived(this.block.element && this.block.nabu.selection && this.getSelection(this.block.element, this.block.nabu.selection));
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {HTMLElement} element
|
|
52
|
+
* @param {NabuSelection} globalSelection
|
|
53
|
+
*/
|
|
54
|
+
getSelection(element, globalSelection) {
|
|
55
|
+
if (!this.block.selected || !element || !globalSelection) return null;
|
|
56
|
+
|
|
57
|
+
const globalRange = globalSelection.firstRange;
|
|
58
|
+
if (!globalRange) return null;
|
|
59
|
+
|
|
60
|
+
const containsStart = element.contains(globalRange.startContainer);
|
|
61
|
+
const containsEnd = element.contains(globalRange.endContainer);
|
|
62
|
+
|
|
63
|
+
let from = 0;
|
|
64
|
+
let to = this.text.length;
|
|
65
|
+
|
|
66
|
+
if (containsStart) {
|
|
67
|
+
from = this.calculateOffset(globalRange.startContainer, globalRange.startOffset);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (containsEnd) {
|
|
71
|
+
to = this.calculateOffset(globalRange.endContainer, globalRange.endOffset);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
from,
|
|
76
|
+
to,
|
|
77
|
+
isCollapsed: from === to,
|
|
78
|
+
direction: globalSelection.direction,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Calcule l'offset textuel d'un point DOM par rapport au début de ce bloc
|
|
84
|
+
* @param {Node} node
|
|
85
|
+
* @param {number} offset
|
|
86
|
+
* @param {HTMLElement?} [element]
|
|
87
|
+
*/
|
|
88
|
+
calculateOffset(node, offset, element = this.block.element) {
|
|
89
|
+
if (!element) return 0;
|
|
90
|
+
const range = document.createRange();
|
|
91
|
+
range.setStart(element, 0);
|
|
92
|
+
range.setEnd(node, offset);
|
|
93
|
+
return range.toString().length;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [selection] */
|
|
97
|
+
handleBeforeInput(event, selection = this.selection) {
|
|
98
|
+
// Hook system (Markdown shortcuts, de-transformation, etc.)
|
|
99
|
+
const hooks = this.nabu.hooks.get("onBeforeInput");
|
|
100
|
+
if (hooks) {
|
|
101
|
+
for (const hook of hooks) {
|
|
102
|
+
const result = hook(this.nabu, event, this.block);
|
|
103
|
+
if (result === this.nabu.BREAK) {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
switch (event.inputType) {
|
|
111
|
+
case "insertText":
|
|
112
|
+
return this.handleInsertText(event, selection);
|
|
113
|
+
case "insertLineBreak":
|
|
114
|
+
case "insertSoftLineBreak":
|
|
115
|
+
return this.handleInsertLineBreak(event, selection);
|
|
116
|
+
case "deleteContentBackward":
|
|
117
|
+
return this.handleDeleteContentBackward(event, selection);
|
|
118
|
+
case "deleteContentForward":
|
|
119
|
+
return this.handleDeleteContentForward(event, selection);
|
|
120
|
+
case "insertParagraph":
|
|
121
|
+
return this.handleInsertParagraph(event, selection);
|
|
122
|
+
default:
|
|
123
|
+
console.warn("Unhandled input type:", event.inputType);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [selection] */
|
|
129
|
+
handleInsertText(event, selection = this.selection) {
|
|
130
|
+
if (!selection) return;
|
|
131
|
+
|
|
132
|
+
const textToInsert = event.data || "";
|
|
133
|
+
if (!selection.isCollapsed) this.delete({index: selection.from, length: selection.to - selection.from}, selection);
|
|
134
|
+
this.insert(selection.from, textToInsert);
|
|
135
|
+
this.block.commit();
|
|
136
|
+
tick().then(() => this.nabu.selection.setCursor(this.block, selection.from + textToInsert.length));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
|
|
140
|
+
handleInsertLineBreak(event, sel = this.selection) {
|
|
141
|
+
if (!sel) return;
|
|
142
|
+
if (!sel.isCollapsed) {
|
|
143
|
+
this.delete({index: sel.from, length: sel.to - sel.from}, sel);
|
|
144
|
+
}
|
|
145
|
+
this.insert(sel.from, "\n");
|
|
146
|
+
this.block.commit();
|
|
147
|
+
|
|
148
|
+
tick().then(() => this.nabu.selection.setCursor(this.block, sel.from + 1));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
|
|
152
|
+
handleDeleteContentBackward(event, sel = this.selection) {
|
|
153
|
+
if (!sel) return;
|
|
154
|
+
if (sel.isCollapsed && sel.from === 0) {
|
|
155
|
+
const previousBlock = this.block.findBackward(b => b.behaviors.has("text") && b.behaviors.get("text") instanceof TextBehavior);
|
|
156
|
+
if (!previousBlock) return;
|
|
157
|
+
const previousBehavior = previousBlock?.behaviors.get("text");
|
|
158
|
+
if (previousBehavior && previousBehavior instanceof TextBehavior) {
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
const previousLength = previousBehavior.text.length;
|
|
161
|
+
// previousBehavior.delta([
|
|
162
|
+
// { retain: previousLength },
|
|
163
|
+
// ...this.container.toDelta()
|
|
164
|
+
// ])
|
|
165
|
+
previousBlock.consume(this.block);
|
|
166
|
+
this.block.destroy();
|
|
167
|
+
this.block.commit();
|
|
168
|
+
|
|
169
|
+
tick().then(() => this.nabu.selection.setCursor(previousBehavior.block, previousLength));
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
const length = sel.isCollapsed ? 1 : sel.to - sel.from;
|
|
174
|
+
const index = sel.isCollapsed ? sel.from - 1 : sel.from;
|
|
175
|
+
this.delete({index, length});
|
|
176
|
+
this.block.commit();
|
|
177
|
+
|
|
178
|
+
tick().then(() => this.nabu.selection.setCursor(this.block, index));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
|
|
184
|
+
handleDeleteContentForward(event, sel = this.selection) {
|
|
185
|
+
if (!sel) return;
|
|
186
|
+
if (sel.isCollapsed && sel.from === this.text.length) {
|
|
187
|
+
const nextBlock = this.block.findForward(b => b.behaviors.has("text") && b.behaviors.get("text") instanceof TextBehavior);
|
|
188
|
+
if (!nextBlock) return;
|
|
189
|
+
const nextBehavior = nextBlock?.behaviors.get("text");
|
|
190
|
+
if (nextBehavior && nextBehavior instanceof TextBehavior) {
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
// this.delta([
|
|
193
|
+
// { retain: this.container.length },
|
|
194
|
+
// ...nextBehavior.container.toDelta()
|
|
195
|
+
// ]);
|
|
196
|
+
this.block.consume(nextBlock);
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
nextBehavior.block.destroy();
|
|
200
|
+
this.block.commit();
|
|
201
|
+
|
|
202
|
+
tick().then(() => this.nabu.selection.setCursor(this.block, sel.from));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
|
|
208
|
+
handleInsertParagraph(event, sel = this.selection) {
|
|
209
|
+
if (!sel) return;
|
|
210
|
+
const from = sel.from;
|
|
211
|
+
const to = sel.isCollapsed ? this.text.length : sel.to ;
|
|
212
|
+
const newBlock = this.block.ascend("onSplit", event, { offset: from, delta: this.container.sliceDelta(from, to)});
|
|
213
|
+
return newBlock;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Retrouve le nœud texte et l'offset DOM pour un offset Modèle donné
|
|
218
|
+
* @param {number} targetOffset
|
|
219
|
+
* @param {HTMLElement?} [element]
|
|
220
|
+
*/
|
|
221
|
+
getDOMPoint(targetOffset, element = this.block.element) {
|
|
222
|
+
if (!element) return null;
|
|
223
|
+
|
|
224
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
225
|
+
let currentOffset = 0;
|
|
226
|
+
let node = walker.nextNode();
|
|
227
|
+
|
|
228
|
+
while (node) {
|
|
229
|
+
const length = node.textContent?.length || 0;
|
|
230
|
+
if (currentOffset + length >= targetOffset) {
|
|
231
|
+
return { node, offset: targetOffset - currentOffset };
|
|
232
|
+
}
|
|
233
|
+
currentOffset += length;
|
|
234
|
+
node = walker.nextNode();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Fallback : fin du bloc ou élément lui-même si vide
|
|
238
|
+
return { node: element, offset: element.childNodes.length };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** @param {number} index @param {string} text */
|
|
242
|
+
insert(index, text) {
|
|
243
|
+
this.container.insert(index, text);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** @param {Parameters<Block["delete"]>[0]} [deletion] @param {ReturnType<TextBehavior["getSelection"]>} [selection] */
|
|
247
|
+
delete(deletion, selection = this.selection) {
|
|
248
|
+
const l = this.container.length;
|
|
249
|
+
let from = deletion?.from ?? selection?.from ?? 0;
|
|
250
|
+
if (from < 0) from = l + from + 1;
|
|
251
|
+
let to = deletion?.to ?? selection?.to ?? l;
|
|
252
|
+
if (to < 0) to = l + (to + 1) ;
|
|
253
|
+
const index = deletion?.index ?? from;
|
|
254
|
+
const length = deletion?.length ?? (to - from);
|
|
255
|
+
this.container.delete(index, length);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** @param {import('loro-crdt').Delta<string>[]} data */
|
|
259
|
+
applyDelta(data = []) {
|
|
260
|
+
this.container.applyDelta(data);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @param {Parameters<Block["split"]>[0]} [options]
|
|
265
|
+
* @param {ReturnType<TextBehavior["getSelection"]>} [selection]
|
|
266
|
+
* @returns {ReturnType<Block["split"]>}
|
|
267
|
+
*/
|
|
268
|
+
split(options, selection = this.selection) {
|
|
269
|
+
const sel = selection;
|
|
270
|
+
if (!sel) return null;
|
|
271
|
+
const offset = options?.index ?? options?.offset ?? options?.from;
|
|
272
|
+
const from = options?.from ?? offset ?? sel.from;
|
|
273
|
+
const to = options?.to ?? (options?.length ? from + options.length : (offset ?? sel.to));
|
|
274
|
+
return this.block.ascend("onSplit", null, { offset: from, delta: this.container.sliceDelta(from, to)});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** @param {string} markName @param {any} value @param {ReturnType<TextBehavior["getSelection"]>} sel */
|
|
278
|
+
applyMark(markName, value, sel) {
|
|
279
|
+
if (!sel || sel.isCollapsed) return;
|
|
280
|
+
this.container.mark({ start: sel.from, end: sel.to }, markName, value);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** @param {string} markName @param {ReturnType<TextBehavior["getSelection"]>} sel */
|
|
284
|
+
removeMark(markName, sel) {
|
|
285
|
+
if (!sel || sel.isCollapsed) return;
|
|
286
|
+
this.container.unmark({ start: sel.from, end: sel.to }, markName);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** @param {string} markName @param {ReturnType<TextBehavior["getSelection"]>} sel */
|
|
290
|
+
isMarkActive(markName, sel) {
|
|
291
|
+
if (!sel || sel.isCollapsed) return false;
|
|
292
|
+
const delta = this.container.toDelta();
|
|
293
|
+
let offset = 0;
|
|
294
|
+
for (const op of delta) {
|
|
295
|
+
if (typeof op.insert !== 'string') continue;
|
|
296
|
+
const opEnd = offset + op.insert.length;
|
|
297
|
+
if (opEnd <= sel.from) { offset = opEnd; continue; }
|
|
298
|
+
if (offset >= sel.to) break;
|
|
299
|
+
if (!op.attributes?.[markName]) return false;
|
|
300
|
+
offset = opEnd;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** @param {string} markName @param {any} value @param {ReturnType<TextBehavior["getSelection"]>} sel */
|
|
306
|
+
toggleMark(markName, value, sel) {
|
|
307
|
+
if (!sel || sel.isCollapsed) return;
|
|
308
|
+
if (this.isMarkActive(markName, sel)) {
|
|
309
|
+
this.removeMark(markName, sel);
|
|
310
|
+
} else {
|
|
311
|
+
this.applyMark(markName, value, sel);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** @returns {string} */
|
|
316
|
+
toMarkdown() {
|
|
317
|
+
return deltaToMarkdown(this.delta);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Converts the text delta to a Slate-like JSON format.
|
|
322
|
+
* @returns {{text: string, [mark: string]: any}[]}
|
|
323
|
+
*/
|
|
324
|
+
toJSON() {
|
|
325
|
+
return this.delta
|
|
326
|
+
.filter(op => typeof op.insert === 'string')
|
|
327
|
+
.map(op => {
|
|
328
|
+
const run = { text: /** @type {string} */ (op.insert) };
|
|
329
|
+
if (op.attributes) Object.assign(run, op.attributes);
|
|
330
|
+
return run;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** @param {Block} other */
|
|
335
|
+
absorbs(other) {
|
|
336
|
+
const otherBehavior = other.behaviors.get("text");
|
|
337
|
+
if (!otherBehavior || !(otherBehavior instanceof TextBehavior)) return false;
|
|
338
|
+
this.applyDelta([
|
|
339
|
+
{ retain: this.container.length },
|
|
340
|
+
...otherBehavior.container.toDelta()
|
|
341
|
+
]);
|
|
342
|
+
// other.destroy();
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/** @type {{ block: import('./block.svelte.js').Block }} */
|
|
3
|
+
let { block } = $props();
|
|
4
|
+
|
|
5
|
+
// Récupération dynamique du composant via le Registry de Nabu
|
|
6
|
+
// block.nabu.components est la Map<string, Component>
|
|
7
|
+
let Component = $derived(block.component || block.nabu.components.get(block.type));
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
{#if Component}
|
|
11
|
+
<!-- On passe l'instance du bloc au composant spécifique -->
|
|
12
|
+
<Component {block} />
|
|
13
|
+
{:else}
|
|
14
|
+
<!-- Fallback pour les types inconnus (utile en dev) -->
|
|
15
|
+
<div style="color: red; border: 1px dashed red; padding: 0.5rem; margin: 0.5rem 0;" contenteditable="false">
|
|
16
|
+
⚠️ Unknown block type: <strong>{block.type}</strong>
|
|
17
|
+
</div>
|
|
18
|
+
{/if}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default Block;
|
|
2
|
+
type Block = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const Block: import("svelte").Component<{
|
|
7
|
+
block: import("./block.svelte.js").Block;
|
|
8
|
+
}, {}, "">;
|
|
9
|
+
type $$ComponentProps = {
|
|
10
|
+
block: import("./block.svelte.js").Block;
|
|
11
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { Nabu } from "./nabu.svelte.js";
|
|
3
|
+
import Block from "./Block.svelte";
|
|
4
|
+
|
|
5
|
+
/** @type {{ engine: Nabu }} */
|
|
6
|
+
let { engine = new Nabu() } = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<div
|
|
10
|
+
contenteditable="true"
|
|
11
|
+
class="nabu-editor"
|
|
12
|
+
spellcheck="false"
|
|
13
|
+
translate="no"
|
|
14
|
+
data-nabu-root="true"
|
|
15
|
+
onbeforeinput={(e) => engine.handleBeforeinput(e)}
|
|
16
|
+
onkeydown={(e) => engine.handleKeydown(e)}
|
|
17
|
+
>
|
|
18
|
+
{#each engine.children as block (block.id)}
|
|
19
|
+
<Block {block} />
|
|
20
|
+
{/each}
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
.nabu-editor {
|
|
25
|
+
outline: none;
|
|
26
|
+
min-height: 100px;
|
|
27
|
+
padding: 1rem;
|
|
28
|
+
white-space: pre-wrap; /* Important pour le comportement éditeur */
|
|
29
|
+
word-break: break-word;
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default Nabu;
|
|
2
|
+
type Nabu = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const Nabu: import("svelte").Component<{
|
|
7
|
+
engine: Nabu;
|
|
8
|
+
}, {}, "">;
|
|
9
|
+
import { Nabu } from "./nabu.svelte.js";
|
|
10
|
+
type $$ComponentProps = {
|
|
11
|
+
engine: Nabu;
|
|
12
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export class Block {
|
|
2
|
+
/** @param {Nabu} nabu @param {NabuNode} node */
|
|
3
|
+
static load(nabu: Nabu, node: NabuNode): Block;
|
|
4
|
+
/** @param {Nabu} nabu @param {string} type @param {Object} [props={}] @param {string|null} [parentId=null] @param {number|null} [index=null] */
|
|
5
|
+
static create(nabu: Nabu, type: string, props?: Object, parentId?: string | null, index?: number | null): Block;
|
|
6
|
+
/** @param {Nabu} nabu @param {NabuNode} node */
|
|
7
|
+
constructor(nabu: Nabu, node: NabuNode);
|
|
8
|
+
nabu: Nabu;
|
|
9
|
+
node: NabuNode;
|
|
10
|
+
id: string;
|
|
11
|
+
type: string;
|
|
12
|
+
/** @type {MegaBlock | null} */
|
|
13
|
+
parent: MegaBlock | null;
|
|
14
|
+
serializers: SvelteMap<any, any>;
|
|
15
|
+
behaviors: SvelteMap<any, any>;
|
|
16
|
+
selected: boolean;
|
|
17
|
+
isSelectionStart: boolean;
|
|
18
|
+
isSelectionEnd: boolean;
|
|
19
|
+
isIntermediate: boolean;
|
|
20
|
+
clearSelection(): void;
|
|
21
|
+
/** @type {{from: number, to: number, direction: "forward" | "backward" | "none"} | null} */
|
|
22
|
+
selection: {
|
|
23
|
+
from: number;
|
|
24
|
+
to: number;
|
|
25
|
+
direction: "forward" | "backward" | "none";
|
|
26
|
+
} | null;
|
|
27
|
+
index: number;
|
|
28
|
+
/** @type {Block?} */
|
|
29
|
+
previous: Block | null;
|
|
30
|
+
/** @type {Block?} */
|
|
31
|
+
next: Block | null;
|
|
32
|
+
parents: MegaBlock[];
|
|
33
|
+
component: import("svelte").Component<any, any, string> | null;
|
|
34
|
+
/** @type {HTMLElement | null} */
|
|
35
|
+
element: HTMLElement | null;
|
|
36
|
+
/** @param {(block: Block) => boolean} predicate @returns {Block | null} */
|
|
37
|
+
findForward(predicate: (block: Block) => boolean): Block | null;
|
|
38
|
+
/** @param {(block: Block) => boolean} predicate @returns {Block | null} */
|
|
39
|
+
findBackward(predicate: (block: Block) => boolean): Block | null;
|
|
40
|
+
/**
|
|
41
|
+
* Returns real, uncommitted siblings, which is useful for extensions that want to check the document structure before the transaction is committed.
|
|
42
|
+
*/
|
|
43
|
+
getAdjacentSiblings(): {
|
|
44
|
+
previous: null;
|
|
45
|
+
next: null;
|
|
46
|
+
previousNode?: undefined;
|
|
47
|
+
nextNode?: undefined;
|
|
48
|
+
} | {
|
|
49
|
+
previous: Block | null | undefined;
|
|
50
|
+
next: Block | null | undefined;
|
|
51
|
+
previousNode: import("loro-crdt").LoroTreeNode<Record<string, unknown>> | import("loro-crdt").LoroTreeNode<{
|
|
52
|
+
type: string;
|
|
53
|
+
}> | null;
|
|
54
|
+
nextNode: import("loro-crdt").LoroTreeNode<Record<string, unknown>> | import("loro-crdt").LoroTreeNode<{
|
|
55
|
+
type: string;
|
|
56
|
+
}> | null;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Helper pour trouver le dernier descendant profond qui matche
|
|
60
|
+
* @param {(block: Block) => boolean} predicate
|
|
61
|
+
* @returns {Block | null}
|
|
62
|
+
*/
|
|
63
|
+
findLastDescendant(predicate: (block: Block) => boolean): Block | null;
|
|
64
|
+
destroy(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Transforme ce bloc en un autre type de bloc.
|
|
67
|
+
* @param {string} newType
|
|
68
|
+
* @param {Object} [props={}]
|
|
69
|
+
*/
|
|
70
|
+
transformTo(newType: string, props?: Object): void;
|
|
71
|
+
/** @param {number} index @param {string} text */
|
|
72
|
+
insert(index: number, text: string): void;
|
|
73
|
+
/** @param {{from?: number, to?: number, index?: number, length?: number}} [deletion] */
|
|
74
|
+
delete(deletion?: {
|
|
75
|
+
from?: number;
|
|
76
|
+
to?: number;
|
|
77
|
+
index?: number;
|
|
78
|
+
length?: number;
|
|
79
|
+
}): void;
|
|
80
|
+
/** @param {Block} block @returns {any} */
|
|
81
|
+
absorbs(block: Block): any;
|
|
82
|
+
/** @param {Block} block @returns {any} */
|
|
83
|
+
mergeWith(block: Block): any;
|
|
84
|
+
/**
|
|
85
|
+
* Consumes another block, handling children relocation intelligently.
|
|
86
|
+
* @param {Block} otherBlock - The block to consume or be consumed by
|
|
87
|
+
* @param {'into' | 'from'} direction - 'into' = this merges into other, 'from' = other merges into this
|
|
88
|
+
* @returns {Block} The surviving block
|
|
89
|
+
*/
|
|
90
|
+
consume(otherBlock: Block, direction?: "into" | "from"): Block;
|
|
91
|
+
/** @param {Block[]} children @param {number | null} [index] */
|
|
92
|
+
adoptChildren(children: Block[], index?: number | null): void;
|
|
93
|
+
/** @param {{from?: number, to?: number, index?: number, length?: number, offset?: number}} options @returns {{block: Block} | null} */
|
|
94
|
+
split(options: {
|
|
95
|
+
from?: number;
|
|
96
|
+
to?: number;
|
|
97
|
+
index?: number;
|
|
98
|
+
length?: number;
|
|
99
|
+
offset?: number;
|
|
100
|
+
}): {
|
|
101
|
+
block: Block;
|
|
102
|
+
} | null;
|
|
103
|
+
/** @param {{start?: number, end?: number, offset?: number} | null } options @param {boolean} [passive=false] */
|
|
104
|
+
focus(options?: {
|
|
105
|
+
start?: number;
|
|
106
|
+
end?: number;
|
|
107
|
+
offset?: number;
|
|
108
|
+
} | null, passive?: boolean): {
|
|
109
|
+
start: {
|
|
110
|
+
node: Node;
|
|
111
|
+
offset: number;
|
|
112
|
+
} | null;
|
|
113
|
+
end: {
|
|
114
|
+
node: Node;
|
|
115
|
+
offset: number;
|
|
116
|
+
} | null;
|
|
117
|
+
options: {
|
|
118
|
+
startOffset: number;
|
|
119
|
+
endOffset: number;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* @param {number} offset
|
|
124
|
+
* @returns {{node: Node, offset: number} | null}
|
|
125
|
+
*/
|
|
126
|
+
getDOMPoint(offset: number): {
|
|
127
|
+
node: Node;
|
|
128
|
+
offset: number;
|
|
129
|
+
} | null;
|
|
130
|
+
/** @param {string} eventName @param {Event} event @param {Object} [data={}] */
|
|
131
|
+
ascend(eventName: string, event: Event, data?: Object): any;
|
|
132
|
+
/** @param {InputEvent} event @returns {any} */
|
|
133
|
+
beforeinput(event: InputEvent): any;
|
|
134
|
+
/** @param {KeyboardEvent} event @returns {any} */
|
|
135
|
+
keydown(event: KeyboardEvent): any;
|
|
136
|
+
commit(): void;
|
|
137
|
+
/** @param {string} format */
|
|
138
|
+
serialize(format: string): any;
|
|
139
|
+
}
|
|
140
|
+
import type { Nabu } from "./nabu.svelte";
|
|
141
|
+
import type { NabuNode } from "./nabu.svelte";
|
|
142
|
+
import type { MegaBlock } from "./megablock.svelte";
|
|
143
|
+
import { SvelteMap } from "svelte/reactivity";
|