svedit 0.3.0 → 0.5.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 +43 -24
- package/dist/{AnnotatedStringProperty.svelte → AnnotatedTextProperty.svelte} +28 -40
- package/dist/AnnotatedTextProperty.svelte.d.ts +7 -0
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -0
- package/dist/Document.svelte.d.ts +56 -17
- package/dist/Document.svelte.d.ts.map +1 -1
- package/dist/Document.svelte.js +251 -102
- package/dist/Svedit.svelte +322 -223
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +16 -19
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +197 -104
- package/dist/commands.svelte.d.ts.map +1 -1
- package/dist/commands.svelte.js +18 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/types.d.ts +66 -21
- package/dist/util.d.ts +17 -16
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +67 -34
- package/package.json +21 -21
- package/src/lib/{AnnotatedStringProperty.svelte → AnnotatedTextProperty.svelte} +28 -40
- package/src/lib/Document.svelte.js +251 -102
- package/src/lib/Svedit.svelte +322 -223
- package/src/lib/Transaction.svelte.js +197 -104
- package/src/lib/commands.svelte.js +18 -15
- package/src/lib/index.js +1 -1
- package/src/lib/types.d.ts +66 -21
- package/src/lib/util.js +67 -34
- package/dist/AnnotatedStringProperty.svelte.d.ts +0 -7
- package/dist/AnnotatedStringProperty.svelte.d.ts.map +0 -1
package/dist/Svedit.svelte
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { setContext } from 'svelte';
|
|
3
3
|
import { snake_to_pascal, get_char_length, utf16_to_char_offset, char_to_utf16_offset, get_char_at, char_slice } from './util.js';
|
|
4
|
-
import { break_text_node,
|
|
4
|
+
import { break_text_node, insert_default_node, select_all } from './commands.svelte.js';
|
|
5
|
+
|
|
6
|
+
/** @import {
|
|
7
|
+
* SveditProps,
|
|
8
|
+
* DocumentPath,
|
|
9
|
+
* Selection,
|
|
10
|
+
* TextSelection,
|
|
11
|
+
* NodeSelection,
|
|
12
|
+
* PropertySelection,
|
|
13
|
+
* NodeId
|
|
14
|
+
* } from './types.d.ts';
|
|
15
|
+
*/
|
|
5
16
|
|
|
6
|
-
/** @import { SveditProps, DocumentPath, Selection, TextSelection, NodeSelection, PropertySelection, NodeId } from './types.d.ts'; */
|
|
7
17
|
/** @type {SveditProps} */
|
|
8
18
|
let {
|
|
9
19
|
doc,
|
|
10
20
|
editable = $bindable(false),
|
|
11
21
|
path,
|
|
12
22
|
class: css_class,
|
|
23
|
+
autocapitalize = "on",
|
|
24
|
+
spellcheck = "true",
|
|
13
25
|
} = $props();
|
|
14
26
|
|
|
15
27
|
let canvas_ref;
|
|
@@ -19,21 +31,32 @@
|
|
|
19
31
|
|
|
20
32
|
// Track temporary disabled onkeydown events (e.g. during character composition)
|
|
21
33
|
let skip_onkeydown = false;
|
|
22
|
-
|
|
23
34
|
let is_composing = $state(false);
|
|
24
|
-
|
|
25
35
|
let input_selection = undefined;
|
|
26
36
|
|
|
27
37
|
// Detect Chrome on desktop (not mobile) - only available in browser
|
|
28
38
|
let is_chrome_desktop = $state(false);
|
|
29
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Detect if the current browser is on a mobile device
|
|
42
|
+
* @returns {boolean} true if mobile browser, false otherwise
|
|
43
|
+
*/
|
|
44
|
+
function is_mobile_browser() {
|
|
45
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const user_agent = navigator.userAgent;
|
|
50
|
+
return /iPhone|iPad|iPod|Android|Mobile/i.test(user_agent) ||
|
|
51
|
+
('ontouchstart' in window) ||
|
|
52
|
+
(navigator.maxTouchPoints > 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
$effect(() => {
|
|
31
56
|
if (typeof window !== 'undefined' && typeof navigator !== 'undefined') {
|
|
32
57
|
const user_agent = navigator.userAgent;
|
|
33
58
|
const is_chrome = user_agent.includes('Chrome') && !user_agent.includes('Edg');
|
|
34
|
-
const is_mobile =
|
|
35
|
-
('ontouchstart' in window) ||
|
|
36
|
-
(navigator.maxTouchPoints > 0);
|
|
59
|
+
const is_mobile = is_mobile_browser();
|
|
37
60
|
is_chrome_desktop = is_chrome && !is_mobile;
|
|
38
61
|
}
|
|
39
62
|
});
|
|
@@ -86,7 +109,7 @@
|
|
|
86
109
|
|
|
87
110
|
function commit_input() {
|
|
88
111
|
if (input_selection?.type !== 'text') return;
|
|
89
|
-
const model_text = doc.get(input_selection.path)
|
|
112
|
+
const model_text = doc.get(input_selection.path).text;
|
|
90
113
|
const dom_text_element = document.querySelector(`[data-path="${input_selection.path.join('.')}"]`);
|
|
91
114
|
const dom_text = dom_text_element.textContent;
|
|
92
115
|
const op = diff_text(model_text, dom_text);
|
|
@@ -142,25 +165,43 @@
|
|
|
142
165
|
|
|
143
166
|
// Only take input when in a valid text selection inside the canvas
|
|
144
167
|
if (
|
|
145
|
-
!(canvas_ref?.contains(document.activeElement))
|
|
146
|
-
(doc.selection?.type !== 'text')
|
|
168
|
+
!(canvas_ref?.contains(document.activeElement))
|
|
147
169
|
) {
|
|
148
170
|
event.preventDefault();
|
|
149
171
|
return;
|
|
150
172
|
}
|
|
151
173
|
|
|
152
|
-
if (event.inputType === 'formatBold') {
|
|
174
|
+
if (event.inputType === 'formatBold' && doc.selection?.type === 'text') {
|
|
153
175
|
doc.apply(doc.tr.annotate_text('strong'));
|
|
154
176
|
event.preventDefault();
|
|
155
177
|
event.stopPropagation();
|
|
156
178
|
}
|
|
157
179
|
|
|
158
|
-
if (event.inputType === 'formatItalic') {
|
|
180
|
+
if (event.inputType === 'formatItalic' && doc.selection?.type === 'text') {
|
|
159
181
|
doc.apply(doc.tr.annotate_text('emphasis'));
|
|
160
182
|
event.preventDefault();
|
|
161
183
|
event.stopPropagation();
|
|
162
184
|
}
|
|
163
185
|
|
|
186
|
+
// NOTE: underline doesn't make much sense as a semantic annotation,
|
|
187
|
+
// so we rewire `cmd + u` to toggle highlights
|
|
188
|
+
if (event.inputType === 'formatUnderline' && doc.selection?.type === 'text') {
|
|
189
|
+
doc.apply(doc.tr.annotate_text('highlight'));
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
event.stopPropagation();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (event.inputType === 'deleteContentBackward') {
|
|
195
|
+
doc.apply(doc.tr.delete_selection('backward'));
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
event.stopPropagation();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (event.inputType === 'deleteContentForward') {
|
|
201
|
+
doc.apply(doc.tr.delete_selection('forward'));
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
event.stopPropagation();
|
|
204
|
+
}
|
|
164
205
|
|
|
165
206
|
// We remember the "input selection", because in some mobile browsers (Samsung Android) the onbeforeinput event
|
|
166
207
|
// is fired on a different selection than the input event.
|
|
@@ -295,29 +336,47 @@ ${fallback_html}`;
|
|
|
295
336
|
* @param {Object} node - Node object
|
|
296
337
|
* @returns {string} HTML representation
|
|
297
338
|
*/
|
|
298
|
-
function
|
|
339
|
+
function default_node_html_exporter(node, doc, html_exporters) {
|
|
299
340
|
let html = '';
|
|
300
|
-
|
|
341
|
+
const node_schema = doc.schema[node.type];
|
|
301
342
|
|
|
302
343
|
for (const [prop_name, prop_value] of Object.entries(node)) {
|
|
303
344
|
if (prop_name === 'id' || prop_name === 'type') continue;
|
|
304
|
-
|
|
305
|
-
// Check if this is an
|
|
306
|
-
if (
|
|
307
|
-
const text_content = prop_value
|
|
345
|
+
const property_definition = node_schema.properties[prop_name];
|
|
346
|
+
// Check if this is an annotated_text property (object with text property)
|
|
347
|
+
if (property_definition.type === 'annotated_text') {
|
|
348
|
+
const text_content = prop_value.text;
|
|
308
349
|
if (text_content.trim()) {
|
|
309
|
-
html += `<p>${text_content}</p>\n`;
|
|
310
|
-
|
|
350
|
+
html += `<p>${text_content.trim()}</p>\n`;
|
|
351
|
+
}
|
|
352
|
+
} else if (property_definition.type === 'node_array') {
|
|
353
|
+
for (const child_id of prop_value) {
|
|
354
|
+
const child = doc.get(child_id);
|
|
355
|
+
const child_exporter = html_exporters[child.type] || default_node_html_exporter;
|
|
356
|
+
html += child_exporter(child, doc, html_exporters);
|
|
311
357
|
}
|
|
312
358
|
}
|
|
313
359
|
}
|
|
314
360
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
361
|
+
return html;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function default_node_plain_text_exporter(node) {
|
|
365
|
+
let plain_text = '';
|
|
366
|
+
|
|
367
|
+
for (const [prop_name, prop_value] of Object.entries(node)) {
|
|
368
|
+
if (prop_name === 'id' || prop_name === 'type') continue;
|
|
369
|
+
|
|
370
|
+
// Check if this is an annotated_text property (object with text property)
|
|
371
|
+
if (typeof prop_value === 'object' && prop_value !== null && typeof prop_value.text === 'string') {
|
|
372
|
+
const text_content = prop_value.text;
|
|
373
|
+
if (text_content.trim()) {
|
|
374
|
+
plain_text += `${text_content.trim()}\n\n`;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
318
377
|
}
|
|
319
378
|
|
|
320
|
-
return
|
|
379
|
+
return plain_text;
|
|
321
380
|
}
|
|
322
381
|
|
|
323
382
|
/**
|
|
@@ -333,14 +392,22 @@ ${fallback_html}`;
|
|
|
333
392
|
|
|
334
393
|
if (html_exporters[node.type]) {
|
|
335
394
|
// Use custom exporter for this node type
|
|
336
|
-
html += html_exporters[node.type](node, doc);
|
|
395
|
+
html += html_exporters[node.type](node, doc, html_exporters);
|
|
337
396
|
} else {
|
|
338
397
|
// Use default exporter
|
|
339
|
-
html +=
|
|
398
|
+
html += default_node_html_exporter(node, doc, html_exporters);
|
|
340
399
|
}
|
|
341
400
|
}
|
|
401
|
+
return html;
|
|
402
|
+
}
|
|
342
403
|
|
|
343
|
-
|
|
404
|
+
function export_plain_text(nodes) {
|
|
405
|
+
let plain_text = '';
|
|
406
|
+
|
|
407
|
+
for (const node of nodes) {
|
|
408
|
+
plain_text += default_node_plain_text_exporter(node);
|
|
409
|
+
}
|
|
410
|
+
return plain_text.trim();
|
|
344
411
|
}
|
|
345
412
|
|
|
346
413
|
/**
|
|
@@ -376,20 +443,29 @@ ${fallback_html}`;
|
|
|
376
443
|
event.preventDefault();
|
|
377
444
|
event.stopPropagation();
|
|
378
445
|
|
|
379
|
-
let plain_text, html;
|
|
446
|
+
let plain_text, annotated_text, html;
|
|
380
447
|
|
|
381
448
|
if (doc.selection?.type === 'text') {
|
|
382
449
|
plain_text = doc.get_selected_plain_text();
|
|
383
|
-
|
|
450
|
+
annotated_text = doc.get_selected_annotated_text();
|
|
451
|
+
const fallback_html = `<span>${annotated_text.text}</span>`;
|
|
452
|
+
|
|
453
|
+
console.log('Text copy:', {
|
|
454
|
+
annotated_text,
|
|
455
|
+
plain_text,
|
|
456
|
+
html,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
html = create_svedit_html_format(annotated_text, fallback_html);
|
|
384
460
|
} else if (doc.selection?.type === 'node') {
|
|
385
461
|
const selected_nodes = doc.get_selected_nodes();
|
|
386
462
|
const { nodes, main_nodes } = prepare_copy_payload(selected_nodes);
|
|
387
463
|
|
|
388
464
|
const json_data = { nodes, main_nodes };
|
|
389
465
|
|
|
390
|
-
console.log('
|
|
466
|
+
console.log('Node copy:', {
|
|
391
467
|
selected_nodes,
|
|
392
|
-
|
|
468
|
+
nodes,
|
|
393
469
|
total_nodes: Object.keys(nodes).length,
|
|
394
470
|
operation: delete_selection ? 'cut' : 'copy'
|
|
395
471
|
});
|
|
@@ -400,30 +476,30 @@ ${fallback_html}`;
|
|
|
400
476
|
|
|
401
477
|
// Create HTML with embedded svedit data
|
|
402
478
|
html = create_svedit_html_format(json_data, fallback_html);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
479
|
+
// Generate plain text representation
|
|
480
|
+
plain_text = export_plain_text(selected_node_objects);
|
|
481
|
+
} else if (doc.selection?.type === 'property') {
|
|
482
|
+
const property_definition = doc.inspect(doc.selection.path);
|
|
483
|
+
const value = doc.get(doc.selection.path);
|
|
484
|
+
const json_data = {
|
|
485
|
+
kind: 'property',
|
|
486
|
+
name: property_definition.name,
|
|
487
|
+
type: property_definition.type,
|
|
488
|
+
value,
|
|
489
|
+
};
|
|
490
|
+
console.log('Property copy:', json_data);
|
|
491
|
+
html = create_svedit_html_format(json_data, `<span>${value}</span>`);
|
|
492
|
+
plain_text = String(value);
|
|
411
493
|
}
|
|
412
494
|
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
'text/plain'
|
|
416
|
-
'text/html'
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
const clipboard_item_raw = new ClipboardItem(data);
|
|
420
|
-
|
|
421
|
-
// Write to clipboard
|
|
422
|
-
navigator.clipboard.write([clipboard_item_raw]).then(() => {
|
|
495
|
+
// Write to clipboard using event.clipboardData
|
|
496
|
+
try {
|
|
497
|
+
event.clipboardData?.setData('text/plain', plain_text || '');
|
|
498
|
+
event.clipboardData?.setData('text/html', html || '');
|
|
423
499
|
console.log('Data copied to clipboard successfully');
|
|
424
|
-
}
|
|
500
|
+
} catch (err) {
|
|
425
501
|
console.error('Failed to copy data: ', err);
|
|
426
|
-
}
|
|
502
|
+
}
|
|
427
503
|
|
|
428
504
|
if (delete_selection) {
|
|
429
505
|
doc.apply(doc.tr.delete_selection());
|
|
@@ -434,83 +510,163 @@ ${fallback_html}`;
|
|
|
434
510
|
oncopy(event, true);
|
|
435
511
|
}
|
|
436
512
|
|
|
437
|
-
async function onpaste(event) {
|
|
438
|
-
// Only handle paste events if focus is within the canvas
|
|
439
|
-
if (!canvas_ref?.contains(document.activeElement)) return;
|
|
440
|
-
|
|
441
|
-
event.preventDefault();
|
|
442
|
-
const clipboardItems = await navigator.clipboard.read();
|
|
443
513
|
|
|
444
|
-
|
|
514
|
+
/**
|
|
515
|
+
* Attempts to paste JSON data as a node at the current selection.
|
|
516
|
+
*
|
|
517
|
+
* @param {string|object} pasted_json - The JSON data to paste, either as a string or parsed object
|
|
518
|
+
* @param {Selection} [selection] - Optional selection (node cursor) where the payload should be pasted
|
|
519
|
+
* @returns {boolean} True if the paste operation was successful, false otherwise
|
|
520
|
+
*/
|
|
521
|
+
function try_node_paste(pasted_json, selection) {
|
|
522
|
+
const { nodes, main_nodes } = pasted_json;
|
|
445
523
|
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
} catch (e) {
|
|
452
|
-
console.log('No HTML format available or failed to extract svedit data:', e);
|
|
453
|
-
pasted_json = undefined;
|
|
524
|
+
// NOTE: At this point, nodes contains a subgraph from the copy
|
|
525
|
+
// with original ids.
|
|
526
|
+
let tr = doc.tr;
|
|
527
|
+
if (selection) {
|
|
528
|
+
tr.set_selection(selection);
|
|
454
529
|
}
|
|
455
530
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
531
|
+
// We can safely assume we're dealing with a node_array property
|
|
532
|
+
const property_definition = doc.inspect(tr.selection.path);
|
|
533
|
+
const first_compatible_text_node_type = property_definition.node_types.find(type => doc.kind({ type }) === 'text');
|
|
534
|
+
|
|
535
|
+
const nodes_to_insert = [];
|
|
536
|
+
let rejected = false;
|
|
537
|
+
for (const node_id of main_nodes) {
|
|
538
|
+
const node = nodes[node_id];
|
|
539
|
+
if (!property_definition.node_types.includes(node.type)) {
|
|
540
|
+
// Incompatible node type detected
|
|
541
|
+
if (doc.kind(node) === 'text' && first_compatible_text_node_type) {
|
|
542
|
+
const new_node_id = tr.build('the_node', {
|
|
543
|
+
the_node: {
|
|
544
|
+
id: 'the_node',
|
|
545
|
+
type: first_compatible_text_node_type,
|
|
546
|
+
content: node.content,
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
nodes_to_insert.push(new_node_id);
|
|
550
|
+
} else {
|
|
551
|
+
console.log(`rejected ${node.type}. Only ${property_definition.node_types.join(', ')} allowed.`);
|
|
552
|
+
rejected = true;
|
|
553
|
+
break;
|
|
469
554
|
}
|
|
555
|
+
} else {
|
|
556
|
+
const new_node_id = tr.build(node_id, nodes);
|
|
557
|
+
nodes_to_insert.push(new_node_id);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
470
560
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const prop_type = doc.schema[node.type]?.[property]?.type;
|
|
482
|
-
|
|
483
|
-
if (prop_type === 'node_array' && Array.isArray(value)) {
|
|
484
|
-
new_node[property] = value.map(ref_id => id_mapping[ref_id] || ref_id);
|
|
485
|
-
} else if (prop_type === 'node' && typeof value === 'string') {
|
|
486
|
-
new_node[property] = id_mapping[value] || value;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
updated_nodes[new_node.id] = new_node;
|
|
561
|
+
if (!rejected) {
|
|
562
|
+
tr.insert_nodes(nodes_to_insert);
|
|
563
|
+
doc.apply(tr);
|
|
564
|
+
return true;
|
|
565
|
+
} else {
|
|
566
|
+
if (tr.selection.path.length >= 2) {
|
|
567
|
+
const next_node_insert_cursor = doc.get_next_node_insert_cursor(tr.selection);
|
|
568
|
+
if (next_node_insert_cursor) {
|
|
569
|
+
try_node_paste(pasted_json, next_node_insert_cursor);
|
|
491
570
|
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
492
575
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
576
|
+
async function onpaste(event) {
|
|
577
|
+
// Only handle paste events if focus is within the canvas
|
|
578
|
+
if (!canvas_ref?.contains(document.activeElement)) return;
|
|
579
|
+
event.preventDefault();
|
|
497
580
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
581
|
+
let plain_text, pasted_json, pasted_images = [];
|
|
582
|
+
|
|
583
|
+
// NOTE: For some reason, await navigator.clipboard.read()
|
|
584
|
+
const clipboard_items = event.clipboardData?.items || [];
|
|
585
|
+
for (const item of clipboard_items || []) {
|
|
586
|
+
if (item.type.startsWith("image/")) {
|
|
587
|
+
const blob = item.getAsFile();
|
|
588
|
+
const data_url = URL.createObjectURL(blob);
|
|
589
|
+
pasted_images.push({
|
|
590
|
+
blob,
|
|
591
|
+
data_url,
|
|
592
|
+
type: item.type,
|
|
593
|
+
size: blob.size
|
|
594
|
+
});
|
|
502
595
|
}
|
|
596
|
+
}
|
|
503
597
|
|
|
504
|
-
|
|
598
|
+
if (pasted_images.length > 0) {
|
|
599
|
+
pasted_json = await doc.config.handle_image_paste(doc, pasted_images);
|
|
600
|
+
console.log('pasted_json_after_image_paste', pasted_json);
|
|
601
|
+
// NOTE: If no pasted_json is returned from the custom handler, we assume that content creation has been
|
|
602
|
+
// handled inside handle_image_paste already.
|
|
603
|
+
if (!pasted_json) return;
|
|
505
604
|
} else {
|
|
506
|
-
//
|
|
605
|
+
// First try to extract svedit data from HTML format
|
|
507
606
|
try {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
607
|
+
const html_content = event.clipboardData?.getData('text/html');
|
|
608
|
+
if (html_content) {
|
|
609
|
+
pasted_json = extract_svedit_data_from_html(html_content);
|
|
610
|
+
}
|
|
611
|
+
} catch (e) {
|
|
612
|
+
console.log('No HTML format available or failed to extract svedit data:', e);
|
|
613
|
+
pasted_json = undefined;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
plain_text = event.clipboardData?.getData('text/plain');
|
|
511
618
|
} catch (e) {
|
|
512
619
|
console.error('Failed to paste any content:', e);
|
|
513
620
|
}
|
|
621
|
+
|
|
622
|
+
// Try to contruct a node payload from plain text when applicable
|
|
623
|
+
if (!pasted_json && typeof plain_text === 'string') {
|
|
624
|
+
const plain_text_fragments = plain_text.split('\n\n').map(fragment => fragment.trim()).filter(Boolean);
|
|
625
|
+
if (plain_text_fragments.length > 1) {
|
|
626
|
+
pasted_json = {
|
|
627
|
+
main_nodes: [],
|
|
628
|
+
nodes: [],
|
|
629
|
+
};
|
|
630
|
+
for (let i = 0; i < plain_text_fragments.length; i++) {
|
|
631
|
+
const fragment = plain_text_fragments[i];
|
|
632
|
+
pasted_json.nodes["fragment_" + i] = {
|
|
633
|
+
id: "fragment_" + i,
|
|
634
|
+
type: 'text',
|
|
635
|
+
content: {
|
|
636
|
+
text: fragment,
|
|
637
|
+
annotations: []
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
pasted_json.main_nodes.push("fragment_" + i);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (pasted_json?.main_nodes && doc.selection?.type === 'node') {
|
|
647
|
+
// Paste nodes at a node selection
|
|
648
|
+
try_node_paste(pasted_json);
|
|
649
|
+
} else if (pasted_json?.kind === 'property' && doc.selection?.type === 'property') {
|
|
650
|
+
const property_definition = doc.inspect(doc.selection.path);
|
|
651
|
+
if (property_definition.type === pasted_json.type) {
|
|
652
|
+
doc.apply(doc.tr.set(doc.selection.path, pasted_json.value));
|
|
653
|
+
}
|
|
654
|
+
} else if (doc.selection?.type === 'text' && pasted_json?.text) {
|
|
655
|
+
// Paste text at a text selection
|
|
656
|
+
doc.apply(doc.tr.insert_text(pasted_json.text, pasted_json.annotations, pasted_json.nodes));
|
|
657
|
+
} else if (doc.selection?.type === 'text' && pasted_json?.main_nodes?.length === 1 && doc.kind(pasted_json?.nodes[pasted_json.main_nodes[0]]) === 'text') {
|
|
658
|
+
// Paste a single text node, at a text cursor
|
|
659
|
+
const text_property = pasted_json.nodes[pasted_json.main_nodes[0]].content;
|
|
660
|
+
doc.apply(doc.tr.insert_text(text_property.text, text_property.annotations, pasted_json.nodes));
|
|
661
|
+
} else if (['text', 'property'].includes(doc.selection?.type) && pasted_json?.nodes) {
|
|
662
|
+
// Paste nodes at a text or property selection by finding the next valid insert cursor
|
|
663
|
+
const next_node_insert_cursor = doc.get_next_node_insert_cursor(doc.selection);
|
|
664
|
+
try_node_paste(pasted_json, next_node_insert_cursor);
|
|
665
|
+
} else if (typeof plain_text === 'string') {
|
|
666
|
+
// External paste: Fallback to plain text when no svedit data is found
|
|
667
|
+
doc.apply(doc.tr.insert_text(plain_text.trim()));
|
|
668
|
+
} else {
|
|
669
|
+
console.log('Could not paste.');
|
|
514
670
|
}
|
|
515
671
|
}
|
|
516
672
|
|
|
@@ -588,47 +744,47 @@ ${fallback_html}`;
|
|
|
588
744
|
const is_collapsed = selection?.anchor_offset === selection?.focus_offset;
|
|
589
745
|
|
|
590
746
|
if (
|
|
591
|
-
(e.key === 'ArrowRight' && e.altKey && e.ctrlKey && doc.
|
|
592
|
-
(e.key === 'ArrowRight' && e.altKey && e.ctrlKey && e.shiftKey && doc.
|
|
747
|
+
(e.key === 'ArrowRight' && e.altKey && e.ctrlKey && doc.layout_node) ||
|
|
748
|
+
(e.key === 'ArrowRight' && e.altKey && e.ctrlKey && e.shiftKey && doc.layout_node)
|
|
593
749
|
) {
|
|
594
|
-
const node = doc.
|
|
750
|
+
const node = doc.layout_node;
|
|
595
751
|
const layout_count = doc.config.node_layouts[node.type];
|
|
596
752
|
|
|
597
753
|
if (layout_count > 1 && node?.layout) {
|
|
598
754
|
const next_layout = (node.layout % layout_count) + 1;
|
|
599
755
|
console.log('layout / count / next_layout', node.layout, layout_count, next_layout);
|
|
600
756
|
const tr = doc.tr;
|
|
601
|
-
tr.set([doc.
|
|
757
|
+
tr.set([doc.layout_node?.id, 'layout'], next_layout);
|
|
602
758
|
doc.apply(tr);
|
|
603
759
|
}
|
|
760
|
+
e.preventDefault();
|
|
604
761
|
} else if (
|
|
605
|
-
(e.key === 'ArrowLeft' && e.altKey && e.ctrlKey && doc.
|
|
606
|
-
(e.key === 'ArrowLeft' && e.altKey && e.ctrlKey && e.shiftKey && doc.
|
|
762
|
+
(e.key === 'ArrowLeft' && e.altKey && e.ctrlKey && doc.layout_node) ||
|
|
763
|
+
(e.key === 'ArrowLeft' && e.altKey && e.ctrlKey && e.shiftKey && doc.layout_node)
|
|
607
764
|
) {
|
|
608
|
-
const node = doc.
|
|
765
|
+
const node = doc.layout_node;
|
|
609
766
|
const layout_count = doc.config.node_layouts[node.type];
|
|
767
|
+
|
|
610
768
|
if (layout_count > 1 && node?.layout) {
|
|
611
769
|
const prev_layout = ((node.layout - 2 + layout_count) % layout_count) + 1;
|
|
612
770
|
const tr = doc.tr;
|
|
613
|
-
tr.set([doc.
|
|
771
|
+
tr.set([doc.layout_node?.id, 'layout'], prev_layout);
|
|
614
772
|
doc.apply(tr);
|
|
615
773
|
console.log('layout / count / prev_layout', node.layout, layout_count, prev_layout);
|
|
616
774
|
}
|
|
775
|
+
e.preventDefault();
|
|
617
776
|
} else if (
|
|
618
|
-
(e.key === 'ArrowDown' && e.altKey && e.ctrlKey && doc.
|
|
619
|
-
(e.key === 'ArrowDown' && e.altKey && e.ctrlKey && e.shiftKey && doc.
|
|
777
|
+
(e.key === 'ArrowDown' && e.altKey && e.ctrlKey && doc.layout_node) ||
|
|
778
|
+
(e.key === 'ArrowDown' && e.altKey && e.ctrlKey && e.shiftKey && doc.layout_node )
|
|
620
779
|
) {
|
|
621
|
-
const node = doc.
|
|
622
|
-
|
|
780
|
+
const node = doc.layout_node;
|
|
623
781
|
if (doc.selection.type !== 'node') {
|
|
624
782
|
doc.select_parent();
|
|
625
783
|
}
|
|
626
784
|
const old_selection = { ...doc.selection };
|
|
627
|
-
|
|
628
785
|
const node_array_schema = doc.inspect(doc.selection.path);
|
|
629
786
|
// If we are not dealing with a node selection in a container, return
|
|
630
787
|
if (node_array_schema.type !== 'node_array') return;
|
|
631
|
-
|
|
632
788
|
const current_type_index = node_array_schema.node_types.indexOf(node.type);
|
|
633
789
|
const next_type_index = (current_type_index + 1) % node_array_schema.node_types.length;
|
|
634
790
|
const next_type = node_array_schema.node_types[next_type_index];
|
|
@@ -636,31 +792,27 @@ ${fallback_html}`;
|
|
|
636
792
|
doc.config.inserters[next_type](tr);
|
|
637
793
|
tr.set_selection(old_selection);
|
|
638
794
|
doc.apply(tr);
|
|
795
|
+
e.preventDefault();
|
|
639
796
|
} else if (
|
|
640
|
-
(e.key === 'ArrowUp' && e.altKey && e.ctrlKey && doc.
|
|
641
|
-
(e.key === 'ArrowUp' && e.altKey && e.ctrlKey && e.shiftKey && doc.
|
|
797
|
+
(e.key === 'ArrowUp' && e.altKey && e.ctrlKey && doc.layout_node) ||
|
|
798
|
+
(e.key === 'ArrowUp' && e.altKey && e.ctrlKey && e.shiftKey && doc.layout_node)
|
|
642
799
|
) {
|
|
643
|
-
const node = doc.
|
|
644
|
-
|
|
800
|
+
const node = doc.layout_node;
|
|
645
801
|
if (doc.selection.type !== 'node') {
|
|
646
802
|
doc.select_parent();
|
|
647
803
|
}
|
|
648
|
-
|
|
649
|
-
// if (selection.type !== 'node') return;
|
|
650
804
|
const old_selection = { ...doc.selection };
|
|
651
805
|
const node_array_schema = doc.inspect(doc.selection.path);
|
|
652
806
|
// If we are not dealing with a node selection in a container, return
|
|
653
807
|
if (node_array_schema.type !== 'node_array') return;
|
|
654
|
-
|
|
655
808
|
const current_type_index = node_array_schema.node_types.indexOf(node.type);
|
|
656
|
-
// return (current_layout_number - 1 + layout_count) % layout_count;
|
|
657
809
|
const prev_type_index = (current_type_index - 1 + node_array_schema.node_types.length) % node_array_schema.node_types.length;
|
|
658
810
|
const prev_type = node_array_schema.node_types[prev_type_index];
|
|
659
811
|
const tr = doc.tr;
|
|
660
812
|
doc.config.inserters[prev_type](tr);
|
|
661
813
|
tr.set_selection(old_selection);
|
|
662
814
|
doc.apply(tr);
|
|
663
|
-
|
|
815
|
+
e.preventDefault();
|
|
664
816
|
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
|
|
665
817
|
const tr = doc.tr;
|
|
666
818
|
if (select_all(tr)) {
|
|
@@ -676,76 +828,32 @@ ${fallback_html}`;
|
|
|
676
828
|
doc.redo();
|
|
677
829
|
e.preventDefault();
|
|
678
830
|
e.stopPropagation();
|
|
679
|
-
} else if (e.key === 'Enter' && (e.shiftKey)) {
|
|
831
|
+
} else if (e.key === 'Enter' && (e.shiftKey) && !is_mobile_browser() && selection?.type === 'text' && doc.inspect(selection.path).allow_newlines) {
|
|
680
832
|
doc.apply(doc.tr.insert_text('\n'));
|
|
681
833
|
e.preventDefault();
|
|
682
834
|
e.stopPropagation();
|
|
683
|
-
} else if (e.key === '
|
|
684
|
-
doc.apply(doc.tr.annotate_text('
|
|
685
|
-
href: window.prompt('Enter the URL', 'https://example.com')
|
|
686
|
-
}));
|
|
835
|
+
} else if (e.key === 'b' && (e.ctrlKey || e.metaKey) && selection?.type === 'text') {
|
|
836
|
+
doc.apply(doc.tr.annotate_text('strong'));
|
|
687
837
|
e.preventDefault();
|
|
688
838
|
e.stopPropagation();
|
|
689
|
-
} else if (e.key === '
|
|
690
|
-
|
|
691
|
-
// For property selections, clear the property value only if it's not already falsy
|
|
692
|
-
if (doc.get(selection.path)) {
|
|
693
|
-
const tr = doc.tr;
|
|
694
|
-
tr.set(selection.path, '');
|
|
695
|
-
doc.apply(tr);
|
|
696
|
-
}
|
|
697
|
-
} else {
|
|
698
|
-
if (doc.selection?.type === 'text' && doc.selection?.anchor_offset === 0 && doc.selection?.focus_offset === 0) {
|
|
699
|
-
const tr = doc.tr;
|
|
700
|
-
join_text_node(tr);
|
|
701
|
-
doc.apply(tr);
|
|
702
|
-
} else {
|
|
703
|
-
// TODO: Things are not yet properly composable, I think we need to
|
|
704
|
-
// realize delete_selection as a command as well.
|
|
705
|
-
// See https://prosemirror.net/docs/guide/#commands for inspiration.
|
|
706
|
-
// For other selections, use the normal delete behavior
|
|
707
|
-
doc.apply(doc.tr.delete_selection());
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
839
|
+
} else if (e.key === 'i' && (e.ctrlKey || e.metaKey) && selection?.type === 'text') {
|
|
840
|
+
doc.apply(doc.tr.annotate_text('emphasis'));
|
|
711
841
|
e.preventDefault();
|
|
712
842
|
e.stopPropagation();
|
|
713
|
-
} else if (e.key === '
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
});
|
|
728
|
-
tr.delete_selection();
|
|
729
|
-
doc.apply(tr);
|
|
730
|
-
} else {
|
|
731
|
-
// At end of text - try to join with next text node
|
|
732
|
-
const tr = doc.tr;
|
|
733
|
-
const node_index = parseInt(selection.path.at(-2), 10);
|
|
734
|
-
const successor_node = doc.get([...selection.path.slice(0, -2), node_index + 1]);
|
|
735
|
-
|
|
736
|
-
// Check if next node is a text node
|
|
737
|
-
if (successor_node && doc.kind(successor_node) === 'text') {
|
|
738
|
-
// Set selection to beginning of next text node
|
|
739
|
-
tr.set_selection({
|
|
740
|
-
type: 'text',
|
|
741
|
-
path: [...selection.path.slice(0, -2), node_index + 1, 'content'],
|
|
742
|
-
anchor_offset: 0,
|
|
743
|
-
focus_offset: 0
|
|
744
|
-
});
|
|
745
|
-
// Use join_text_node to merge with previous
|
|
746
|
-
join_text_node(tr);
|
|
747
|
-
doc.apply(tr);
|
|
748
|
-
}
|
|
843
|
+
} else if (e.key === 'u' && (e.ctrlKey || e.metaKey) && selection?.type === 'text') {
|
|
844
|
+
doc.apply(doc.tr.annotate_text('highlight'));
|
|
845
|
+
e.preventDefault();
|
|
846
|
+
e.stopPropagation();
|
|
847
|
+
} else if (e.key === 'k' && (e.ctrlKey || e.metaKey)) {
|
|
848
|
+
const has_link = doc.active_annotation('link');
|
|
849
|
+
if (has_link) {
|
|
850
|
+
// Delete link
|
|
851
|
+
doc.apply(doc.tr.annotate_text('link'));
|
|
852
|
+
} else {
|
|
853
|
+
// Create link
|
|
854
|
+
const href = window.prompt('Enter the URL', 'https://example.com');
|
|
855
|
+
if (href) {
|
|
856
|
+
doc.apply(doc.tr.annotate_text('link', { href }));
|
|
749
857
|
}
|
|
750
858
|
}
|
|
751
859
|
|
|
@@ -760,16 +868,10 @@ ${fallback_html}`;
|
|
|
760
868
|
const span_length = Math.abs(selection.focus_offset - selection.anchor_offset);
|
|
761
869
|
|
|
762
870
|
if (is_collapsed) {
|
|
763
|
-
//
|
|
871
|
+
// Always insert default node on ENTER
|
|
764
872
|
const tr = doc.tr;
|
|
765
873
|
insert_default_node(tr);
|
|
766
874
|
doc.apply(tr);
|
|
767
|
-
// if (insert_default_node(tr)) {
|
|
768
|
-
// doc.apply(tr);
|
|
769
|
-
// } else {
|
|
770
|
-
// // Fall back to focusing toolbar when multiple types are available
|
|
771
|
-
// focus_toolbar();
|
|
772
|
-
// }
|
|
773
875
|
} else if (span_length === 1) {
|
|
774
876
|
focus_toolbar();
|
|
775
877
|
}
|
|
@@ -1023,7 +1125,7 @@ ${fallback_html}`;
|
|
|
1023
1125
|
// 2. Last child node is a <br> (the visual representation of that \n)
|
|
1024
1126
|
// 3. Cursor is positioned at or near the end
|
|
1025
1127
|
// And returns the correct text.length position instead.
|
|
1026
|
-
const text_content = doc.get(path)
|
|
1128
|
+
const text_content = doc.get(path).text;
|
|
1027
1129
|
const text_length = get_char_length(text_content);
|
|
1028
1130
|
if (text_length > 0) {
|
|
1029
1131
|
const last_char = get_char_at(text_content, text_length - 1);
|
|
@@ -1184,7 +1286,7 @@ ${fallback_html}`;
|
|
|
1184
1286
|
(is_backward ? focus_node : anchor_node).scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1185
1287
|
}, 0);
|
|
1186
1288
|
} else {
|
|
1187
|
-
console.log('
|
|
1289
|
+
console.log('No container element found!');
|
|
1188
1290
|
}
|
|
1189
1291
|
}
|
|
1190
1292
|
|
|
@@ -1193,7 +1295,6 @@ ${fallback_html}`;
|
|
|
1193
1295
|
// The element that holds the property
|
|
1194
1296
|
const el = canvas_ref.querySelector(`[data-path="${selection.path.join('.')}"][data-type="property"]`);
|
|
1195
1297
|
const cursor_trap_selectable = el.querySelector('.svedit-selectable');
|
|
1196
|
-
|
|
1197
1298
|
const range = window.document.createRange();
|
|
1198
1299
|
const dom_selection = window.getSelection();
|
|
1199
1300
|
|
|
@@ -1202,13 +1303,21 @@ ${fallback_html}`;
|
|
|
1202
1303
|
range.collapse(true); // Collapse to start position
|
|
1203
1304
|
dom_selection.removeAllRanges();
|
|
1204
1305
|
dom_selection.addRange(range);
|
|
1306
|
+
|
|
1307
|
+
// Scroll the selection into view
|
|
1308
|
+
setTimeout(() => {
|
|
1309
|
+
const selectedElement = dom_selection.focusNode.parentElement;
|
|
1310
|
+
if (selectedElement) {
|
|
1311
|
+
selectedElement.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1312
|
+
}
|
|
1313
|
+
}, 0);
|
|
1205
1314
|
}
|
|
1206
1315
|
|
|
1207
1316
|
function __render_text_selection() {
|
|
1208
1317
|
const selection = /** @type {any} */ (doc.selection);
|
|
1209
1318
|
// The element that holds the annotated string
|
|
1210
1319
|
const el = canvas_ref.querySelector(`[data-path="${selection.path.join('.')}"][data-type="text"]`);
|
|
1211
|
-
const empty_text = doc.get(selection.path)
|
|
1320
|
+
const empty_text = doc.get(selection.path).text.length === 0;
|
|
1212
1321
|
|
|
1213
1322
|
const range = window.document.createRange();
|
|
1214
1323
|
const dom_selection = window.getSelection();
|
|
@@ -1261,7 +1370,6 @@ ${fallback_html}`;
|
|
|
1261
1370
|
return true; // Stop iteration
|
|
1262
1371
|
}
|
|
1263
1372
|
}
|
|
1264
|
-
|
|
1265
1373
|
current_offset += node_char_length;
|
|
1266
1374
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
1267
1375
|
for (const child_node of node.childNodes) {
|
|
@@ -1284,16 +1392,6 @@ ${fallback_html}`;
|
|
|
1284
1392
|
for (const child_node of el.childNodes) {
|
|
1285
1393
|
if (process_node(child_node)) break;
|
|
1286
1394
|
}
|
|
1287
|
-
|
|
1288
|
-
// EDGE CASE: cursor at the end of an annotation
|
|
1289
|
-
// TODO: It seems this edge case is handled elsewhere. We keep around the checker for a
|
|
1290
|
-
// while and delete this branch, if we don't see any problems.
|
|
1291
|
-
if (anchor_node && !focus_node && current_offset === end_offset) {
|
|
1292
|
-
alert('EDGE CASE: cursor at the end of an annotation.');
|
|
1293
|
-
// console.log('EDGE CASE: cursor at the end of an annotation');
|
|
1294
|
-
// focus_node = /** @type {HTMLElement | Text} */ (anchor_node.nextSibling || anchor_node);
|
|
1295
|
-
// focus_node_offset = focus_node === anchor_node ? anchor_node.length : 0;
|
|
1296
|
-
}
|
|
1297
1395
|
}
|
|
1298
1396
|
|
|
1299
1397
|
// Set the range if both start and end were found
|
|
@@ -1379,14 +1477,15 @@ ${fallback_html}`;
|
|
|
1379
1477
|
class="svedit-canvas {css_class}"
|
|
1380
1478
|
class:hide-selection={doc.selection?.type === 'node'}
|
|
1381
1479
|
class:node-cursor={doc.selection?.type === 'node' && doc.selection.anchor_offset === doc.selection.focus_offset}
|
|
1480
|
+
class:property-selection={doc.selection?.type === 'property'}
|
|
1382
1481
|
bind:this={canvas_ref}
|
|
1383
1482
|
{oninput}
|
|
1384
1483
|
{onbeforeinput}
|
|
1385
1484
|
{oncompositionstart}
|
|
1386
1485
|
{oncompositionend}
|
|
1387
1486
|
contenteditable={editable ? 'true' : 'false'}
|
|
1388
|
-
autocapitalize
|
|
1389
|
-
spellcheck
|
|
1487
|
+
{autocapitalize}
|
|
1488
|
+
{spellcheck}
|
|
1390
1489
|
{...{
|
|
1391
1490
|
// NOTE: Autocomplete and autocorrect make immense troubles
|
|
1392
1491
|
// on Desktop Chrome, so we disable them only for Chrome desktop.
|
|
@@ -1423,7 +1522,7 @@ ${fallback_html}`;
|
|
|
1423
1522
|
}
|
|
1424
1523
|
|
|
1425
1524
|
/* When the cursor is in a cursor-trap we never want to see the caret */
|
|
1426
|
-
.svedit-canvas.node-cursor {
|
|
1525
|
+
.svedit-canvas.node-cursor, .svedit-canvas.property-selection {
|
|
1427
1526
|
caret-color: transparent;
|
|
1428
1527
|
}
|
|
1429
1528
|
|