@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,94 @@
1
+ import { TextBehavior } from "../../behaviors/text";
2
+ import HeadingComponent from "./Heading.svelte";
3
+ import { Block } from "../block.svelte";
4
+ import { LoroText } from "loro-crdt";
5
+
6
+ /**
7
+ * @import { Nabu, NabuNode } from "../nabu.svelte";
8
+ * @import { TextNode } from "../../behaviors/text";
9
+ */
10
+
11
+ /**
12
+ * @typedef {NabuNode<{type: "heading", text: LoroText, level: number}>} HeadingNode
13
+ */
14
+
15
+ export class Heading extends Block {
16
+ /** @param {Nabu} nabu @param {HeadingNode} node */
17
+ constructor(nabu, node) {
18
+ super(nabu, node);
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.level = data.get("level") || 1;
27
+
28
+ this.serializers.set('markdown', () => `${'#'.repeat(this.level)} ${this.behavior.toMarkdown()}`);
29
+ this.serializers.set('json', () => ({
30
+ id: this.id,
31
+ type: 'heading',
32
+ props: { level: this.level },
33
+ content: this.behavior.toJSON()
34
+ }));
35
+
36
+ // Synchronisation du niveau
37
+ this.node.data.subscribe(() => {
38
+ this.level = this.node.data.get("level") || 1;
39
+ });
40
+ }
41
+
42
+ /** @type {number} */
43
+ level = $state(1);
44
+
45
+ component = $derived(this.nabu.components.get("heading") || HeadingComponent);
46
+
47
+ get text() {
48
+ return this.behavior.text;
49
+ }
50
+
51
+ get delta() {
52
+ return this.behavior.delta;
53
+ }
54
+
55
+ selection = $derived(this.behavior.selection);
56
+
57
+ /** @param {InputEvent} event */
58
+ beforeinput(event) {
59
+ return this.behavior.handleBeforeInput(event);
60
+ }
61
+
62
+ /**
63
+ * @param {number} targetOffset
64
+ * @returns {{node: Node, offset: number} | null}
65
+ */
66
+ getDOMPoint(targetOffset) { return this.behavior.getDOMPoint(targetOffset); }
67
+
68
+ /** @param {Block} block */
69
+ absorbs(block) { return this.behavior.absorbs(block); }
70
+
71
+ /** @param {number} index @param {string} text */
72
+ insert(index, text) { return this.behavior.insert(index, text); }
73
+
74
+ /** @param {Parameters<Block["delete"]>[0]} [deletion] */
75
+ delete(deletion) { return this.behavior.delete(deletion); }
76
+
77
+ /** @param {import('loro-crdt').Delta<string>[]} data */
78
+ applyDelta(data = []) { return this.behavior.applyDelta(data); }
79
+
80
+ /** @param {Parameters<Block["split"]>[0]} [options] @returns {ReturnType<Block["split"]>} */
81
+ split(options) { return this.behavior.split(options); }
82
+
83
+ /** @param {Nabu} nabu @param {string} type @param {Object} [props={}] @param {string|null} [parentId=null] @param {number|null} [index=null] */
84
+ static create(nabu, type, props = {}, parentId = null, index = null) {
85
+ const node = nabu.tree.createNode(parentId || undefined, index || undefined);
86
+ node.data.set("type", "heading");
87
+ node.data.set("level", props.level || 1);
88
+ const container = node.data.setContainer("text", new LoroText());
89
+ if (props.text) container.insert(0, props.text);
90
+ if (props.delta) container.applyDelta([...props.delta]);
91
+ const block = new Heading(nabu, node);
92
+ return block;
93
+ }
94
+ }
@@ -0,0 +1,3 @@
1
+ export function onBeforeInput(nabu: Nabu, event: InputEvent, block: Block): symbol;
2
+ import type { Nabu } from "../..";
3
+ import type { Block } from "../..";
@@ -0,0 +1,58 @@
1
+ import { tick } from "svelte";
2
+ import { TextBehavior } from "../../../behaviors/text";
3
+ import {Heading} from "../heading.svelte";
4
+
5
+ /**
6
+ * @import { Nabu, Block } from "../..";
7
+ */
8
+
9
+ /** @param {Nabu} nabu @param {InputEvent} event @param {Block} block */
10
+ export const onBeforeInput = (nabu, event, block) => {
11
+ const {BREAK, CONTINUE} = nabu;
12
+ if (event.inputType === "deleteContentBackward" && block instanceof Heading) {
13
+ const behavior = block.behaviors.get("text");
14
+ if (!behavior || !(behavior instanceof TextBehavior)) return CONTINUE;
15
+ const sel = behavior.selection;
16
+ if (!sel || !sel.isCollapsed || sel.from !== 0) return CONTINUE;
17
+ const blockId = block.node.id.toString();
18
+ block.transformTo("paragraph");
19
+ tick().then(() => {
20
+ const block = nabu.blocks.get(blockId);
21
+ if (block) block.focus({start: 0, end: 0});
22
+ });
23
+ return BREAK;
24
+ }
25
+
26
+
27
+ if (event.data !== " ") return CONTINUE;
28
+
29
+ const behavior = block.behaviors.get("text");
30
+ if (!behavior || !(behavior instanceof TextBehavior)) return CONTINUE;
31
+
32
+ const sel = behavior.selection;
33
+ if (!sel || !sel.isCollapsed) return CONTINUE;
34
+
35
+ const text = behavior.text;
36
+ const textBeforeCursor = text.slice(0, sel.from);
37
+
38
+ const match = textBeforeCursor.match(/^(#{1,6})$/);
39
+ if (!match) return CONTINUE;
40
+
41
+ const level = match[1].length;
42
+
43
+ if (block.type === "heading" && block.level === level) return CONTINUE;
44
+
45
+ behavior.delete({index: 0, length: level});
46
+
47
+ const blockId = block.node.id.toString();
48
+
49
+ block.transformTo("heading", { level });
50
+
51
+ tick().then(() => {
52
+ const block = nabu.blocks.get(blockId);
53
+ if (block) block.focus({start: 0, end: 0});
54
+ });
55
+
56
+ return BREAK;
57
+ }
58
+
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @import { Nabu } from "../nabu.svelte";
3
+ */
4
+ export const HeadingExtension: import("../..").Extension;
5
+ import HeadingComponent from "./Heading.svelte";
6
+ import { Heading } from "./heading.svelte";
7
+ export { HeadingComponent, Heading };
@@ -0,0 +1,41 @@
1
+ import { tick } from "svelte";
2
+ import { TextBehavior } from "../../behaviors/text";
3
+ import { extension } from "../../utils/extensions";
4
+ import { Heading } from "./heading.svelte";
5
+ import HeadingComponent from "./Heading.svelte";
6
+ import { onBeforeInput } from "./hooks/onBeforeInput.hook";
7
+
8
+ /**
9
+ * @import { Nabu } from "../nabu.svelte";
10
+ */
11
+
12
+ const HeadingExtension = extension("heading", {
13
+ block: Heading,
14
+ component: HeadingComponent,
15
+ hooks: {
16
+ /** @param {Nabu} nabu @param {Heading} block @param {Event} event @param {{offset: number, delta: import('loro-crdt').Delta<string>}} data */
17
+ onSplit: (nabu, block, event, data) => {
18
+ const { offset, delta } = data;
19
+
20
+ block.delete({from: offset, to: -1});
21
+
22
+ const currentIndex = block.node.index();
23
+ const parent = block.node.parent();
24
+ const parentId = parent?.id.toString() || null;
25
+
26
+ // On split souvent un titre pour créer un paragraphe après
27
+ const newBlock = nabu.insert("paragraph", { delta }, parentId, currentIndex + 1);
28
+
29
+ block.commit();
30
+
31
+ setTimeout(() => {
32
+ nabu.selection.setCursor(newBlock, 0);
33
+ }, 0);
34
+
35
+ return { block: newBlock };
36
+ },
37
+ onBeforeInput,
38
+ }
39
+ })
40
+
41
+ export { HeadingExtension, HeadingComponent, Heading };
@@ -0,0 +1,10 @@
1
+ export * from "./block.svelte";
2
+ export * from "./nabu.svelte";
3
+ export * from "./megablock.svelte";
4
+ export * from "./container.utils";
5
+ export * from "./paragraph";
6
+ export * from "./heading";
7
+ export * from "./list";
8
+ export * from "./selection.svelte";
9
+ export { default as NabuEditor } from "./Nabu.svelte";
10
+ export { default as BlockComponent } from "./Block.svelte";
@@ -0,0 +1,12 @@
1
+ export * from "./block.svelte";
2
+ export * from "./nabu.svelte";
3
+ export * from "./megablock.svelte";
4
+ export { default as NabuEditor } from "./Nabu.svelte";
5
+ export { default as BlockComponent } from "./Block.svelte";
6
+ export * from "./container.utils";
7
+
8
+ export * from "./paragraph";
9
+ export * from "./heading";
10
+ export * from "./list";
11
+
12
+ export * from "./selection.svelte";
@@ -0,0 +1,25 @@
1
+ <script>
2
+ import Block from "../Block.svelte";
3
+
4
+ /** @type {{block: import('./list.svelte.js').List}}*/
5
+ let {block} = $props();
6
+ </script>
7
+
8
+ <svelte:element
9
+ this={block.listType === 'ordered' ? 'ol' : 'ul'}
10
+ data-block-id={block.id}
11
+ data-block-type="list"
12
+ class="nabu-list"
13
+ >
14
+ {#each block.children as child (child.id)}
15
+ <Block block={child} />
16
+ {/each}
17
+ </svelte:element>
18
+
19
+ <style>
20
+ .nabu-list {
21
+ margin-top: 0.5rem;
22
+ margin-bottom: 0.5rem;
23
+ padding-left: 1.5rem;
24
+ }
25
+ </style>
@@ -0,0 +1,11 @@
1
+ export default List;
2
+ type List = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ declare const List: import("svelte").Component<{
7
+ block: import("./list.svelte.js").List;
8
+ }, {}, "">;
9
+ type $$ComponentProps = {
10
+ block: import("./list.svelte.js").List;
11
+ };
@@ -0,0 +1,45 @@
1
+ <script>
2
+ import Block from "../Block.svelte";
3
+ import RichText from '../../behaviors/text/RichText.svelte';
4
+
5
+ /** @type {{block: import('./list-item.svelte.js').ListItem}}*/
6
+ let {block} = $props();
7
+ </script>
8
+
9
+ <li
10
+ data-block-id={block.id}
11
+ data-block-type="list-item"
12
+ class="nabu-list-item"
13
+ class:selected={block.selected}
14
+ class:first={block.isSelectionStart}
15
+ class:last={block.isSelectionEnd}
16
+ >
17
+ <div bind:this={block.element} class="item-content"><RichText delta={block.delta} /></div><!--
18
+ -->{#if block.children && block.children.length > 0}
19
+ <div class="item-children">
20
+ {#each block.children as child (child.id)}
21
+ <Block block={child} />
22
+ {/each}
23
+ </div>
24
+ {/if}
25
+ </li>
26
+
27
+ <style>
28
+ .nabu-list-item {
29
+ margin: 0.25rem 0;
30
+
31
+ &.selected > .item-content {
32
+ background-color: rgba(59, 130, 246, 0.25);
33
+ }
34
+ }
35
+
36
+ .item-content {
37
+ outline: none;
38
+ white-space: pre-wrap;
39
+ min-height: 1.5em; /* Permet de cliquer dans un item vide */
40
+ }
41
+
42
+ .item-children {
43
+ /* Pas de margin-left ici, c'est le <ul> imbriqué qui gèrera l'indentation */
44
+ }
45
+ </style>
@@ -0,0 +1,11 @@
1
+ export default ListItem;
2
+ type ListItem = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ declare const ListItem: import("svelte").Component<{
7
+ block: import("./list-item.svelte.js").ListItem;
8
+ }, {}, "">;
9
+ type $$ComponentProps = {
10
+ block: import("./list-item.svelte.js").ListItem;
11
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @import {SvelteSet} from "svelte/reactivity";
3
+ */
4
+ export const ListExtension: import("../..").Extension;
5
+ export const ListItemExtension: import("../..").Extension;
6
+ import { List } from "./list.svelte";
7
+ import { ListItem } from "./list-item.svelte";
8
+ import ListComponent from "./List.svelte";
9
+ import ListItemComponent from "./ListItem.svelte";
10
+ export { List, ListItem, ListComponent, ListItemComponent };
@@ -0,0 +1,41 @@
1
+ import { extension } from "../../utils/extensions";
2
+ import { List } from "./list.svelte";
3
+ import ListComponent from "./List.svelte";
4
+ import { ListItem } from "./list-item.svelte";
5
+ import ListItemComponent from "./ListItem.svelte";
6
+ import { ListBehavior } from "./list.behavior.svelte";
7
+
8
+ /**
9
+ * @import {SvelteSet} from "svelte/reactivity";
10
+ */
11
+
12
+ const ListExtension = extension("list", {
13
+ block: List,
14
+ component: ListComponent,
15
+ hooks: {
16
+ onBeforeTransaction(nabu) {
17
+ const lists = /** @type {SvelteSet<List>} */ (nabu.blocksByType.get("list"));
18
+ lists?.forEach(list => {
19
+ const behavior = list.behaviors.get("list");
20
+ if (!behavior || !(behavior instanceof ListBehavior)) return;
21
+ const sibblings = list.getAdjacentSiblings();
22
+ const previous = sibblings.previous;
23
+ if (!previous) return;
24
+ const previousBehavior = previous.behaviors.get("list");
25
+ if (!previousBehavior || !(previousBehavior instanceof ListBehavior)) return;
26
+
27
+ previousBehavior.absorbs(list);
28
+ list.destroy();
29
+
30
+ })
31
+
32
+ }
33
+ }
34
+ });
35
+
36
+ const ListItemExtension = extension("list-item", {
37
+ block: ListItem,
38
+ component: ListItemComponent,
39
+ });
40
+
41
+ export { ListExtension, ListItemExtension, List, ListItem, ListComponent, ListItemComponent };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @import { Nabu, NabuNode, Block } from "..";
3
+ */
4
+ /**
5
+ * @typedef {NabuNode<{type: "list-item", text: LoroText}>} ListItemNode
6
+ */
7
+ export class ListItem extends MegaBlock {
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): ListItem;
10
+ /** @param {Nabu} nabu @param {ListItemNode} node */
11
+ constructor(nabu: Nabu, node: ListItemNode);
12
+ container: LoroText;
13
+ /** @type {TextBehavior} */
14
+ behavior: TextBehavior;
15
+ component: import("svelte").Component<any, any, string> | import("svelte").Component<{
16
+ block: import("./list-item.svelte").ListItem;
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
+ sublist: Block | undefined;
27
+ /**
28
+ * Retourne la sous-liste existante, ou en crée une nouvelle à la fin des enfants.
29
+ * @param {"bullet" | "ordered"} [listType="bullet"]
30
+ */
31
+ getOrCreateSublist(listType?: "bullet" | "ordered"): Block;
32
+ /** @param {KeyboardEvent} event */
33
+ keydown(event: KeyboardEvent): boolean;
34
+ /** @param {import('../block.svelte').Block} block */
35
+ absorbs(block: import("../block.svelte").Block): boolean;
36
+ /** @param {import('loro-crdt').Delta<string>[]} data */
37
+ applyDelta(data?: import("loro-crdt").Delta<string>[]): void;
38
+ /** @param {Parameters<import('../block.svelte').Block["split"]>[0]} [options] @returns {ReturnType<import('../block.svelte').Block["split"]>} */
39
+ split(options?: Parameters<import("../block.svelte").Block["split"]>[0]): ReturnType<import("../block.svelte").Block["split"]>;
40
+ }
41
+ export type ListItemNode = NabuNode<{
42
+ type: "list-item";
43
+ text: LoroText;
44
+ }>;
45
+ import { MegaBlock } from "../megablock.svelte";
46
+ import { LoroText } from "loro-crdt";
47
+ import { TextBehavior } from "../../behaviors/text";
48
+ import type { Block } from "..";
49
+ import type { Nabu } from "..";
50
+ import type { NabuNode } from "..";
@@ -0,0 +1,213 @@
1
+ import { MegaBlock } from "../megablock.svelte";
2
+ import ListItemComponent from "./ListItem.svelte";
3
+ import { LoroText } from "loro-crdt";
4
+ import { TextBehavior } from "../../behaviors/text";
5
+
6
+ /**
7
+ * @import { Nabu, NabuNode, Block } from "..";
8
+ */
9
+
10
+ /**
11
+ * @typedef {NabuNode<{type: "list-item", text: LoroText}>} ListItemNode
12
+ */
13
+
14
+ export class ListItem extends MegaBlock {
15
+ /** @param {Nabu} nabu @param {ListItemNode} 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', () => {
27
+ const depth = this.parents.filter(p => p.type === 'listItem').length;
28
+ const indent = ' '.repeat(depth);
29
+ const listType = /** @type {any} */ (this.parent)?.listType ?? 'bullet';
30
+ const prefix = listType === 'ordered' ? `${this.index + 1}.` : '-';
31
+ const lines = [`${indent}${prefix} ${this.behavior.toMarkdown()}`];
32
+ if (this.sublist) {
33
+ const sublistMd = this.sublist.serialize('markdown');
34
+ if (sublistMd) lines.push(sublistMd);
35
+ }
36
+ return lines.join('\n');
37
+ });
38
+ this.serializers.set('json', () => {
39
+ /** @type {Record<string, any>} */
40
+ const result = {
41
+ id: this.id,
42
+ type: 'list-item',
43
+ content: this.behavior.toJSON()
44
+ };
45
+ const childrenJson = this.children.map(c => c.serialize('json')).filter(Boolean);
46
+ if (childrenJson.length) result.children = childrenJson;
47
+ return result;
48
+ });
49
+ }
50
+
51
+ component = $derived(this.nabu.components.get("list-item") || ListItemComponent);
52
+
53
+ get text() {
54
+ return this.behavior.text;
55
+ }
56
+
57
+ get delta() {
58
+ return this.behavior.delta;
59
+ }
60
+
61
+ selection = $derived(this.behavior.selection);
62
+
63
+ sublist = $derived(this.children.find(child => child.node.data.get("type") === "list"));
64
+
65
+ /**
66
+ * Retourne la sous-liste existante, ou en crée une nouvelle à la fin des enfants.
67
+ * @param {"bullet" | "ordered"} [listType="bullet"]
68
+ */
69
+ getOrCreateSublist(listType = "bullet") {
70
+ if (this.sublist) return this.sublist;
71
+
72
+ // S'il n'y en a pas, on demande à Nabu d'en créer une en tant qu'enfant de ce ListItem
73
+ const newList = this.nabu.insert("list", { listType }, this.node.id.toString());
74
+ return newList;
75
+ }
76
+
77
+ /** @param {KeyboardEvent} event */
78
+ keydown(event) {
79
+ if (event.key === "Tab") {
80
+ event.preventDefault(); // On bloque le comportement natif
81
+
82
+ if (event.shiftKey) {
83
+ // --- Shift+Tab (Unindent with Carry) ---
84
+
85
+ // 1. Notre parent immédiat (une List)
86
+ const parentListNode = this.node.parent();
87
+ if (!parentListNode) return true;
88
+
89
+ // 2. Le ListItem qui contient cette liste (le "grand-parent" logique)
90
+ const grandParentItemNode = parentListNode.parent();
91
+
92
+ // Si on n'a pas de ListItem au-dessus (on est déjà au premier niveau)
93
+ if (!grandParentItemNode || grandParentItemNode.data.get("type") !== "list-item") {
94
+ return true;
95
+ }
96
+
97
+ // 3. Gérer le "Carry" : emporter les frères suivants avec nous
98
+ const myIndex = this.node.index();
99
+ const siblings = parentListNode.children();
100
+ const followers = siblings.slice(myIndex + 1);
101
+
102
+ if (followers.length > 0) {
103
+ // On s'assure d'avoir notre propre sous-liste pour accueillir nos anciens frères
104
+ const mySublist = this.getOrCreateSublist();
105
+ for (const follower of followers) {
106
+ this.nabu.tree.move(follower.id.toString(), mySublist.node.id.toString());
107
+ }
108
+ }
109
+
110
+ // 4. Le parent cible est la List qui contient notre grand-parent
111
+ const targetParentListNode = grandParentItemNode.parent();
112
+ if (!targetParentListNode) return true;
113
+
114
+ // 5. On se place juste après notre ancien grand-parent
115
+ const targetIndex = grandParentItemNode.index() + 1;
116
+
117
+ this.nabu.tree.move(this.node.id.toString(), targetParentListNode.id.toString(), targetIndex);
118
+
119
+ this.commit();
120
+
121
+ // 6. Restauration du focus
122
+ setTimeout(() => this.behavior.block.focus(), 0);
123
+ } else {
124
+ // --- Tab (Indent with Hoist) ---
125
+
126
+ // 1. On trouve notre index actuel
127
+ const currentIndex = this.node.index();
128
+ if (currentIndex === 0) return true; // Impossible d'indenter le premier item
129
+
130
+ // 2. On trouve le frère précédent (qui va devenir notre parent "logique")
131
+ const parentList = this.node.parent();
132
+ if (!parentList) return true;
133
+
134
+ const previousNode = parentList.children()?.[currentIndex - 1];
135
+ if (!previousNode) return true;
136
+ const previousItem = this.nabu.blocks.get(previousNode.id.toString());
137
+
138
+ // Si le frère précédent n'est pas un ListItem
139
+ if (!previousItem || previousItem.type !== "list-item") return true;
140
+
141
+ // 3. On demande au frère précédent sa sous-liste (ou de la créer)
142
+ // @ts-ignore : On sait que c'est un ListItem
143
+ const targetSublist = previousItem.getOrCreateSublist();
144
+ const targetSublistId = targetSublist.node.id.toString();
145
+
146
+ // 4. On se déplace à la fin de cette sous-liste cible
147
+ this.nabu.tree.move(this.node.id.toString(), targetSublistId);
148
+
149
+ // 5. Si on avait des enfants (Hoist), on les remonte avec nous en tant que frères
150
+ if (this.sublist) {
151
+ const childrenNodes = this.sublist.node.children();
152
+ for (const childNode of childrenNodes) {
153
+ // Ils se placeront après nous dans la nouvelle liste
154
+ this.nabu.tree.move(childNode.id.toString(), targetSublistId);
155
+ }
156
+ }
157
+
158
+ this.commit();
159
+
160
+ // 6. On restaure le curseur
161
+ setTimeout(() => this.behavior.block.focus(), 0);
162
+ }
163
+ return true;
164
+ }
165
+
166
+ // On délègue les flèches etc.
167
+ return false;
168
+ }
169
+
170
+ /** @param {InputEvent} event */
171
+ beforeinput(event) {
172
+ return this.behavior.handleBeforeInput(event);
173
+ }
174
+
175
+ /**
176
+ * @param {number} targetOffset
177
+ * @returns {{node: Node, offset: number} | null}
178
+ */
179
+ getDOMPoint(targetOffset) { return this.behavior.getDOMPoint(targetOffset); }
180
+
181
+ /** @param {import('../block.svelte').Block} block */
182
+ absorbs(block) { return this.behavior.absorbs(block); }
183
+
184
+
185
+
186
+ /** @param {number} index @param {string} text */
187
+ insert(index, text) { return this.behavior.insert(index, text); }
188
+
189
+ /** @param {Parameters<import('../block.svelte').Block["delete"]>[0]} [deletion] */
190
+ delete(deletion) { return this.behavior.delete(deletion); }
191
+
192
+ /** @param {import('loro-crdt').Delta<string>[]} data */
193
+ applyDelta(data = []) { return this.behavior.applyDelta(data); }
194
+
195
+ /** @param {Parameters<import('../block.svelte').Block["split"]>[0]} [options] @returns {ReturnType<import('../block.svelte').Block["split"]>} */
196
+ split(options) {
197
+ return this.ascend("onSplit", null, {
198
+ offset: options?.offset || 0,
199
+ delta: this.behavior.container.sliceDelta(options?.offset || 0, this.text.length)
200
+ });
201
+ }
202
+
203
+ /** @param {Nabu} nabu @param {string} type @param {Object} [props={}] @param {string|null} [parentId=null] @param {number|null} [index=null] */
204
+ static create(nabu, type, props = {}, parentId = null, index = null) {
205
+ const node = nabu.tree.createNode(parentId || undefined, index || undefined);
206
+ node.data.set("type", "list-item");
207
+ const container = node.data.setContainer("text", new LoroText());
208
+ if (props.text) container.insert(0, props.text);
209
+ if (props.delta) container.applyDelta([...props.delta]);
210
+ const block = new ListItem(nabu, node);
211
+ return block;
212
+ }
213
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @import {Block, NabuNode} from "..";
3
+ * @import {LoroText} from "loro-crdt";
4
+ */
5
+ /**
6
+ * @typedef {NabuNode<{type: "list", listType: "bullet" | "ordered"}>} ListNode
7
+ */
8
+ export class ListBehavior {
9
+ /** @param {Block} block */
10
+ constructor(block: Block);
11
+ block: Block;
12
+ node: ListNode;
13
+ /** @type {"bullet" | "ordered"} */
14
+ listType: "bullet" | "ordered";
15
+ /** @param {Block} block */
16
+ absorbs(block: Block): true | undefined;
17
+ }
18
+ export type ListNode = NabuNode<{
19
+ type: "list";
20
+ listType: "bullet" | "ordered";
21
+ }>;
22
+ import type { Block } from "..";
23
+ import type { NabuNode } from "..";