@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,611 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{
|
|
3
|
+
* anchorNode: Node | null,
|
|
4
|
+
* anchorOffset: number,
|
|
5
|
+
* focusNode: Node | null,
|
|
6
|
+
* focusOffset: number,
|
|
7
|
+
* startNode: Node | null,
|
|
8
|
+
* startOffset: number,
|
|
9
|
+
* endNode: Node | null,
|
|
10
|
+
* endOffset: number,
|
|
11
|
+
* isCollapsed: boolean,
|
|
12
|
+
* rangeCount: number,
|
|
13
|
+
* type: string,
|
|
14
|
+
* direction: "forward" | "backward" | "none",
|
|
15
|
+
* text: string,
|
|
16
|
+
* html: string
|
|
17
|
+
* }} SelectionObject
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Utilitaire réactif pour gérer la sélection de texte avec Svelte 5 runes
|
|
23
|
+
* @example
|
|
24
|
+
* const selection = new SvelteSelection();
|
|
25
|
+
* // Utiliser selection.text, selection.isCollapsed, etc. dans vos composants
|
|
26
|
+
*/
|
|
27
|
+
export class SvelteSelection {
|
|
28
|
+
constructor() {
|
|
29
|
+
// Utilisation d'un effect root pour éviter les fuites mémoire
|
|
30
|
+
this.#cleanup = $effect.root(() => {
|
|
31
|
+
$effect(() => {
|
|
32
|
+
// Vérification de la disponibilité de l'API
|
|
33
|
+
if (typeof window === 'undefined' || !window.getSelection) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
document.addEventListener('selectionchange', this.#updateSelection);
|
|
38
|
+
this.#updateSelection();
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
document.removeEventListener('selectionchange', this.#updateSelection);
|
|
42
|
+
this.#cleanup?.();
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @type {Function?} */
|
|
49
|
+
#cleanup = null;
|
|
50
|
+
|
|
51
|
+
/** @type {Selection?} */
|
|
52
|
+
raw = null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* État interne de la sélection
|
|
56
|
+
* @type {SelectionObject?}
|
|
57
|
+
*/
|
|
58
|
+
#sel = $state(null);
|
|
59
|
+
is = $derived(!!this.#sel);
|
|
60
|
+
|
|
61
|
+
/** @type {Range[]} */
|
|
62
|
+
#ranges = $state([]);
|
|
63
|
+
|
|
64
|
+
// Propriétés réactives de base
|
|
65
|
+
anchorNode = $derived(this.#sel?.anchorNode ?? null);
|
|
66
|
+
anchorOffset = $derived(this.#sel?.anchorOffset ?? 0);
|
|
67
|
+
focusNode = $derived(this.#sel?.focusNode ?? null);
|
|
68
|
+
focusOffset = $derived(this.#sel?.focusOffset ?? 0);
|
|
69
|
+
isCollapsed = $derived(this.#sel?.isCollapsed ?? true);
|
|
70
|
+
rangeCount = $derived(this.#sel?.rangeCount ?? 0);
|
|
71
|
+
type = $derived(this.#sel?.type ?? '');
|
|
72
|
+
direction = $derived(this.#sel?.direction ?? 'none');
|
|
73
|
+
|
|
74
|
+
startNode = $derived(this.#sel?.startNode ?? null);
|
|
75
|
+
endNode = $derived(this.#sel?.endNode ?? null);
|
|
76
|
+
startOffset = $derived(this.#sel?.startOffset ?? 0);
|
|
77
|
+
endOffset = $derived(this.#sel?.endOffset ?? 0);
|
|
78
|
+
|
|
79
|
+
// Propriétés des ranges
|
|
80
|
+
ranges = $derived(this.#ranges);
|
|
81
|
+
firstRange = $derived(this.#ranges[0] ?? null);
|
|
82
|
+
get range() {
|
|
83
|
+
// Retourne la première range si elle existe, sinon null
|
|
84
|
+
return this.firstRange;
|
|
85
|
+
}
|
|
86
|
+
lastRange = $derived(this.#ranges[this.#ranges.length - 1] ?? null);
|
|
87
|
+
|
|
88
|
+
// Propriétés dérivées utiles
|
|
89
|
+
text = $derived(this.#sel?.text ?? '');
|
|
90
|
+
html = $derived(this.#sel?.html ?? '');
|
|
91
|
+
hasSelection = $derived(!this.isCollapsed && this.rangeCount > 0);
|
|
92
|
+
isEmpty = $derived(this.text.length === 0);
|
|
93
|
+
hasMultipleRanges = $derived(this.#ranges.length > 1);
|
|
94
|
+
|
|
95
|
+
// Informations sur les éléments contenants
|
|
96
|
+
commonAncestor = $derived.by(() => {
|
|
97
|
+
if (!this.hasSelection || this.rangeCount === 0) return null;
|
|
98
|
+
try {
|
|
99
|
+
const range = this.firstRange;
|
|
100
|
+
return range?.commonAncestorContainer ?? null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Informations dérivées des ranges
|
|
107
|
+
allText = $derived.by(() => {
|
|
108
|
+
return this.#ranges.map(range => range.toString()).join('');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
allHtml = $derived.by(() => {
|
|
112
|
+
return this.#ranges.map(range => {
|
|
113
|
+
try {
|
|
114
|
+
const fragment = range.cloneContents();
|
|
115
|
+
const div = document.createElement('div');
|
|
116
|
+
div.appendChild(fragment);
|
|
117
|
+
return div.innerHTML;
|
|
118
|
+
} catch {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
}).join('');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Bounding rectangles de toutes les ranges
|
|
125
|
+
boundingRects = $derived.by(() => {
|
|
126
|
+
return this.#ranges.map(range => {
|
|
127
|
+
try {
|
|
128
|
+
return range.getBoundingClientRect();
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}).filter(rect => rect !== null);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Méthode privée pour mettre à jour la sélection
|
|
136
|
+
#updateSelection = () => {
|
|
137
|
+
try {
|
|
138
|
+
this.raw = window.getSelection();
|
|
139
|
+
|
|
140
|
+
if (!this.raw) {
|
|
141
|
+
this.#sel = null;
|
|
142
|
+
this.#ranges = [];
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Mise à jour des ranges
|
|
147
|
+
const newRanges = [];
|
|
148
|
+
for (let i = 0; i < this.raw.rangeCount; i++) {
|
|
149
|
+
try {
|
|
150
|
+
const range = this.raw.getRangeAt(i);
|
|
151
|
+
newRanges.push(range);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.warn(`Erreur lors de la récupération de la range ${i}:`, error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
this.#ranges = newRanges;
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
let text = '';
|
|
160
|
+
let html = '';
|
|
161
|
+
|
|
162
|
+
if (this.raw.rangeCount > 0 && !this.raw.isCollapsed) {
|
|
163
|
+
try {
|
|
164
|
+
const range = this.raw.getRangeAt(0);
|
|
165
|
+
text = range.toString();
|
|
166
|
+
|
|
167
|
+
const fragment = range.cloneContents();
|
|
168
|
+
const div = document.createElement('div');
|
|
169
|
+
div.appendChild(fragment);
|
|
170
|
+
html = div.innerHTML;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.warn('Erreur lors de l\'extraction du contenu de la sélection:', error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const range = this.raw.rangeCount > 0 ? this.raw.getRangeAt(0) : null;
|
|
177
|
+
|
|
178
|
+
this.#sel = {
|
|
179
|
+
//SEL
|
|
180
|
+
anchorNode: this.raw.anchorNode,
|
|
181
|
+
anchorOffset: this.raw.anchorOffset,
|
|
182
|
+
focusNode: this.raw.focusNode,
|
|
183
|
+
focusOffset: this.raw.focusOffset,
|
|
184
|
+
//RANGE:
|
|
185
|
+
startNode: range?.startContainer || null,
|
|
186
|
+
startOffset: range?.startOffset || 0,
|
|
187
|
+
endNode: range?.endContainer || null,
|
|
188
|
+
endOffset: range?.endOffset || 0,
|
|
189
|
+
//MISC
|
|
190
|
+
isCollapsed: this.raw.isCollapsed,
|
|
191
|
+
rangeCount: this.raw.rangeCount,
|
|
192
|
+
type: this.raw.type,
|
|
193
|
+
direction: this.raw.direction || 'none',
|
|
194
|
+
text,
|
|
195
|
+
html
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error('Erreur lors de la mise à jour de la sélection:', error);
|
|
199
|
+
this.#sel = null;
|
|
200
|
+
this.#ranges = [];
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Méthode pour rafraîchir la sélection manuellement
|
|
205
|
+
refresh = () => this.#updateSelection();
|
|
206
|
+
|
|
207
|
+
// Méthodes de manipulation de la sélection avec gestion d'erreurs
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Ajoute une range à la sélection.
|
|
211
|
+
* @param {Range} range - La range à ajouter.
|
|
212
|
+
* @returns {boolean} True si l'opération a réussi
|
|
213
|
+
*/
|
|
214
|
+
addRange = (range) => {
|
|
215
|
+
try {
|
|
216
|
+
this.raw?.addRange(range);
|
|
217
|
+
this.#updateSelection();
|
|
218
|
+
return true;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.warn('Erreur lors de l\'ajout de la range:', error);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Supprime toutes les ranges de la sélection.
|
|
227
|
+
* @returns {boolean} True si l'opération a réussi
|
|
228
|
+
*/
|
|
229
|
+
removeAllRanges = () => {
|
|
230
|
+
try {
|
|
231
|
+
this.raw?.removeAllRanges();
|
|
232
|
+
this.#updateSelection();
|
|
233
|
+
return true;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.warn('Erreur lors de la suppression des ranges:', error);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Supprime une range spécifique de la sélection.
|
|
242
|
+
* @param {Range} range - La range à supprimer.
|
|
243
|
+
* @returns {boolean} True si l'opération a réussi
|
|
244
|
+
*/
|
|
245
|
+
removeRange = (range) => {
|
|
246
|
+
try {
|
|
247
|
+
this.raw?.removeRange(range);
|
|
248
|
+
this.#updateSelection();
|
|
249
|
+
return true;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.warn('Erreur lors de la suppression de la range:', error);
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Récupère la range à l'index spécifié.
|
|
258
|
+
* @param {number} index - L'index de la range à récupérer.
|
|
259
|
+
* @returns {Range|null} La range à l'index spécifié, ou null si elle n'existe pas.
|
|
260
|
+
*/
|
|
261
|
+
getRangeAt = (index) => {
|
|
262
|
+
try {
|
|
263
|
+
return this.raw?.getRangeAt(index) ?? null;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.warn('Erreur lors de la récupération de la range:', error);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Réduit la sélection à un nœud et offset spécifiques.
|
|
272
|
+
* @param {Node} node - Le nœud vers lequel réduire la sélection.
|
|
273
|
+
* @param {number} offset - L'offset dans le nœud.
|
|
274
|
+
* @returns {boolean} True si l'opération a réussi
|
|
275
|
+
*/
|
|
276
|
+
collapse = (node, offset = 0) => {
|
|
277
|
+
try {
|
|
278
|
+
this.raw?.collapse(node, offset);
|
|
279
|
+
this.#updateSelection();
|
|
280
|
+
return true;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.warn('Erreur lors de la réduction de la sélection:', error);
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Réduit la sélection au début de la range courante.
|
|
289
|
+
* @returns {boolean} True si l'opération a réussi
|
|
290
|
+
*/
|
|
291
|
+
collapseToStart = () => {
|
|
292
|
+
try {
|
|
293
|
+
this.raw?.collapseToStart();
|
|
294
|
+
this.#updateSelection();
|
|
295
|
+
return true;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.warn('Erreur lors de la réduction au début:', error);
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Réduit la sélection à la fin de la range courante.
|
|
304
|
+
* @returns {boolean} True si l'opération a réussi
|
|
305
|
+
*/
|
|
306
|
+
collapseToEnd = () => {
|
|
307
|
+
try {
|
|
308
|
+
this.raw?.collapseToEnd();
|
|
309
|
+
this.#updateSelection();
|
|
310
|
+
return true;
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.warn('Erreur lors de la réduction à la fin:', error);
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Sélectionne le contenu d'un nœud.
|
|
319
|
+
* @param {Node} node - Le nœud dont le contenu doit être sélectionné.
|
|
320
|
+
* @returns {boolean} True si l'opération a réussi
|
|
321
|
+
*/
|
|
322
|
+
selectAllChildren = (node) => {
|
|
323
|
+
try {
|
|
324
|
+
this.raw?.selectAllChildren(node);
|
|
325
|
+
this.#updateSelection();
|
|
326
|
+
return true;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.warn('Erreur lors de la sélection des enfants:', error);
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Étend la sélection vers un nœud et offset spécifiques.
|
|
335
|
+
* @param {Node} node - Le nœud vers lequel étendre.
|
|
336
|
+
* @param {number} offset - L'offset dans le nœud.
|
|
337
|
+
* @returns {boolean} True si l'opération a réussi
|
|
338
|
+
*/
|
|
339
|
+
extend = (node, offset = 0) => {
|
|
340
|
+
try {
|
|
341
|
+
this.raw?.extend(node, offset);
|
|
342
|
+
this.#updateSelection();
|
|
343
|
+
return true;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.warn('Erreur lors de l\'extension de la sélection:', error);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Définit la base et l'extension de la sélection.
|
|
352
|
+
* @param {Node} anchorNode - Le nœud d'ancrage.
|
|
353
|
+
* @param {number} anchorOffset - L'offset d'ancrage.
|
|
354
|
+
* @param {Node} focusNode - Le nœud de focus.
|
|
355
|
+
* @param {number} focusOffset - L'offset de focus.
|
|
356
|
+
* @returns {boolean} True si l'opération a réussi
|
|
357
|
+
|
|
358
|
+
*/
|
|
359
|
+
setBaseAndExtent = (anchorNode, anchorOffset, focusNode, focusOffset) => {
|
|
360
|
+
try {
|
|
361
|
+
this.raw?.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
|
|
362
|
+
this.#updateSelection();
|
|
363
|
+
return true;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.warn('Erreur lors de la définition de la base et extension:', error);
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Vérifie si la sélection contient un nœud donné.
|
|
372
|
+
* @param {Node} node - Le nœud à vérifier.
|
|
373
|
+
* @param {boolean} allowPartialContainment - Permet la contenance partielle.
|
|
374
|
+
* @returns {boolean} True si le nœud est contenu dans la sélection.
|
|
375
|
+
*/
|
|
376
|
+
containsNode = (node, allowPartialContainment = false) => {
|
|
377
|
+
try {
|
|
378
|
+
return this.raw?.containsNode(node, allowPartialContainment) ?? false;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.warn('Erreur lors de la vérification de contenance:', error);
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Crée une nouvelle range basée sur la sélection courante.
|
|
387
|
+
* @returns {Range|null} Une nouvelle range ou null.
|
|
388
|
+
*/
|
|
389
|
+
createRange = () => {
|
|
390
|
+
try {
|
|
391
|
+
if (!this.hasSelection) return null;
|
|
392
|
+
return this.firstRange?.cloneRange() ?? null;
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.warn('Erreur lors de la création de la range:', error);
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Crée des copies de toutes les ranges courantes.
|
|
401
|
+
* @returns {Range[]} Un array de toutes les ranges clonées.
|
|
402
|
+
*/
|
|
403
|
+
createAllRanges = () => {
|
|
404
|
+
try {
|
|
405
|
+
return this.#ranges.map(range => range.cloneRange());
|
|
406
|
+
} catch (error) {
|
|
407
|
+
console.warn('Erreur lors de la création des ranges:', error);
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Récupère les coordonnées de toutes les ranges.
|
|
414
|
+
* @returns {DOMRect[]} Un array des rectangles de toutes les ranges.
|
|
415
|
+
*/
|
|
416
|
+
getAllBoundingRects = () => {
|
|
417
|
+
return this.boundingRects;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Vérifie si toutes les ranges sont dans un élément donné.
|
|
422
|
+
* @param {Element} element - L'élément à vérifier.
|
|
423
|
+
* @returns {boolean} True si toutes les ranges sont contenues.
|
|
424
|
+
*/
|
|
425
|
+
allRangesInElement = (element) => {
|
|
426
|
+
return this.#ranges.every(range => {
|
|
427
|
+
try {
|
|
428
|
+
return element.contains(range.commonAncestorContainer);
|
|
429
|
+
} catch {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Filtre les ranges selon un prédicat.
|
|
437
|
+
* @param {function(Range): boolean} predicate - Fonction de filtre.
|
|
438
|
+
* @returns {Range[]} Les ranges qui passent le filtre.
|
|
439
|
+
*/
|
|
440
|
+
filterRanges = (predicate) => {
|
|
441
|
+
return this.#ranges.filter(predicate);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Sélectionne tout le contenu du document.
|
|
446
|
+
* @returns {boolean} True si l'opération a réussi
|
|
447
|
+
*/
|
|
448
|
+
selectAll = () => {
|
|
449
|
+
try {
|
|
450
|
+
this.raw?.selectAllChildren(document.body);
|
|
451
|
+
this.#updateSelection();
|
|
452
|
+
return true;
|
|
453
|
+
} catch (error) {
|
|
454
|
+
console.warn('Erreur lors de la sélection de tout:', error);
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Efface la sélection courante.
|
|
461
|
+
* @returns {boolean} True si l'opération a réussi
|
|
462
|
+
*/
|
|
463
|
+
clear = () => {
|
|
464
|
+
return this.removeAllRanges();
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Ajout à la classe SvelteSelection pour observer les mutations DOM
|
|
469
|
+
* qui peuvent affecter la position du caret sans déclencher selectionchange
|
|
470
|
+
*/
|
|
471
|
+
|
|
472
|
+
// Propriétés à ajouter à la classe
|
|
473
|
+
/** @type {MutationObserver?} */
|
|
474
|
+
#mutationObserver = null;
|
|
475
|
+
|
|
476
|
+
/** @type {Set<Element>} */
|
|
477
|
+
#observedElements = new Set();
|
|
478
|
+
|
|
479
|
+
/** @type {number?} */
|
|
480
|
+
#debounceTimer = null;
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Observe un élément contenteditable pour les mutations DOM qui peuvent
|
|
484
|
+
* affecter la position du caret sans déclencher selectionchange
|
|
485
|
+
*
|
|
486
|
+
* @param {Element} element - L'élément contenteditable à observer
|
|
487
|
+
* @param {Object} options - Options d'observation
|
|
488
|
+
* @param {number} options.debounceMs - Délai de debounce en ms (défaut: 16ms = ~1 frame)
|
|
489
|
+
* @param {boolean} options.observeAttributes - Observer les changements d'attributs (défaut: false)
|
|
490
|
+
* @returns {boolean} True si l'observation a été configurée avec succès
|
|
491
|
+
*/
|
|
492
|
+
observe = (element, options = {}) => {
|
|
493
|
+
const {
|
|
494
|
+
debounceMs = 16,
|
|
495
|
+
observeAttributes = false
|
|
496
|
+
} = options;
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
// Vérifier que l'élément est contenteditable
|
|
500
|
+
if (!element.isContentEditable && element.contentEditable !== 'true') {
|
|
501
|
+
console.warn('L\'élément observé n\'est pas contenteditable');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Créer l'observer si il n'existe pas encore
|
|
505
|
+
if (!this.#mutationObserver) {
|
|
506
|
+
this.#mutationObserver = new MutationObserver((mutations) => {
|
|
507
|
+
// Filtrer les mutations pertinentes
|
|
508
|
+
const relevantMutations = mutations.filter(mutation => {
|
|
509
|
+
// Ignorer les mutations sur les attributs non-pertinents
|
|
510
|
+
if (mutation.type === 'attributes') {
|
|
511
|
+
const attr = mutation.attributeName;
|
|
512
|
+
// Ne considérer que les attributs qui peuvent affecter le layout/contenu
|
|
513
|
+
return ['class', 'style', 'contenteditable'].includes(attr);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Toujours considérer les changements de contenu
|
|
517
|
+
return mutation.type === 'childList' || mutation.type === 'characterData';
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
if (relevantMutations.length === 0) return;
|
|
521
|
+
|
|
522
|
+
// Debounce pour éviter trop d'appels
|
|
523
|
+
this.#updateSelection();
|
|
524
|
+
// clearTimeout(this.#debounceTimer);
|
|
525
|
+
// this.#debounceTimer = setTimeout(() => {
|
|
526
|
+
// this.#updateSelection();
|
|
527
|
+
// }, debounceMs);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Ajouter l'élément à la liste des observés
|
|
532
|
+
this.#observedElements.add(element);
|
|
533
|
+
|
|
534
|
+
// Configurer l'observation
|
|
535
|
+
this.#mutationObserver.observe(element, {
|
|
536
|
+
childList: true, // Ajout/suppression d'enfants
|
|
537
|
+
subtree: true, // Observer tous les descendants
|
|
538
|
+
characterData: true, // Changements de texte dans les nœuds text
|
|
539
|
+
attributes: observeAttributes, // Changements d'attributs (optionnel)
|
|
540
|
+
attributeFilter: observeAttributes ? ['class', 'style', 'contenteditable'] : undefined
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
return true;
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.warn('Erreur lors de la configuration de l\'observation:', error);
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Arrête l'observation d'un élément spécifique
|
|
552
|
+
* @param {Element} element - L'élément à ne plus observer
|
|
553
|
+
* @returns {boolean} True si l'élément était observé et a été retiré
|
|
554
|
+
*/
|
|
555
|
+
unobserve = (element) => {
|
|
556
|
+
const wasObserved = this.#observedElements.delete(element);
|
|
557
|
+
|
|
558
|
+
// Si plus aucun élément n'est observé, déconnecter l'observer
|
|
559
|
+
if (this.#observedElements.size === 0 && this.#mutationObserver) {
|
|
560
|
+
this.#mutationObserver.disconnect();
|
|
561
|
+
this.#mutationObserver = null;
|
|
562
|
+
clearTimeout(this.#debounceTimer);
|
|
563
|
+
this.#debounceTimer = null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return wasObserved;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Arrête l'observation de tous les éléments
|
|
571
|
+
*/
|
|
572
|
+
unobserveAll = () => {
|
|
573
|
+
if (this.#mutationObserver) {
|
|
574
|
+
this.#mutationObserver.disconnect();
|
|
575
|
+
this.#mutationObserver = null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
this.#observedElements.clear();
|
|
579
|
+
clearTimeout(this.#debounceTimer);
|
|
580
|
+
this.#debounceTimer = null;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Getter pour savoir quels éléments sont observés
|
|
585
|
+
* @returns {Element[]} Liste des éléments actuellement observés
|
|
586
|
+
*/
|
|
587
|
+
get observedElements() {
|
|
588
|
+
return Array.from(this.#observedElements);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Vérifie si un élément est actuellement observé
|
|
593
|
+
* @param {Element} element - L'élément à vérifier
|
|
594
|
+
* @returns {boolean} True si l'élément est observé
|
|
595
|
+
*/
|
|
596
|
+
isObserving = (element) => {
|
|
597
|
+
return this.#observedElements.has(element);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Nettoie les ressources utilisées par l'instance.
|
|
602
|
+
* À appeler lors de la destruction du composant.
|
|
603
|
+
*/
|
|
604
|
+
destroy = () => {
|
|
605
|
+
this.unobserveAll();
|
|
606
|
+
this.#cleanup?.();
|
|
607
|
+
this.raw = null;
|
|
608
|
+
this.#sel = null;
|
|
609
|
+
this.#ranges = [];
|
|
610
|
+
};
|
|
611
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aionbuilders/nabu",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "vite dev",
|
|
6
|
+
"build": "vite build && npm run prepack",
|
|
7
|
+
"preview": "vite preview",
|
|
8
|
+
"prepare": "svelte-kit sync || echo ''",
|
|
9
|
+
"prepack": "bunx svelte-kit sync && bunx svelte-package && bunx publint",
|
|
10
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
|
11
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"!dist/**/*.test.*",
|
|
16
|
+
"!dist/**/*.spec.*"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": [
|
|
19
|
+
"**/*.css"
|
|
20
|
+
],
|
|
21
|
+
"svelte": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"svelte": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"svelte": "^5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@sveltejs/adapter-auto": "^7.0.0",
|
|
35
|
+
"@sveltejs/kit": "^2.50.2",
|
|
36
|
+
"@sveltejs/package": "^2.5.7",
|
|
37
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
38
|
+
"publint": "^0.3.17",
|
|
39
|
+
"svelte": "^5.49.2",
|
|
40
|
+
"svelte-check": "^4.3.6",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vite": "^7.3.1",
|
|
43
|
+
"vite-plugin-top-level-await": "^1.6.0",
|
|
44
|
+
"vite-plugin-wasm": "^3.5.0"
|
|
45
|
+
},
|
|
46
|
+
"description": "Modular, local-first block editor engine for Svelte 5, built on a single contenteditable architecture and Loro-CRDT.",
|
|
47
|
+
"author": {
|
|
48
|
+
"name": "Killian Di Vincenzo",
|
|
49
|
+
"url": "https://github.com/killiandvcz"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "https://github.com/aionbuilders/nabu"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/aionbuilders/nabu#readme",
|
|
60
|
+
"keywords": [
|
|
61
|
+
"svelte",
|
|
62
|
+
"svelte5",
|
|
63
|
+
"editor",
|
|
64
|
+
"block-editor",
|
|
65
|
+
"rich-text",
|
|
66
|
+
"crdt",
|
|
67
|
+
"loro",
|
|
68
|
+
"local-first",
|
|
69
|
+
"collaborative"
|
|
70
|
+
],
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"loro-crdt": "^1.10.6"
|
|
73
|
+
}
|
|
74
|
+
}
|