svedit 0.1.1 → 0.3.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 +38 -0
- package/dist/AnnotatedStringProperty.svelte +42 -39
- package/dist/AnnotatedStringProperty.svelte.d.ts.map +1 -1
- package/dist/CustomProperty.svelte +21 -10
- package/dist/CustomProperty.svelte.d.ts +1 -2
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/Document.svelte.d.ts +1 -1
- package/dist/Document.svelte.d.ts.map +1 -1
- package/dist/Document.svelte.js +5 -2
- package/dist/Node.svelte +18 -14
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +1 -1
- package/dist/Svedit.svelte +552 -84
- package/dist/Svedit.svelte.d.ts +1 -1
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +0 -3
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +19 -9
- package/dist/commands.svelte.d.ts.map +1 -1
- package/dist/commands.svelte.js +6 -4
- package/dist/util.d.ts +125 -3
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +170 -12
- package/package.json +1 -1
- package/src/lib/AnnotatedStringProperty.svelte +42 -39
- package/src/lib/CustomProperty.svelte +21 -10
- package/src/lib/Document.svelte.js +5 -2
- package/src/lib/Node.svelte +18 -14
- package/src/lib/NodeArrayProperty.svelte +1 -1
- package/src/lib/Svedit.svelte +552 -84
- package/src/lib/Transaction.svelte.js +19 -9
- package/src/lib/commands.svelte.js +6 -4
- package/src/lib/util.js +170 -12
package/dist/Svedit.svelte
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { setContext } from 'svelte';
|
|
3
|
-
import { snake_to_pascal } from './util.js';
|
|
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
4
|
import { break_text_node, join_text_node, insert_default_node, select_all } from './commands.svelte.js';
|
|
5
5
|
|
|
6
6
|
/** @import { SveditProps, DocumentPath, Selection, TextSelection, NodeSelection, PropertySelection, NodeId } from './types.d.ts'; */
|
|
7
7
|
/** @type {SveditProps} */
|
|
8
8
|
let {
|
|
9
9
|
doc,
|
|
10
|
-
editable = false,
|
|
10
|
+
editable = $bindable(false),
|
|
11
11
|
path,
|
|
12
12
|
class: css_class,
|
|
13
13
|
} = $props();
|
|
@@ -17,31 +17,225 @@
|
|
|
17
17
|
let Overlays = $derived(doc.config.system_components.Overlays);
|
|
18
18
|
let RootComponent = $derived(doc.config.node_components[snake_to_pascal(root_node.type)]);
|
|
19
19
|
|
|
20
|
+
// Track temporary disabled onkeydown events (e.g. during character composition)
|
|
21
|
+
let skip_onkeydown = false;
|
|
22
|
+
|
|
23
|
+
let is_composing = $state(false);
|
|
24
|
+
|
|
25
|
+
let input_selection = undefined;
|
|
26
|
+
|
|
27
|
+
// Detect Chrome on desktop (not mobile) - only available in browser
|
|
28
|
+
let is_chrome_desktop = $state(false);
|
|
29
|
+
|
|
30
|
+
$effect(() => {
|
|
31
|
+
if (typeof window !== 'undefined' && typeof navigator !== 'undefined') {
|
|
32
|
+
const user_agent = navigator.userAgent;
|
|
33
|
+
const is_chrome = user_agent.includes('Chrome') && !user_agent.includes('Edg');
|
|
34
|
+
const is_mobile = /iPhone|iPad|iPod|Android|Mobile/i.test(user_agent) ||
|
|
35
|
+
('ontouchstart' in window) ||
|
|
36
|
+
(navigator.maxTouchPoints > 0);
|
|
37
|
+
is_chrome_desktop = is_chrome && !is_mobile;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
20
41
|
/** Expose function so parent can call it */
|
|
21
42
|
export { focus_canvas };
|
|
22
43
|
|
|
23
44
|
setContext("svedit", {
|
|
24
45
|
get doc() {
|
|
25
46
|
return doc;
|
|
26
|
-
}
|
|
47
|
+
},
|
|
48
|
+
get editable() {
|
|
49
|
+
return editable;
|
|
50
|
+
},
|
|
51
|
+
get is_composing() {
|
|
52
|
+
return is_composing
|
|
53
|
+
},
|
|
27
54
|
});
|
|
28
55
|
|
|
56
|
+
function diff_text(old_text, new_text) {
|
|
57
|
+
const old_length = get_char_length(old_text);
|
|
58
|
+
const new_length = get_char_length(new_text);
|
|
59
|
+
|
|
60
|
+
let start = 0;
|
|
61
|
+
while (
|
|
62
|
+
start < old_length &&
|
|
63
|
+
start < new_length &&
|
|
64
|
+
get_char_at(old_text, start) === get_char_at(new_text, start)
|
|
65
|
+
) {
|
|
66
|
+
start++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let old_end = old_length;
|
|
70
|
+
let new_end = new_length;
|
|
71
|
+
while (
|
|
72
|
+
old_end > start &&
|
|
73
|
+
new_end > start &&
|
|
74
|
+
get_char_at(old_text, old_end - 1) === get_char_at(new_text, new_end - 1)
|
|
75
|
+
) {
|
|
76
|
+
old_end--;
|
|
77
|
+
new_end--;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
start_offset: start,
|
|
82
|
+
end_offset: old_end,
|
|
83
|
+
replacement_text: char_slice(new_text, start, new_end)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function commit_input() {
|
|
88
|
+
if (input_selection?.type !== 'text') return;
|
|
89
|
+
const model_text = doc.get(input_selection.path)[0];
|
|
90
|
+
const dom_text_element = document.querySelector(`[data-path="${input_selection.path.join('.')}"]`);
|
|
91
|
+
const dom_text = dom_text_element.textContent;
|
|
92
|
+
const op = diff_text(model_text, dom_text);
|
|
93
|
+
// console.log('======== COMMIT INPUT ========')
|
|
94
|
+
// console.log('mdl_text:', model_text);
|
|
95
|
+
// console.log('dom_text:', dom_text);
|
|
96
|
+
// console.log('op', op);
|
|
97
|
+
|
|
98
|
+
// NOTE: We are currently in an out-of-sync DOM state, right after
|
|
99
|
+
// contenteditable applied the input. But we can use this state
|
|
100
|
+
// to determine the ideal intended selection after the operation.
|
|
101
|
+
// Because our diffing may miss some context. E.g. when you replace "mchael"
|
|
102
|
+
// to "Michael" it will replace "m" with "Mi" and put the cursor after the "i",
|
|
103
|
+
// but we actually want it after "Michael".
|
|
104
|
+
const before_selection = __get_text_selection_from_dom();
|
|
105
|
+
|
|
106
|
+
// Now we are applying the change to the document
|
|
107
|
+
const tr = doc.tr;
|
|
108
|
+
tr.set_selection({
|
|
109
|
+
type: 'text',
|
|
110
|
+
path: [...input_selection.path],
|
|
111
|
+
anchor_offset: op.start_offset,
|
|
112
|
+
focus_offset: op.end_offset,
|
|
113
|
+
});
|
|
114
|
+
tr.insert_text(op.replacement_text);
|
|
115
|
+
tr.set_selection(before_selection);
|
|
116
|
+
doc.apply(tr);
|
|
117
|
+
// console.log('applied op', op);
|
|
118
|
+
|
|
119
|
+
// After each commited replacement, we start accepting custom key handlers again.
|
|
120
|
+
skip_onkeydown = false;
|
|
121
|
+
is_composing = false;
|
|
122
|
+
input_selection = null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function oninput(event) {
|
|
126
|
+
// console.log(`oninput: ${event.inputType}, data: "${event.data}", isComposing: ${event.isComposing}`, event);
|
|
127
|
+
if (
|
|
128
|
+
// canvas_ref?.contains(document.activeElement) &&
|
|
129
|
+
// doc.selection?.type === 'text' &&
|
|
130
|
+
input_selection.type === 'text' &&
|
|
131
|
+
!event.isComposing
|
|
132
|
+
) {
|
|
133
|
+
commit_input();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
29
137
|
/**
|
|
30
138
|
* @param {InputEvent} event
|
|
31
139
|
*/
|
|
32
140
|
function onbeforeinput(event) {
|
|
33
|
-
|
|
141
|
+
// console.log(`onbeforeinput: ${event.inputType}, data: "${event.data}", isComposing: ${event.isComposing}`, event);
|
|
34
142
|
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
143
|
+
// Only take input when in a valid text selection inside the canvas
|
|
144
|
+
if (
|
|
145
|
+
!(canvas_ref?.contains(document.activeElement)) ||
|
|
146
|
+
(doc.selection?.type !== 'text')
|
|
147
|
+
) {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (event.inputType === 'formatBold') {
|
|
153
|
+
doc.apply(doc.tr.annotate_text('strong'));
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
event.stopPropagation();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (event.inputType === 'formatItalic') {
|
|
159
|
+
doc.apply(doc.tr.annotate_text('emphasis'));
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
event.stopPropagation();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
// We remember the "input selection", because in some mobile browsers (Samsung Android) the onbeforeinput event
|
|
166
|
+
// is fired on a different selection than the input event.
|
|
167
|
+
input_selection = { ...doc.selection };
|
|
168
|
+
|
|
169
|
+
// If replacement is detected, I let contenteditable do the thing and wait for
|
|
170
|
+
// the oninput event to do the diffing and update the model.
|
|
171
|
+
if (event.inputType === 'insertReplacementText') {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// For now I reject drag+drop text movements.
|
|
176
|
+
// TODO: If I want to support those, I need to handle them in such a way that
|
|
177
|
+
// you can drag from one text property to another too.
|
|
178
|
+
if (event.inputType === 'deleteByDrag' || event.inputType === 'insertFromDrop') {
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// While composing, I do nothing and let the oncompositionend
|
|
184
|
+
// event handle the final replacement by recovering it from the DOM.
|
|
185
|
+
if (event.isComposing) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Insert the character, unless there is none.
|
|
190
|
+
let inserted_text = event.data;
|
|
191
|
+
|
|
192
|
+
// Sometimes (e.g. for replacements) the inserted_text is available via
|
|
193
|
+
// event.dataTransfer, not event.data
|
|
194
|
+
if (!inserted_text && event.dataTransfer) {
|
|
195
|
+
inserted_text = event.dataTransfer?.getData('text/plain');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// If there's no inserted_text, I'm not handling this.
|
|
199
|
+
if (!inserted_text) {
|
|
200
|
+
event.preventDefault();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// For now I don't accept structural replacements for savety reasons.
|
|
205
|
+
// E.g. this will catch Apple's writing tools that will attempt to mess with our DOM.
|
|
206
|
+
if (inserted_text.includes('\n') || inserted_text.includes('\t')) {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function get_base_char(char) {
|
|
212
|
+
if (!char) return undefined;
|
|
213
|
+
const normalized = char.normalize('NFD');
|
|
214
|
+
// If normalization doesn't change it OR it's longer than 2 chars, it's probably not a simple diacritic
|
|
215
|
+
if (normalized.length <= 2 && normalized !== char) {
|
|
216
|
+
return normalized[0];
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
(get_char_length(inserted_text) > 1) ||
|
|
223
|
+
(get_char_length(inserted_text) === 1 && get_base_char(inserted_text))
|
|
224
|
+
) {
|
|
225
|
+
skip_onkeydown = true;
|
|
226
|
+
return; // we are dealing with a replacement
|
|
40
227
|
}
|
|
228
|
+
|
|
229
|
+
const tr = doc.tr;
|
|
230
|
+
tr.insert_text(inserted_text);
|
|
231
|
+
doc.apply(tr);
|
|
232
|
+
input_selection = undefined;
|
|
233
|
+
event.preventDefault();
|
|
41
234
|
}
|
|
42
235
|
|
|
43
236
|
// Map DOM selection to internal model
|
|
44
237
|
function onselectionchange() {
|
|
238
|
+
if (!editable) return;
|
|
45
239
|
const dom_selection = window.getSelection();
|
|
46
240
|
if (!dom_selection.rangeCount) return;
|
|
47
241
|
|
|
@@ -54,6 +248,100 @@
|
|
|
54
248
|
}
|
|
55
249
|
}
|
|
56
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Creates HTML clipboard format with embedded svedit data
|
|
253
|
+
* @param {Object} json_data - The svedit data to embed
|
|
254
|
+
* @param {string} fallback_html - HTML for cross-app compatibility
|
|
255
|
+
* @returns {string} HTML with embedded svedit data
|
|
256
|
+
*/
|
|
257
|
+
function create_svedit_html_format(json_data, fallback_html) {
|
|
258
|
+
// Use encodeURIComponent to handle Unicode, then base64 encode
|
|
259
|
+
const json_string = JSON.stringify(json_data);
|
|
260
|
+
const encoded_data = btoa(encodeURIComponent(json_string));
|
|
261
|
+
|
|
262
|
+
return `<meta charset="utf-8">
|
|
263
|
+
<div>
|
|
264
|
+
<span data-svedit="${encoded_data}"></span>
|
|
265
|
+
</div>
|
|
266
|
+
${fallback_html}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Extracts svedit data from HTML clipboard format
|
|
271
|
+
* @param {string} html - HTML content from clipboard
|
|
272
|
+
* @returns {Object|null} Parsed svedit data or null if not found
|
|
273
|
+
*/
|
|
274
|
+
function extract_svedit_data_from_html(html) {
|
|
275
|
+
const svedit_regex = /data-svedit="([^"]+)"/;
|
|
276
|
+
const match = html.match(svedit_regex);
|
|
277
|
+
|
|
278
|
+
if (match && match[1]) {
|
|
279
|
+
try {
|
|
280
|
+
// Decode base64, then decode URI component to handle Unicode
|
|
281
|
+
const base64_decoded = atob(match[1]);
|
|
282
|
+
const decoded_data = decodeURIComponent(base64_decoded);
|
|
283
|
+
return JSON.parse(decoded_data);
|
|
284
|
+
} catch (e) {
|
|
285
|
+
console.warn('Failed to decode svedit data from HTML:', e);
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Default node exporter for nodes without specific exporters
|
|
295
|
+
* @param {Object} node - Node object
|
|
296
|
+
* @returns {string} HTML representation
|
|
297
|
+
*/
|
|
298
|
+
function default_node_exporter(node) {
|
|
299
|
+
let html = '';
|
|
300
|
+
let has_content = false;
|
|
301
|
+
|
|
302
|
+
for (const [prop_name, prop_value] of Object.entries(node)) {
|
|
303
|
+
if (prop_name === 'id' || prop_name === 'type') continue;
|
|
304
|
+
|
|
305
|
+
// Check if this is an annotated_string property (array with string as first element)
|
|
306
|
+
if (Array.isArray(prop_value) && typeof prop_value[0] === 'string') {
|
|
307
|
+
const text_content = prop_value[0];
|
|
308
|
+
if (text_content.trim()) {
|
|
309
|
+
html += `<p>${text_content}</p>\n`;
|
|
310
|
+
has_content = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!has_content) {
|
|
316
|
+
// Generic fallback for unknown node types with no annotated_string content
|
|
317
|
+
html += `<div data-node-type="${node.type}">Content from ${node.type}</div>\n`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return html;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Exports nodes to HTML using document config exporters
|
|
325
|
+
* @param {Object[]} nodes - Array of node objects
|
|
326
|
+
* @returns {string} HTML representation
|
|
327
|
+
*/
|
|
328
|
+
function export_html(nodes) {
|
|
329
|
+
let html = '';
|
|
330
|
+
|
|
331
|
+
for (const node of nodes) {
|
|
332
|
+
const html_exporters = doc.config.html_exporters || {};
|
|
333
|
+
|
|
334
|
+
if (html_exporters[node.type]) {
|
|
335
|
+
// Use custom exporter for this node type
|
|
336
|
+
html += html_exporters[node.type](node, doc);
|
|
337
|
+
} else {
|
|
338
|
+
// Use default exporter
|
|
339
|
+
html += default_node_exporter(node);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return html || '<p>Copied content</p>';
|
|
344
|
+
}
|
|
57
345
|
|
|
58
346
|
/**
|
|
59
347
|
* @param {NodeId[]} selected_node_ids
|
|
@@ -77,7 +365,6 @@
|
|
|
77
365
|
return { nodes, main_nodes: selected_node_ids };
|
|
78
366
|
}
|
|
79
367
|
|
|
80
|
-
|
|
81
368
|
/**
|
|
82
369
|
* @param {ClipboardEvent} event
|
|
83
370
|
* @param {boolean} delete_selection - used by oncut()
|
|
@@ -89,16 +376,16 @@
|
|
|
89
376
|
event.preventDefault();
|
|
90
377
|
event.stopPropagation();
|
|
91
378
|
|
|
92
|
-
let plain_text, html
|
|
379
|
+
let plain_text, html;
|
|
93
380
|
|
|
94
381
|
if (doc.selection?.type === 'text') {
|
|
95
382
|
plain_text = doc.get_selected_plain_text();
|
|
96
|
-
html = plain_text
|
|
383
|
+
html = `<span style="white-space:pre-wrap;">${plain_text}</span>`;
|
|
97
384
|
} else if (doc.selection?.type === 'node') {
|
|
98
385
|
const selected_nodes = doc.get_selected_nodes();
|
|
99
386
|
const { nodes, main_nodes } = prepare_copy_payload(selected_nodes);
|
|
100
387
|
|
|
101
|
-
json_data = { nodes, main_nodes };
|
|
388
|
+
const json_data = { nodes, main_nodes };
|
|
102
389
|
|
|
103
390
|
console.log('Copy operation:', {
|
|
104
391
|
selected_nodes,
|
|
@@ -106,18 +393,29 @@
|
|
|
106
393
|
total_nodes: Object.keys(nodes).length,
|
|
107
394
|
operation: delete_selection ? 'cut' : 'copy'
|
|
108
395
|
});
|
|
396
|
+
|
|
397
|
+
// Generate fallback HTML for cross-app compatibility
|
|
398
|
+
const selected_node_objects = main_nodes.map(id => nodes[id]);
|
|
399
|
+
const fallback_html = export_html(selected_node_objects);
|
|
400
|
+
|
|
401
|
+
// Create HTML with embedded svedit data
|
|
402
|
+
html = create_svedit_html_format(json_data, fallback_html);
|
|
403
|
+
|
|
404
|
+
// Create plain text representation
|
|
405
|
+
plain_text = selected_node_objects.map(node => {
|
|
406
|
+
if (node.type === 'story') {
|
|
407
|
+
return (node.title || 'Untitled Story') + (node.content ? '\n' + node.content : '');
|
|
408
|
+
}
|
|
409
|
+
return `Content from ${node.type}`;
|
|
410
|
+
}).join('\n\n');
|
|
109
411
|
}
|
|
110
412
|
|
|
111
413
|
// Create a ClipboardItem with multiple formats
|
|
112
414
|
const data = {
|
|
113
|
-
'text/plain': new Blob([plain_text], {type: 'text/plain'}),
|
|
114
|
-
'text/html': new Blob([html], {type: 'text/html'}),
|
|
415
|
+
'text/plain': new Blob([plain_text || ''], {type: 'text/plain'}),
|
|
416
|
+
'text/html': new Blob([html || ''], {type: 'text/html'}),
|
|
115
417
|
};
|
|
116
418
|
|
|
117
|
-
if (json_data) {
|
|
118
|
-
const json_blob = new Blob([JSON.stringify(json_data)], {type: 'application/json'});
|
|
119
|
-
data['web application/json'] = json_blob;
|
|
120
|
-
}
|
|
121
419
|
const clipboard_item_raw = new ClipboardItem(data);
|
|
122
420
|
|
|
123
421
|
// Write to clipboard
|
|
@@ -145,15 +443,18 @@
|
|
|
145
443
|
|
|
146
444
|
let pasted_json;
|
|
147
445
|
|
|
148
|
-
//
|
|
149
|
-
// plaintext copy and pasting for all other situations.
|
|
446
|
+
// First try to extract svedit data from HTML format
|
|
150
447
|
try {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
448
|
+
const html_blob = await clipboardItems[0].getType('text/html');
|
|
449
|
+
const html_content = await html_blob.text();
|
|
450
|
+
pasted_json = extract_svedit_data_from_html(html_content);
|
|
451
|
+
} catch (e) {
|
|
452
|
+
console.log('No HTML format available or failed to extract svedit data:', e);
|
|
154
453
|
pasted_json = undefined;
|
|
155
454
|
}
|
|
156
455
|
|
|
456
|
+
|
|
457
|
+
|
|
157
458
|
if (pasted_json) {
|
|
158
459
|
const tr = doc.tr;
|
|
159
460
|
|
|
@@ -202,10 +503,14 @@
|
|
|
202
503
|
|
|
203
504
|
doc.apply(tr);
|
|
204
505
|
} else {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
506
|
+
// Fallback to plain text when no svedit data is found
|
|
507
|
+
try {
|
|
508
|
+
const plain_text_blob = await clipboardItems[0].getType('text/plain');
|
|
509
|
+
const plain_text = await plain_text_blob.text();
|
|
510
|
+
doc.apply(doc.tr.insert_text(plain_text));
|
|
511
|
+
} catch (e) {
|
|
512
|
+
console.error('Failed to paste any content:', e);
|
|
513
|
+
}
|
|
209
514
|
}
|
|
210
515
|
}
|
|
211
516
|
|
|
@@ -233,7 +538,7 @@
|
|
|
233
538
|
} else if (selection?.type === 'property') {
|
|
234
539
|
__render_property_selection();
|
|
235
540
|
} else {
|
|
236
|
-
console.log('unsupported selection', selection);
|
|
541
|
+
console.log('unsupported selection', $state.snapshot(selection));
|
|
237
542
|
}
|
|
238
543
|
}
|
|
239
544
|
|
|
@@ -254,13 +559,34 @@
|
|
|
254
559
|
}
|
|
255
560
|
|
|
256
561
|
function onkeydown(e) {
|
|
562
|
+
// Turn editable on
|
|
563
|
+
if (e.key === 'e' && (e.ctrlKey || e.metaKey)) {
|
|
564
|
+
editable = true;
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Turn editable off (=save)
|
|
569
|
+
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
|
570
|
+
doc.selection = null;
|
|
571
|
+
editable = false;
|
|
572
|
+
e.preventDefault();
|
|
573
|
+
e.stopPropagation();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
257
577
|
// Only handle keyboard events if focus is within the canvas
|
|
258
578
|
if (!canvas_ref?.contains(document.activeElement)) return;
|
|
259
579
|
|
|
580
|
+
// Key handling temporarily disabled (e.g. while character composition takes place)
|
|
581
|
+
if (skip_onkeydown) {
|
|
582
|
+
// console.log('onkeydown skipped');
|
|
583
|
+
// Currently we do nothing, but we could handle keydown during character composition here.
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
260
587
|
const selection = /** @type {any} */ (doc.selection);
|
|
261
588
|
const is_collapsed = selection?.anchor_offset === selection?.focus_offset;
|
|
262
589
|
|
|
263
|
-
// console.log('onkeydown', e.key);
|
|
264
590
|
if (
|
|
265
591
|
(e.key === 'ArrowRight' && e.altKey && e.ctrlKey && doc.selected_node) ||
|
|
266
592
|
(e.key === 'ArrowRight' && e.altKey && e.ctrlKey && e.shiftKey && doc.selected_node)
|
|
@@ -354,14 +680,6 @@
|
|
|
354
680
|
doc.apply(doc.tr.insert_text('\n'));
|
|
355
681
|
e.preventDefault();
|
|
356
682
|
e.stopPropagation();
|
|
357
|
-
} else if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
|
|
358
|
-
doc.apply(doc.tr.annotate_text('strong'));
|
|
359
|
-
e.preventDefault();
|
|
360
|
-
e.stopPropagation();
|
|
361
|
-
} else if (e.key === 'i' && (e.ctrlKey || e.metaKey)) {
|
|
362
|
-
doc.apply(doc.tr.annotate_text('emphasis'));
|
|
363
|
-
e.preventDefault();
|
|
364
|
-
e.stopPropagation();
|
|
365
683
|
} else if (e.key === 'k' && (e.ctrlKey || e.metaKey)) {
|
|
366
684
|
doc.apply(doc.tr.annotate_text('link', {
|
|
367
685
|
href: window.prompt('Enter the URL', 'https://example.com')
|
|
@@ -378,7 +696,6 @@
|
|
|
378
696
|
}
|
|
379
697
|
} else {
|
|
380
698
|
if (doc.selection?.type === 'text' && doc.selection?.anchor_offset === 0 && doc.selection?.focus_offset === 0) {
|
|
381
|
-
console.log('try to join with previous node');
|
|
382
699
|
const tr = doc.tr;
|
|
383
700
|
join_text_node(tr);
|
|
384
701
|
doc.apply(tr);
|
|
@@ -391,6 +708,47 @@
|
|
|
391
708
|
}
|
|
392
709
|
}
|
|
393
710
|
|
|
711
|
+
e.preventDefault();
|
|
712
|
+
e.stopPropagation();
|
|
713
|
+
} else if (e.key === 'Delete') {
|
|
714
|
+
// Forward delete: only handle collapsed text selections
|
|
715
|
+
if (selection?.type === 'text' && is_collapsed) {
|
|
716
|
+
const text_content = doc.get(selection.path)[0];
|
|
717
|
+
const text_length = get_char_length(text_content);
|
|
718
|
+
|
|
719
|
+
if (selection.focus_offset < text_length) {
|
|
720
|
+
// There's a character after the cursor - select and delete it
|
|
721
|
+
const tr = doc.tr;
|
|
722
|
+
tr.set_selection({
|
|
723
|
+
type: 'text',
|
|
724
|
+
path: [...selection.path],
|
|
725
|
+
anchor_offset: selection.focus_offset,
|
|
726
|
+
focus_offset: selection.focus_offset + 1
|
|
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
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
394
752
|
e.preventDefault();
|
|
395
753
|
e.stopPropagation();
|
|
396
754
|
} else if (e.key === 'Enter' && selection?.type === 'property') {
|
|
@@ -420,8 +778,11 @@
|
|
|
420
778
|
e.stopPropagation();
|
|
421
779
|
} else if (e.key === 'Enter' && selection?.type === 'text') {
|
|
422
780
|
const tr = doc.tr;
|
|
423
|
-
break_text_node(tr)
|
|
424
|
-
|
|
781
|
+
if (break_text_node(tr)) {
|
|
782
|
+
doc.apply(tr);
|
|
783
|
+
}
|
|
784
|
+
e.preventDefault();
|
|
785
|
+
e.stopPropagation();
|
|
425
786
|
} else if (e.key === 'Escape' && selection) {
|
|
426
787
|
doc.select_parent();
|
|
427
788
|
e.preventDefault();
|
|
@@ -429,6 +790,34 @@
|
|
|
429
790
|
}
|
|
430
791
|
}
|
|
431
792
|
|
|
793
|
+
/**
|
|
794
|
+
* Handles composition start events for input methods like dead keys
|
|
795
|
+
* This occurs when user starts typing a composed character (e.g., backtick for accents)
|
|
796
|
+
* @param {CompositionEvent} event
|
|
797
|
+
*/
|
|
798
|
+
function oncompositionstart(event) {
|
|
799
|
+
if (doc.selection.type !== 'text') return;
|
|
800
|
+
console.log('DEBUG: oncompositionstart', event.data);
|
|
801
|
+
// Disable keydown event handling during composition. This way, you can confirm
|
|
802
|
+
// a diacritic (a->ä) with ENTER without causing a line break.
|
|
803
|
+
skip_onkeydown = true;
|
|
804
|
+
is_composing = true;
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Handles composition end events for input methods like dead keys
|
|
810
|
+
* This occurs when composition is complete (e.g., after typing 'a' following backtick to get 'à')
|
|
811
|
+
*/
|
|
812
|
+
function oncompositionend(/*event*/) {
|
|
813
|
+
// console.log('DEBUG: oncompositionend, insert:', event.data);
|
|
814
|
+
if (!canvas_ref?.contains(document.activeElement)) return;
|
|
815
|
+
if (canvas_ref?.contains(document.activeElement) && doc.selection?.type === 'text') {
|
|
816
|
+
commit_input();
|
|
817
|
+
}
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
432
821
|
/**
|
|
433
822
|
* Extracts a NodeSelection from the current DOM selection.
|
|
434
823
|
*
|
|
@@ -524,23 +913,23 @@
|
|
|
524
913
|
focus_offset += 1;
|
|
525
914
|
}
|
|
526
915
|
|
|
527
|
-
// Exclude first node when anchor_node
|
|
916
|
+
// EDGE CASE: Exclude first node when anchor_node is an afer-node-cursor-trap
|
|
528
917
|
// in a non-collapsed forward selection
|
|
529
918
|
if (
|
|
530
|
-
anchor_node.dataset.type === 'after-node-cursor-trap' &&
|
|
919
|
+
anchor_node.parentElement?.dataset.type === 'after-node-cursor-trap' &&
|
|
531
920
|
!is_backwards &&
|
|
532
921
|
anchor_offset !== focus_offset
|
|
533
922
|
) {
|
|
534
923
|
anchor_offset += 1;
|
|
535
924
|
}
|
|
536
|
-
// Exclude first node when focus_node
|
|
925
|
+
// EDGE CASE: Exclude first node when focus_node is an afer-node-cursor-trap
|
|
537
926
|
// in a non-collapsed backward selection
|
|
538
927
|
else if (
|
|
539
|
-
focus_node.dataset.type === 'after-node-cursor-trap' &&
|
|
928
|
+
focus_node.parentElement?.dataset.type === 'after-node-cursor-trap' &&
|
|
540
929
|
is_backwards &&
|
|
541
930
|
anchor_offset !== focus_offset &&
|
|
542
|
-
// EDGE CASE:
|
|
543
|
-
|
|
931
|
+
// EDGE CASE: Only do correction when drag started from a deeper or equally deep anchor node
|
|
932
|
+
anchor_node_depth >= focus_node_depth
|
|
544
933
|
) {
|
|
545
934
|
focus_offset += 1;
|
|
546
935
|
}
|
|
@@ -553,7 +942,6 @@
|
|
|
553
942
|
};
|
|
554
943
|
}
|
|
555
944
|
|
|
556
|
-
|
|
557
945
|
/**
|
|
558
946
|
* Extracts a PropertySelection from the current DOM selection.
|
|
559
947
|
*
|
|
@@ -593,8 +981,9 @@
|
|
|
593
981
|
const focus_node = /** @type {HTMLElement} */ (dom_selection.focusNode);
|
|
594
982
|
let focus_root, anchor_root;
|
|
595
983
|
|
|
984
|
+
|
|
596
985
|
if (dom_selection.focusNode === dom_selection.anchorNode && focus_node.dataset?.type === 'text') {
|
|
597
|
-
//
|
|
986
|
+
// EDGE CASE 1: Either text node is empty (only a <br> is present), or cursor is after a <br> at the very end of the text node
|
|
598
987
|
focus_root = anchor_root = focus_node;
|
|
599
988
|
} else {
|
|
600
989
|
focus_root = /** @type {HTMLElement} */ (dom_selection.focusNode.parentElement?.closest('[data-path][data-type="text"]'));
|
|
@@ -612,29 +1001,80 @@
|
|
|
612
1001
|
|
|
613
1002
|
if (!path) return null;
|
|
614
1003
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1004
|
+
// EDGE CASE 1B: Cursor after trailing <br> at end of text
|
|
1005
|
+
//
|
|
1006
|
+
// When text ends with a newline (\n), the browser renders it as a <br> element.
|
|
1007
|
+
// If the user places their cursor after this <br>, the selection detection
|
|
1008
|
+
// incorrectly reports position 0 instead of the actual text length.
|
|
1009
|
+
//
|
|
1010
|
+
// Example DOM structure when text is "Hello\n":
|
|
1011
|
+
// <div data-type="text">
|
|
1012
|
+
// #text "Hello" ← index 0
|
|
1013
|
+
// <br> ← index 1 (represents the \n)
|
|
1014
|
+
// </div>
|
|
1015
|
+
//
|
|
1016
|
+
// When cursor is after the <br>, we get:
|
|
1017
|
+
// - focusNode: the div element
|
|
1018
|
+
// - focusOffset: 2 (after the 2nd child)
|
|
1019
|
+
// - But normal processing returns position 0 instead of text.length
|
|
1020
|
+
//
|
|
1021
|
+
// This edge case detects when:
|
|
1022
|
+
// 1. Text content ends with \n
|
|
1023
|
+
// 2. Last child node is a <br> (the visual representation of that \n)
|
|
1024
|
+
// 3. Cursor is positioned at or near the end
|
|
1025
|
+
// And returns the correct text.length position instead.
|
|
1026
|
+
const text_content = doc.get(path)[0];
|
|
1027
|
+
const text_length = get_char_length(text_content);
|
|
1028
|
+
if (text_length > 0) {
|
|
1029
|
+
const last_char = get_char_at(text_content, text_length - 1);
|
|
1030
|
+
if (dom_selection.focusNode === dom_selection.anchorNode &&
|
|
1031
|
+
dom_selection.focusNode === focus_node &&
|
|
1032
|
+
focus_node.dataset?.type === 'text' &&
|
|
1033
|
+
!focus_node.classList.contains('empty') &&
|
|
1034
|
+
last_char === '\n') {
|
|
1035
|
+
|
|
1036
|
+
const child_nodes = focus_node.childNodes;
|
|
1037
|
+
|
|
1038
|
+
// Check if the last child is a BR (indicating text ends with newline)
|
|
1039
|
+
// and cursor is positioned near the end (after any of the last few elements)
|
|
1040
|
+
if (child_nodes.length > 0 &&
|
|
1041
|
+
child_nodes[child_nodes.length - 1].nodeName === 'BR' &&
|
|
1042
|
+
dom_selection.focusOffset >= child_nodes.length - 2) {
|
|
1043
|
+
return {
|
|
1044
|
+
type: 'text',
|
|
1045
|
+
path,
|
|
1046
|
+
anchor_offset: text_length,
|
|
1047
|
+
focus_offset: text_length
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
let anchor_offset = 0;
|
|
1054
|
+
let focus_offset = 0;
|
|
1055
|
+
let current_offset = 0;
|
|
618
1056
|
|
|
619
1057
|
function processNode(node) {
|
|
620
1058
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
621
|
-
const
|
|
622
|
-
|
|
1059
|
+
const nodeText = node.textContent;
|
|
1060
|
+
const nodeCharLength = get_char_length(nodeText);
|
|
623
1061
|
if (node === range.startContainer) {
|
|
624
|
-
|
|
1062
|
+
// Convert UTF-16 offset to character offset
|
|
1063
|
+
const char_start_offset = utf16_to_char_offset(nodeText, range.startOffset);
|
|
1064
|
+
anchor_offset = current_offset + char_start_offset;
|
|
625
1065
|
}
|
|
626
1066
|
if (node === range.endContainer) {
|
|
627
|
-
|
|
1067
|
+
// Convert UTF-16 offset to character offset
|
|
1068
|
+
const char_end_offset = utf16_to_char_offset(nodeText, range.endOffset);
|
|
1069
|
+
focus_offset = current_offset + char_end_offset;
|
|
628
1070
|
}
|
|
629
|
-
|
|
630
|
-
currentOffset += nodeLength;
|
|
1071
|
+
current_offset += nodeCharLength;
|
|
631
1072
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
632
1073
|
for (const childNode of node.childNodes) {
|
|
633
1074
|
processNode(childNode);
|
|
634
1075
|
}
|
|
635
1076
|
}
|
|
636
|
-
|
|
637
|
-
return (focusOffset !== 0);
|
|
1077
|
+
return (focus_offset !== 0);
|
|
638
1078
|
}
|
|
639
1079
|
|
|
640
1080
|
// Process nodes to find offsets
|
|
@@ -648,14 +1088,14 @@
|
|
|
648
1088
|
|
|
649
1089
|
// Swap offsets if it's a backward selection
|
|
650
1090
|
if (is_backward) {
|
|
651
|
-
[
|
|
1091
|
+
[anchor_offset, focus_offset] = [focus_offset, anchor_offset];
|
|
652
1092
|
}
|
|
653
1093
|
|
|
654
1094
|
return {
|
|
655
1095
|
type: 'text',
|
|
656
1096
|
path,
|
|
657
|
-
anchor_offset:
|
|
658
|
-
focus_offset:
|
|
1097
|
+
anchor_offset: anchor_offset,
|
|
1098
|
+
focus_offset: focus_offset
|
|
659
1099
|
};
|
|
660
1100
|
}
|
|
661
1101
|
|
|
@@ -672,7 +1112,6 @@
|
|
|
672
1112
|
function __render_node_selection() {
|
|
673
1113
|
const selection = /** @type {NodeSelection} */ (doc.selection);
|
|
674
1114
|
const node_array_path = selection.path.join('.');
|
|
675
|
-
|
|
676
1115
|
let is_collapsed = selection.anchor_offset === selection.focus_offset;
|
|
677
1116
|
let is_backward = !is_collapsed && selection.anchor_offset > selection.focus_offset;
|
|
678
1117
|
|
|
@@ -704,7 +1143,6 @@
|
|
|
704
1143
|
selection.anchor_offset === 0 ? ':scope > .position-zero-cursor-trap' : ':scope > .after-node-cursor-trap'
|
|
705
1144
|
);
|
|
706
1145
|
|
|
707
|
-
|
|
708
1146
|
range.setStart(cursor_trap_el, 1);
|
|
709
1147
|
range.setEnd(cursor_trap_el, 1);
|
|
710
1148
|
dom_selection.removeAllRanges();
|
|
@@ -754,13 +1192,13 @@
|
|
|
754
1192
|
const selection = doc.selection;
|
|
755
1193
|
// The element that holds the property
|
|
756
1194
|
const el = canvas_ref.querySelector(`[data-path="${selection.path.join('.')}"][data-type="property"]`);
|
|
757
|
-
const
|
|
1195
|
+
const cursor_trap_selectable = el.querySelector('.svedit-selectable');
|
|
758
1196
|
|
|
759
1197
|
const range = window.document.createRange();
|
|
760
1198
|
const dom_selection = window.getSelection();
|
|
761
1199
|
|
|
762
1200
|
// Select the entire cursor trap element contents and collapse to start
|
|
763
|
-
range.selectNodeContents(
|
|
1201
|
+
range.selectNodeContents(cursor_trap_selectable);
|
|
764
1202
|
range.collapse(true); // Collapse to start position
|
|
765
1203
|
dom_selection.removeAllRanges();
|
|
766
1204
|
dom_selection.addRange(range);
|
|
@@ -787,40 +1225,44 @@
|
|
|
787
1225
|
const start_offset = Math.min(selection.anchor_offset, selection.focus_offset);
|
|
788
1226
|
const end_offset = Math.max(selection.anchor_offset, selection.focus_offset);
|
|
789
1227
|
|
|
790
|
-
|
|
791
1228
|
// Helper function to process each node
|
|
792
1229
|
function process_node(node) {
|
|
793
1230
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
794
|
-
const
|
|
1231
|
+
const node_text = node.textContent;
|
|
1232
|
+
const node_char_length = get_char_length(node_text);
|
|
795
1233
|
|
|
796
1234
|
if (is_backward) {
|
|
797
|
-
if (!focus_node && current_offset +
|
|
1235
|
+
if (!focus_node && current_offset + node_char_length >= start_offset) {
|
|
798
1236
|
focus_node = node;
|
|
799
|
-
|
|
1237
|
+
const char_offset = start_offset - current_offset;
|
|
1238
|
+
focus_node_offset = char_to_utf16_offset(node_text, char_offset);
|
|
800
1239
|
}
|
|
801
1240
|
} else {
|
|
802
|
-
if (!anchor_node && current_offset +
|
|
1241
|
+
if (!anchor_node && current_offset + node_char_length >= start_offset) {
|
|
803
1242
|
anchor_node = node;
|
|
804
|
-
|
|
1243
|
+
const char_offset = start_offset - current_offset;
|
|
1244
|
+
anchor_node_offset = char_to_utf16_offset(node_text, char_offset);
|
|
805
1245
|
}
|
|
806
1246
|
}
|
|
807
1247
|
|
|
808
1248
|
// Find end node
|
|
809
1249
|
if (is_backward) {
|
|
810
|
-
if (!anchor_node && current_offset +
|
|
1250
|
+
if (!anchor_node && current_offset + node_char_length >= end_offset) {
|
|
811
1251
|
anchor_node = node;
|
|
812
|
-
|
|
1252
|
+
const char_offset = end_offset - current_offset;
|
|
1253
|
+
anchor_node_offset = char_to_utf16_offset(node_text, char_offset);
|
|
813
1254
|
return true; // Stop iteration
|
|
814
1255
|
}
|
|
815
1256
|
} else {
|
|
816
|
-
if (!focus_node && current_offset +
|
|
1257
|
+
if (!focus_node && current_offset + node_char_length >= end_offset) {
|
|
817
1258
|
focus_node = node;
|
|
818
|
-
|
|
1259
|
+
const char_offset = end_offset - current_offset;
|
|
1260
|
+
focus_node_offset = char_to_utf16_offset(node_text, char_offset);
|
|
819
1261
|
return true; // Stop iteration
|
|
820
1262
|
}
|
|
821
1263
|
}
|
|
822
1264
|
|
|
823
|
-
current_offset +=
|
|
1265
|
+
current_offset += node_char_length;
|
|
824
1266
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
825
1267
|
for (const child_node of node.childNodes) {
|
|
826
1268
|
if (process_node(child_node)) return true; // Stop iteration if end found
|
|
@@ -936,9 +1378,24 @@
|
|
|
936
1378
|
<div
|
|
937
1379
|
class="svedit-canvas {css_class}"
|
|
938
1380
|
class:hide-selection={doc.selection?.type === 'node'}
|
|
1381
|
+
class:node-cursor={doc.selection?.type === 'node' && doc.selection.anchor_offset === doc.selection.focus_offset}
|
|
939
1382
|
bind:this={canvas_ref}
|
|
1383
|
+
{oninput}
|
|
940
1384
|
{onbeforeinput}
|
|
1385
|
+
{oncompositionstart}
|
|
1386
|
+
{oncompositionend}
|
|
941
1387
|
contenteditable={editable ? 'true' : 'false'}
|
|
1388
|
+
autocapitalize="off"
|
|
1389
|
+
spellcheck="true"
|
|
1390
|
+
{...{
|
|
1391
|
+
// NOTE: Autocomplete and autocorrect make immense troubles
|
|
1392
|
+
// on Desktop Chrome, so we disable them only for Chrome desktop.
|
|
1393
|
+
// Additionally, OSX-native auto-complete also breaks, because
|
|
1394
|
+
// I'm using a keyed block that always wipes the DOM of a text node
|
|
1395
|
+
// on every change.
|
|
1396
|
+
autocomplete: is_chrome_desktop ? "off" : "on",
|
|
1397
|
+
autocorrect: is_chrome_desktop ? "off" : "on"
|
|
1398
|
+
}}
|
|
942
1399
|
>
|
|
943
1400
|
<RootComponent {path} />
|
|
944
1401
|
</div>
|
|
@@ -955,15 +1412,26 @@
|
|
|
955
1412
|
}
|
|
956
1413
|
}
|
|
957
1414
|
|
|
958
|
-
.svedit-canvas.hide-selection {
|
|
959
|
-
caret-color: transparent;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
1415
|
.svedit-canvas :global(::selection) {
|
|
963
1416
|
background: var(--editing-fill-color);
|
|
964
1417
|
}
|
|
965
1418
|
|
|
966
|
-
|
|
967
|
-
|
|
1419
|
+
@media not (pointer: coarse) {
|
|
1420
|
+
.svedit-canvas.hide-selection {
|
|
1421
|
+
caret-color: transparent;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/* When the cursor is in a cursor-trap we never want to see the caret */
|
|
1426
|
+
.svedit-canvas.node-cursor {
|
|
1427
|
+
caret-color: transparent;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
@media not (pointer: coarse) {
|
|
1431
|
+
@supports (anchor-name: --test) {
|
|
1432
|
+
.svedit-canvas.hide-selection :global(::selection) {
|
|
1433
|
+
background: transparent;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
968
1436
|
}
|
|
969
1437
|
</style>
|