svedit 0.12.0 → 0.14.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 +35 -22
- package/dist/Command.svelte.d.ts +38 -30
- package/dist/Command.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.js +271 -326
- package/dist/CustomProperty.svelte +3 -5
- package/dist/CustomProperty.svelte.d.ts +3 -6
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.d.ts +19 -6
- package/dist/KeyMapper.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.js +95 -112
- package/dist/Node.svelte +16 -13
- package/dist/Node.svelte.d.ts +3 -5
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +39 -22
- package/dist/NodeArrayProperty.svelte.d.ts +3 -5
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeCaret.svelte +2 -2
- package/dist/NodeCaret.svelte.d.ts +6 -14
- package/dist/NodeCaret.svelte.d.ts.map +1 -1
- package/dist/NodeGap.svelte +12 -6
- package/dist/NodeGap.svelte.d.ts +7 -14
- package/dist/NodeGap.svelte.d.ts.map +1 -1
- package/dist/NodeGapMarkers.svelte +36 -14
- package/dist/NodeGapMarkers.svelte.d.ts +6 -11
- package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
- package/dist/NodeSelectionMarkers.svelte +21 -7
- package/dist/NodeSelectionMarkers.svelte.d.ts +2 -5
- package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.d.ts +81 -129
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +542 -648
- package/dist/Svedit.svelte +146 -140
- package/dist/Svedit.svelte.d.ts +30 -10
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/TextProperty.svelte +37 -23
- package/dist/TextProperty.svelte.d.ts +3 -5
- package/dist/TextProperty.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +64 -127
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +839 -1001
- package/dist/UnknownNode.svelte +4 -3
- package/dist/UnknownNode.svelte.d.ts +5 -9
- package/dist/UnknownNode.svelte.d.ts.map +1 -1
- package/dist/doc_utils.d.ts +67 -155
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +619 -863
- package/dist/index.d.ts +20 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -17
- package/dist/node_visibility.svelte.d.ts +110 -9
- package/dist/node_visibility.svelte.d.ts.map +1 -1
- package/dist/node_visibility.svelte.js +473 -544
- package/dist/paste_utils.d.ts +11 -44
- package/dist/paste_utils.d.ts.map +1 -1
- package/dist/paste_utils.js +111 -152
- package/dist/styles/svedit-colors.css +8 -5
- package/dist/transforms.svelte.d.ts +9 -7
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +125 -150
- package/dist/types.d.ts +294 -309
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +40 -119
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +346 -473
- package/package.json +70 -70
- package/src/lib/{Command.svelte.js → Command.svelte.ts} +29 -17
- package/src/lib/CustomProperty.svelte +3 -5
- package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
- package/src/lib/Node.svelte +16 -13
- package/src/lib/NodeArrayProperty.svelte +39 -22
- package/src/lib/NodeCaret.svelte +2 -2
- package/src/lib/NodeGap.svelte +12 -6
- package/src/lib/NodeGapMarkers.svelte +36 -14
- package/src/lib/NodeSelectionMarkers.svelte +21 -7
- package/src/lib/{Session.svelte.js → Session.svelte.ts} +130 -127
- package/src/lib/Svedit.svelte +146 -140
- package/src/lib/TextProperty.svelte +37 -23
- package/src/lib/{Transaction.svelte.js → Transaction.svelte.ts} +139 -143
- package/src/lib/UnknownNode.svelte +4 -3
- package/src/lib/{doc_utils.js → doc_utils.ts} +247 -206
- package/src/lib/{index.js → index.ts} +48 -0
- package/src/lib/{node_visibility.svelte.js → node_visibility.svelte.ts} +68 -92
- package/src/lib/{paste_utils.js → paste_utils.ts} +47 -58
- package/src/lib/styles/svedit-colors.css +8 -5
- package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +39 -21
- package/src/lib/{types.d.ts → types.ts} +161 -66
- package/src/lib/{utils.js → utils.ts} +110 -129
|
@@ -18,74 +18,82 @@ import {
|
|
|
18
18
|
get_selected_annotations,
|
|
19
19
|
validate_selection
|
|
20
20
|
} from './doc_utils.js';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
import type { SelectedAttachment } from './doc_utils.js';
|
|
22
|
+
import type {
|
|
23
|
+
NodeId,
|
|
24
|
+
DocumentPath,
|
|
25
|
+
Selection,
|
|
26
|
+
Attachment,
|
|
27
|
+
NodeKind,
|
|
28
|
+
DocumentSchema,
|
|
29
|
+
DocumentNode,
|
|
30
|
+
Document,
|
|
31
|
+
AnyNode,
|
|
32
|
+
Text,
|
|
33
|
+
NodeArray,
|
|
34
|
+
CommandRegistry,
|
|
35
|
+
Inspection,
|
|
36
|
+
SessionConfig,
|
|
37
|
+
SveditContext
|
|
38
|
+
} from './types.js';
|
|
39
|
+
import type { Keymap } from './KeyMapper.svelte.js';
|
|
36
40
|
|
|
37
41
|
const BATCH_WINDOW_MS = 1000; // 1 second
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
type HistoryEntry = {
|
|
44
|
+
ops: Transaction['ops'];
|
|
45
|
+
inverse_ops: Transaction['inverse_ops'];
|
|
46
|
+
selection_before: Selection | null;
|
|
47
|
+
selection_after: Selection | null;
|
|
48
|
+
};
|
|
43
49
|
|
|
44
|
-
export default class Session {
|
|
45
|
-
|
|
46
|
-
#selection = $state.raw(null);
|
|
50
|
+
export default class Session<S extends DocumentSchema = DocumentSchema> {
|
|
51
|
+
#selection: Selection | null = $state.raw(null);
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
schema = $state.raw();
|
|
53
|
+
schema: S = $state.raw() as S;
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
doc = $state.raw();
|
|
55
|
+
doc: Document = $state.raw() as Document;
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
config = $state.raw();
|
|
57
|
+
config: SessionConfig = $state.raw({});
|
|
56
58
|
|
|
57
|
-
history = $state.raw([]);
|
|
59
|
+
history: HistoryEntry[] = $state.raw([]);
|
|
58
60
|
history_index = $state.raw(-1);
|
|
59
|
-
last_batch_started = $state.raw(undefined); // Timestamp for debounced batching
|
|
61
|
+
last_batch_started: number | undefined = $state.raw(undefined); // Timestamp for debounced batching
|
|
60
62
|
|
|
61
63
|
// Commands and keymap - initialized by Svedit when ready
|
|
62
64
|
// NOTE: Assumes single Svedit instance per session
|
|
63
|
-
commands = $state.raw({});
|
|
64
|
-
keymap = $state.raw({});
|
|
65
|
+
commands: CommandRegistry = $state.raw({});
|
|
66
|
+
keymap: Keymap = $state.raw({});
|
|
65
67
|
|
|
66
68
|
// Reactive helpers for UI state
|
|
67
69
|
can_undo = $derived(this.history_index >= 0);
|
|
68
70
|
can_redo = $derived(this.history_index < this.history.length - 1);
|
|
69
71
|
|
|
70
72
|
// Reactive variable for selected node
|
|
71
|
-
selected_node = $derived(this.get_selected_node());
|
|
73
|
+
selected_node: AnyNode<S> | null = $derived(this.get_selected_node());
|
|
72
74
|
available_mark_types = $derived(this.get_available_mark_types());
|
|
73
75
|
available_annotation_types = $derived(this.get_available_annotation_types());
|
|
74
76
|
selected_marks = $derived(get_selected_marks(this.schema, this.doc, this.selection));
|
|
75
|
-
active_mark = $derived(
|
|
77
|
+
active_mark: SelectedAttachment | null = $derived(
|
|
78
|
+
this.selected_marks.length === 1 ? this.selected_marks[0] : null
|
|
79
|
+
);
|
|
76
80
|
selected_annotations = $derived(get_selected_annotations(this.schema, this.doc, this.selection));
|
|
77
|
-
active_annotation = $derived(
|
|
81
|
+
active_annotation: SelectedAttachment | null = $derived(
|
|
78
82
|
this.selected_annotations.length === 1 ? this.selected_annotations[0] : null
|
|
79
83
|
);
|
|
80
84
|
|
|
81
85
|
/**
|
|
82
|
-
* @param
|
|
83
|
-
* @param
|
|
84
|
-
* @param
|
|
85
|
-
* @param
|
|
86
|
-
* @param {Selection} [options.selection] - Initial selection state
|
|
86
|
+
* @param schema - The document schema
|
|
87
|
+
* @param doc - The document
|
|
88
|
+
* @param config - configuration object
|
|
89
|
+
* @param options - Optional settings (e.g. initial selection state)
|
|
87
90
|
*/
|
|
88
|
-
constructor(
|
|
91
|
+
constructor(
|
|
92
|
+
schema: S,
|
|
93
|
+
doc: Document,
|
|
94
|
+
config: SessionConfig,
|
|
95
|
+
options: { selection?: Selection | null } = {}
|
|
96
|
+
) {
|
|
89
97
|
// Validate the schema first
|
|
90
98
|
validate_document_schema(schema);
|
|
91
99
|
|
|
@@ -101,18 +109,16 @@ export default class Session {
|
|
|
101
109
|
|
|
102
110
|
/**
|
|
103
111
|
* Gets the current selection
|
|
104
|
-
* @returns {Selection | null}
|
|
105
112
|
*/
|
|
106
|
-
get selection() {
|
|
113
|
+
get selection(): Selection | null {
|
|
107
114
|
return this.#selection;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
/**
|
|
111
118
|
* Sets the selection with validation
|
|
112
|
-
* @param {Selection | null} value - The new selection
|
|
113
119
|
* @throws {Error} Throws if the selection is invalid
|
|
114
120
|
*/
|
|
115
|
-
set selection(value) {
|
|
121
|
+
set selection(value: Selection | null) {
|
|
116
122
|
this._validate_selection(value);
|
|
117
123
|
this.#selection = value;
|
|
118
124
|
}
|
|
@@ -120,19 +126,16 @@ export default class Session {
|
|
|
120
126
|
/**
|
|
121
127
|
* Validates that a selection is within bounds and refers to valid paths.
|
|
122
128
|
*
|
|
123
|
-
* @param {Selection} selection - The selection to validate
|
|
124
129
|
* @throws {Error} Throws if the selection is invalid
|
|
125
|
-
* @private
|
|
126
130
|
*/
|
|
127
|
-
_validate_selection(selection) {
|
|
131
|
+
private _validate_selection(selection: Selection | null): void {
|
|
128
132
|
validate_selection(selection, this);
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
/**
|
|
132
136
|
* Gets the document_id from the doc
|
|
133
|
-
* @returns {string}
|
|
134
137
|
*/
|
|
135
|
-
get document_id() {
|
|
138
|
+
get document_id(): string {
|
|
136
139
|
return this.doc.document_id;
|
|
137
140
|
}
|
|
138
141
|
|
|
@@ -144,14 +147,12 @@ export default class Session {
|
|
|
144
147
|
* deleted nodes. That catches local node invariants and dangling references left
|
|
145
148
|
* behind by deletes without scanning every node on every apply.
|
|
146
149
|
*
|
|
147
|
-
* @param {Transaction} transaction - The transaction result to validate
|
|
148
150
|
* @throws {Error} Throws if the transaction result is invalid
|
|
149
151
|
*/
|
|
150
|
-
validate_transaction_result(transaction) {
|
|
152
|
+
validate_transaction_result(transaction: Transaction): void {
|
|
151
153
|
const doc = transaction.doc;
|
|
152
|
-
/** @type {Set<NodeId>} */
|
|
153
154
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- Non-reactive local dedup set.
|
|
154
|
-
const affected_node_ids = new Set([
|
|
155
|
+
const affected_node_ids: Set<NodeId> = new Set([
|
|
155
156
|
...transaction.created_node_ids,
|
|
156
157
|
...transaction.modified_node_ids
|
|
157
158
|
]);
|
|
@@ -184,7 +185,7 @@ export default class Session {
|
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
generate_id() {
|
|
188
|
+
generate_id(): string {
|
|
188
189
|
const id = this.config?.generate_id ? this.config.generate_id() : `node_${crypto.randomUUID()}`;
|
|
189
190
|
if (!is_id_valid(id)) {
|
|
190
191
|
throw new Error(
|
|
@@ -202,9 +203,9 @@ export default class Session {
|
|
|
202
203
|
* For multiple editors on the same document, this architecture would need
|
|
203
204
|
* to be refactored to support multiple sessions per document.
|
|
204
205
|
*
|
|
205
|
-
* @param
|
|
206
|
+
* @param context - The svedit context with session, editable, canvas, etc.
|
|
206
207
|
*/
|
|
207
|
-
initialize_commands(context) {
|
|
208
|
+
initialize_commands(context: SveditContext<S>): void {
|
|
208
209
|
if (this.config?.create_commands_and_keymap) {
|
|
209
210
|
const { commands, keymap } = this.config.create_commands_and_keymap(context);
|
|
210
211
|
this.commands = commands;
|
|
@@ -212,20 +213,20 @@ export default class Session {
|
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
get_available_mark_types() {
|
|
216
|
+
get_available_mark_types(): string[] {
|
|
216
217
|
if (this.selection?.type !== 'text' && this.selection?.type !== 'node') return [];
|
|
217
218
|
const property_definition = this.inspect(this.selection.path);
|
|
218
219
|
return property_definition.mark_types || [];
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
get_available_annotation_types() {
|
|
222
|
+
get_available_annotation_types(): string[] {
|
|
222
223
|
if (this.selection?.type !== 'text' && this.selection?.type !== 'node') return [];
|
|
223
224
|
const property_definition = this.inspect(this.selection.path);
|
|
224
225
|
return property_definition.annotation_types || [];
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
// Helper function to get the currently selected node
|
|
228
|
-
get_selected_node() {
|
|
229
|
+
get_selected_node(): AnyNode<S> | null {
|
|
229
230
|
if (!this.selection) return null;
|
|
230
231
|
|
|
231
232
|
if (this.selection.type === 'node') {
|
|
@@ -233,7 +234,7 @@ export default class Session {
|
|
|
233
234
|
const end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
234
235
|
// Only consider selection of a single node
|
|
235
236
|
if (end - start !== 1) return null;
|
|
236
|
-
const node_array = this.get(this.selection.path);
|
|
237
|
+
const node_array: NodeArray = this.get(this.selection.path);
|
|
237
238
|
const node_id = node_array.nodes[start];
|
|
238
239
|
return node_id ? this.get(node_id) : null;
|
|
239
240
|
} else {
|
|
@@ -247,23 +248,20 @@ export default class Session {
|
|
|
247
248
|
|
|
248
249
|
/**
|
|
249
250
|
* Creates a new transaction for making atomic changes to the document.
|
|
250
|
-
*
|
|
251
|
-
* @returns {Transaction} A new transaction instance
|
|
252
251
|
*/
|
|
253
|
-
get tr() {
|
|
252
|
+
get tr(): Transaction {
|
|
254
253
|
// We create a copy of the current state to avoid modifying the original
|
|
255
254
|
return new Transaction(this.schema, this.doc, this.selection, this.config);
|
|
256
255
|
}
|
|
257
256
|
|
|
258
257
|
/**
|
|
259
258
|
* Applies a transaction to the document.
|
|
260
|
-
* Auto-batches history entries with debounced behavior (max one entry per
|
|
259
|
+
* Auto-batches history entries with debounced behavior (max one entry per second) when batch is true.
|
|
261
260
|
*
|
|
262
|
-
* @param
|
|
263
|
-
* @param
|
|
264
|
-
* @param {boolean} [options.batch=false] - Whether to allow batching with previous transaction
|
|
261
|
+
* @param transaction - The transaction to apply
|
|
262
|
+
* @param options - Optional configuration (batch: whether to allow batching with previous transaction)
|
|
265
263
|
*/
|
|
266
|
-
apply(transaction, { batch = false } = {}) {
|
|
264
|
+
apply(transaction: Transaction, { batch = false }: { batch?: boolean } = {}): this {
|
|
267
265
|
this.validate_transaction_result(transaction);
|
|
268
266
|
// Only swap the doc when something changed. The transaction draft is
|
|
269
267
|
// always a fresh object, and assigning it for an ops-less transaction
|
|
@@ -285,7 +283,7 @@ export default class Session {
|
|
|
285
283
|
now - this.last_batch_started < BATCH_WINDOW_MS;
|
|
286
284
|
|
|
287
285
|
if (should_batch) {
|
|
288
|
-
// Append to existing history entry (within
|
|
286
|
+
// Append to existing history entry (within the batch window)
|
|
289
287
|
const last_entry = this.history[this.history_index];
|
|
290
288
|
last_entry.ops.push(...transaction.ops);
|
|
291
289
|
last_entry.inverse_ops.push(...transaction.inverse_ops);
|
|
@@ -293,7 +291,7 @@ export default class Session {
|
|
|
293
291
|
// Trigger update
|
|
294
292
|
this.history = [...this.history];
|
|
295
293
|
} else {
|
|
296
|
-
// Create new history entry (
|
|
294
|
+
// Create new history entry (batch window elapsed, first edit, or batch not requested)
|
|
297
295
|
this.history = [
|
|
298
296
|
...this.history,
|
|
299
297
|
{
|
|
@@ -315,7 +313,7 @@ export default class Session {
|
|
|
315
313
|
return this;
|
|
316
314
|
}
|
|
317
315
|
|
|
318
|
-
undo() {
|
|
316
|
+
undo(): this | undefined {
|
|
319
317
|
if (this.history_index < 0) {
|
|
320
318
|
return;
|
|
321
319
|
}
|
|
@@ -332,10 +330,13 @@ export default class Session {
|
|
|
332
330
|
this.doc = doc;
|
|
333
331
|
this.selection = change.selection_before;
|
|
334
332
|
this.history_index = this.history_index - 1;
|
|
333
|
+
// History navigation ends any open typing batch, so the next batched
|
|
334
|
+
// apply starts a fresh entry instead of appending to a stale one.
|
|
335
|
+
this.last_batch_started = undefined;
|
|
335
336
|
return this;
|
|
336
337
|
}
|
|
337
338
|
|
|
338
|
-
redo() {
|
|
339
|
+
redo(): this | undefined {
|
|
339
340
|
if (this.history_index >= this.history.length - 1) {
|
|
340
341
|
return;
|
|
341
342
|
}
|
|
@@ -347,13 +348,17 @@ export default class Session {
|
|
|
347
348
|
});
|
|
348
349
|
this.doc = doc;
|
|
349
350
|
this.selection = change.selection_after;
|
|
351
|
+
// Redo ends any open typing batch, matching undo().
|
|
352
|
+
this.last_batch_started = undefined;
|
|
350
353
|
return this;
|
|
351
354
|
}
|
|
352
355
|
|
|
353
356
|
/**
|
|
354
357
|
* Gets a node instance or property value at the specified path.
|
|
355
|
-
*
|
|
356
|
-
*
|
|
358
|
+
*
|
|
359
|
+
* The result type defaults to `any` because a path can address a node or
|
|
360
|
+
* any property value. Annotate the target to type the result.
|
|
361
|
+
*
|
|
357
362
|
* @example
|
|
358
363
|
* // Get a node by ID
|
|
359
364
|
* session.get('list_1') // => { type: 'list', id: 'list_1', ... }
|
|
@@ -370,7 +375,8 @@ export default class Session {
|
|
|
370
375
|
* // Get a text property
|
|
371
376
|
* session.get(['page_1', 'cover', 'title']) // => {content: 'Hello world', marks: [], annotations: []}
|
|
372
377
|
*/
|
|
373
|
-
|
|
378
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
379
|
+
get<T = any>(path: DocumentPath | string): T {
|
|
374
380
|
return doc_get(this.schema, this.doc, path);
|
|
375
381
|
}
|
|
376
382
|
|
|
@@ -396,31 +402,28 @@ export default class Session {
|
|
|
396
402
|
* type: 'paragraph',
|
|
397
403
|
* properties: {...}
|
|
398
404
|
* }
|
|
399
|
-
*
|
|
400
|
-
* @param {DocumentPath} path
|
|
401
|
-
* @returns {{kind: 'property'|'node', [key: string]: any}}
|
|
402
405
|
*/
|
|
403
|
-
inspect(path) {
|
|
406
|
+
inspect(path: DocumentPath): Inspection {
|
|
404
407
|
return doc_inspect(this.schema, this.doc, path);
|
|
405
408
|
}
|
|
406
409
|
|
|
407
410
|
/**
|
|
408
|
-
* Determines the kind of a node ('
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* @returns {NodeKind}
|
|
411
|
+
* Determines the kind of a node ('document' for root nodes, 'block' for
|
|
412
|
+
* structured blocks, 'text' for pure text nodes, 'mark' for mark nodes or
|
|
413
|
+
* 'annotation' for annotation nodes).
|
|
412
414
|
*/
|
|
413
|
-
kind(node) {
|
|
415
|
+
kind(node: DocumentNode): NodeKind {
|
|
414
416
|
return doc_kind(this.schema, node);
|
|
415
417
|
}
|
|
416
418
|
|
|
417
419
|
/**
|
|
418
420
|
* Determines whether a node type can be inserted at a given selection.
|
|
419
|
-
*
|
|
420
|
-
* @param
|
|
421
|
-
* @
|
|
421
|
+
*
|
|
422
|
+
* @param node_type - The type of node to insert.
|
|
423
|
+
* @param selection - The selection at which to insert the node.
|
|
424
|
+
* @returns True if the node type can be inserted, false otherwise.
|
|
422
425
|
*/
|
|
423
|
-
can_insert(node_type, selection = this.selection) {
|
|
426
|
+
can_insert(node_type: string, selection: Selection | null = this.selection): boolean {
|
|
424
427
|
if (selection?.type === 'node') {
|
|
425
428
|
const property_definition = this.inspect(selection.path);
|
|
426
429
|
if (property_definition.node_types.includes(node_type)) {
|
|
@@ -437,16 +440,16 @@ export default class Session {
|
|
|
437
440
|
/**
|
|
438
441
|
* Compute next possible insert position from a given selection
|
|
439
442
|
*
|
|
440
|
-
* @param
|
|
441
|
-
* @returns
|
|
443
|
+
* @param selection - Reference selection
|
|
444
|
+
* @returns The next node insert caret selection, or null if none is available
|
|
442
445
|
*/
|
|
443
|
-
get_next_node_insert_caret(selection = this.selection) {
|
|
446
|
+
get_next_node_insert_caret(selection: Selection | null = this.selection): Selection | null {
|
|
444
447
|
// There's no parent path to insert into
|
|
445
448
|
if (!selection || selection.path.length <= 2) {
|
|
446
449
|
return null;
|
|
447
450
|
}
|
|
448
451
|
|
|
449
|
-
const node_offset =
|
|
452
|
+
const node_offset = (selection.path.at(-2) as number) + 1;
|
|
450
453
|
return {
|
|
451
454
|
type: 'node',
|
|
452
455
|
path: selection.path.slice(0, -2),
|
|
@@ -455,16 +458,16 @@ export default class Session {
|
|
|
455
458
|
};
|
|
456
459
|
}
|
|
457
460
|
|
|
458
|
-
get_selected_text() {
|
|
461
|
+
get_selected_text(): (Text & { nodes: Record<string, DocumentNode> }) | null {
|
|
459
462
|
if (this.selection?.type !== 'text') return null;
|
|
460
463
|
|
|
461
464
|
const selection_start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
462
465
|
const selection_end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
463
|
-
const text_value = this.get(this.selection.path);
|
|
466
|
+
const text_value: Text = this.get(this.selection.path);
|
|
464
467
|
const selected_text = char_slice(text_value.content, selection_start, selection_end);
|
|
465
|
-
const nodes = {};
|
|
468
|
+
const nodes: Record<string, DocumentNode> = {};
|
|
466
469
|
|
|
467
|
-
const clip_ranges = (ranges) =>
|
|
470
|
+
const clip_ranges = (ranges: Attachment[]) =>
|
|
468
471
|
ranges
|
|
469
472
|
.map((range) => {
|
|
470
473
|
if (selection_start < range.end_offset && selection_end > range.start_offset) {
|
|
@@ -486,7 +489,7 @@ export default class Session {
|
|
|
486
489
|
return null;
|
|
487
490
|
}
|
|
488
491
|
})
|
|
489
|
-
.filter(
|
|
492
|
+
.filter((range): range is Attachment => range !== null);
|
|
490
493
|
|
|
491
494
|
const marks = clip_ranges(text_value.marks);
|
|
492
495
|
const annotations = clip_ranges(text_value.annotations);
|
|
@@ -494,16 +497,21 @@ export default class Session {
|
|
|
494
497
|
return { content: selected_text, marks, annotations, nodes };
|
|
495
498
|
}
|
|
496
499
|
|
|
497
|
-
get_selected_annotated_nodes() {
|
|
500
|
+
get_selected_annotated_nodes(): {
|
|
501
|
+
nodes: Record<string, DocumentNode>;
|
|
502
|
+
main_nodes: NodeId[];
|
|
503
|
+
marks: Attachment[];
|
|
504
|
+
annotations: Attachment[];
|
|
505
|
+
} | null {
|
|
498
506
|
if (this.selection?.type !== 'node') return null;
|
|
499
507
|
|
|
500
508
|
const selection_start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
501
509
|
const selection_end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
502
|
-
const node_array = this.get(this.selection.path);
|
|
510
|
+
const node_array: NodeArray = this.get(this.selection.path);
|
|
503
511
|
const main_nodes = node_array.nodes.slice(selection_start, selection_end);
|
|
504
|
-
const nodes = {};
|
|
512
|
+
const nodes: Record<string, DocumentNode> = {};
|
|
505
513
|
|
|
506
|
-
const add_subgraph = (node_id) => {
|
|
514
|
+
const add_subgraph = (node_id: NodeId) => {
|
|
507
515
|
for (const node of this.traverse(node_id)) {
|
|
508
516
|
if (!nodes[node.id]) nodes[node.id] = node;
|
|
509
517
|
}
|
|
@@ -511,7 +519,7 @@ export default class Session {
|
|
|
511
519
|
|
|
512
520
|
for (const node_id of main_nodes) add_subgraph(node_id);
|
|
513
521
|
|
|
514
|
-
const clip_ranges = (ranges) =>
|
|
522
|
+
const clip_ranges = (ranges: Attachment[]) =>
|
|
515
523
|
ranges
|
|
516
524
|
.map((range) => {
|
|
517
525
|
if (selection_start >= range.end_offset || selection_end <= range.start_offset) {
|
|
@@ -521,11 +529,14 @@ export default class Session {
|
|
|
521
529
|
add_subgraph(range.node_id);
|
|
522
530
|
return {
|
|
523
531
|
start_offset: Math.max(range.start_offset - selection_start, 0),
|
|
524
|
-
end_offset: Math.min(
|
|
532
|
+
end_offset: Math.min(
|
|
533
|
+
range.end_offset - selection_start,
|
|
534
|
+
selection_end - selection_start
|
|
535
|
+
),
|
|
525
536
|
node_id: range.node_id
|
|
526
537
|
};
|
|
527
538
|
})
|
|
528
|
-
.filter(
|
|
539
|
+
.filter((range): range is Attachment => range !== null);
|
|
529
540
|
|
|
530
541
|
const marks = clip_ranges(node_array.marks);
|
|
531
542
|
const annotations = clip_ranges(node_array.annotations);
|
|
@@ -534,34 +545,34 @@ export default class Session {
|
|
|
534
545
|
}
|
|
535
546
|
|
|
536
547
|
// TODO: think about ways how we can also turn a node selection into plain text.
|
|
537
|
-
get_selected_plain_text() {
|
|
548
|
+
get_selected_plain_text(): string | null {
|
|
538
549
|
if (this.selection?.type !== 'text') return null;
|
|
539
550
|
|
|
540
551
|
const start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
541
552
|
const end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
542
|
-
const text = this.get(this.selection.path);
|
|
553
|
+
const text: Text = this.get(this.selection.path);
|
|
543
554
|
return char_slice(text.content, start, end);
|
|
544
555
|
}
|
|
545
556
|
|
|
546
|
-
get_selected_nodes() {
|
|
557
|
+
get_selected_nodes(): NodeId[] | null {
|
|
547
558
|
if (this.selection?.type !== 'node') return null;
|
|
548
559
|
|
|
549
560
|
const start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
550
561
|
const end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
551
|
-
const node_array = this.get(this.selection.path);
|
|
562
|
+
const node_array: NodeArray = this.get(this.selection.path);
|
|
552
563
|
// node_array.nodes is a plain array of id strings (doc is $state.raw),
|
|
553
564
|
// so a slice is already a safe fresh copy.
|
|
554
565
|
return node_array.nodes.slice(start, end);
|
|
555
566
|
}
|
|
556
567
|
|
|
557
|
-
select_parent() {
|
|
568
|
+
select_parent(): void {
|
|
558
569
|
if (!this.selection) return;
|
|
559
|
-
if (['text', 'property'].includes(this.selection
|
|
570
|
+
if (['text', 'property'].includes(this.selection.type)) {
|
|
560
571
|
// For text and property selections (e.g. ['page_1', 'body', 0, 'image']), we need to go up two levels
|
|
561
572
|
// in the path
|
|
562
573
|
if (this.selection.path.length > 3) {
|
|
563
574
|
const parent_path = this.selection.path.slice(0, -2);
|
|
564
|
-
const current_index =
|
|
575
|
+
const current_index = this.selection.path[this.selection.path.length - 2] as number;
|
|
565
576
|
this.selection = {
|
|
566
577
|
type: 'node',
|
|
567
578
|
path: parent_path,
|
|
@@ -575,7 +586,7 @@ export default class Session {
|
|
|
575
586
|
// For node selections, we go up one level
|
|
576
587
|
if (this.selection.path.length > 3) {
|
|
577
588
|
const parent_path = this.selection.path.slice(0, -2);
|
|
578
|
-
const current_index =
|
|
589
|
+
const current_index = this.selection.path[this.selection.path.length - 2] as number;
|
|
579
590
|
|
|
580
591
|
this.selection = {
|
|
581
592
|
type: 'node',
|
|
@@ -599,11 +610,9 @@ export default class Session {
|
|
|
599
610
|
* 2. Branch nodes second
|
|
600
611
|
* 3. Root node (entry point) last
|
|
601
612
|
*
|
|
602
|
-
*
|
|
603
|
-
* @returns {Array<DocumentNode>} Array of nodes in depth-first order
|
|
604
|
-
* @note Nodes that are not reachable from the entry point node will not be included
|
|
613
|
+
* Nodes that are not reachable from the entry point node will not be included.
|
|
605
614
|
*/
|
|
606
|
-
traverse(node_id) {
|
|
615
|
+
traverse(node_id: string): Array<DocumentNode> {
|
|
607
616
|
// doc is $state.raw, so doc.nodes is a plain (non-proxied) object —
|
|
608
617
|
// traverse() reads it directly and deep-clones each visited node.
|
|
609
618
|
// The previous $state.snapshot(this.doc.nodes) here deep-cloned the
|
|
@@ -616,10 +625,8 @@ export default class Session {
|
|
|
616
625
|
*
|
|
617
626
|
* We make a traversal to ensure that orphaned nodes are not included,
|
|
618
627
|
* and that leaf nodes go first, followed by branches and the root node at last.
|
|
619
|
-
*
|
|
620
|
-
* @returns {Document} The document
|
|
621
628
|
*/
|
|
622
|
-
to_json() {
|
|
629
|
+
to_json(): Document {
|
|
623
630
|
// this will order the nodes (depth-first traversal)
|
|
624
631
|
const nodes_array = this.traverse(this.document_id);
|
|
625
632
|
// convert nodes array to object with node IDs as keys
|
|
@@ -632,21 +639,17 @@ export default class Session {
|
|
|
632
639
|
|
|
633
640
|
// property_type('page', 'body') => 'node_array'
|
|
634
641
|
// property_type('paragraph', 'content') => 'text'
|
|
635
|
-
property_type(type, property) {
|
|
642
|
+
property_type(type: string, property: string): string {
|
|
636
643
|
return doc_property_type(this.schema, type, property);
|
|
637
644
|
}
|
|
638
645
|
|
|
639
646
|
// Count how many times a node is referenced in the document
|
|
640
|
-
count_references(node_id) {
|
|
647
|
+
count_references(node_id: NodeId): number {
|
|
641
648
|
return doc_count_references(this.schema, this.doc, node_id);
|
|
642
649
|
}
|
|
643
650
|
|
|
644
651
|
// Get all nodes referenced by a given node (recursively)
|
|
645
|
-
|
|
646
|
-
* @param {NodeId} node_id
|
|
647
|
-
* @returns {NodeId[]}
|
|
648
|
-
*/
|
|
649
|
-
get_referenced_nodes(node_id) {
|
|
652
|
+
get_referenced_nodes(node_id: NodeId): NodeId[] {
|
|
650
653
|
// Id-only traversal — no need to deep-clone the subgraph for ids.
|
|
651
654
|
return traverse_ids(node_id, this.schema, this.doc.nodes).slice(0, -1);
|
|
652
655
|
}
|