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