svedit 0.1.0 → 0.2.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 +36 -0
- package/dist/CustomProperty.svelte +4 -1
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/Svedit.svelte +46 -12
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/CustomProperty.svelte +4 -1
- package/src/lib/Svedit.svelte +46 -12
package/README.md
CHANGED
|
@@ -286,6 +286,42 @@ Now you can start making your Svelte pages in-place editable by wrapping your de
|
|
|
286
286
|
<Svedit {doc} path={[doc.document_id]} editable={true} />
|
|
287
287
|
```
|
|
288
288
|
|
|
289
|
+
## Mastering contenteditable
|
|
290
|
+
|
|
291
|
+
Svedit relies on the contenteditable attribute to make elements editable. The below example shows you
|
|
292
|
+
a simplified version of the markup of `<NodeCursorTrap>` and why it is implemented the way it is.
|
|
293
|
+
|
|
294
|
+
```html
|
|
295
|
+
<div contenteditable="true">
|
|
296
|
+
<div class="some-wrapper">
|
|
297
|
+
<!--
|
|
298
|
+
Putting a <br> tag into a div gives you a single addressable cursor position.
|
|
299
|
+
|
|
300
|
+
Adding a ​ (or any character) here will lead to 2 cursor
|
|
301
|
+
positions (one before, and one after the character)
|
|
302
|
+
|
|
303
|
+
Svedit uses this behavior for node-cursor-traps, and when an
|
|
304
|
+
<AnnotatedTextProperty> is empty.
|
|
305
|
+
-->
|
|
306
|
+
<div class="cursor-trap"><br></div>
|
|
307
|
+
<!--
|
|
308
|
+
If you create a contenteditable="false" island, there needs to be some content in it,
|
|
309
|
+
otherwise it will create two additional cursor positions. One before, and another one
|
|
310
|
+
after the island.
|
|
311
|
+
|
|
312
|
+
The Svedit demo uses this technique in `<NodeCursorTrap>` to create a node-cursor
|
|
313
|
+
visualization, that doesn't mess with the contenteditable cursor positions.
|
|
314
|
+
-->
|
|
315
|
+
<div contenteditable="false" class="node-cursor">​</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Further things to consider:
|
|
321
|
+
|
|
322
|
+
- If you make a sub-tree `contenteditable="false"`, be aware that you can't create a `contenteditable="true"` segment somewhere inside it. Svedit can only work reliably when there's one contenteditable="true" at root (it's set by `<Svedit`>)
|
|
323
|
+
- `<AnnotatedString>` and `<CustomProperty>` must not be wrapped in `contenteditable="false"` to work properly.
|
|
324
|
+
|
|
289
325
|
## Full API docs?
|
|
290
326
|
|
|
291
327
|
Not yet. Please just [read the code](./src) for now. It's only a couple of files with less than 3000 LOC in total. The files in `routes` are considered example code (copy them and adapt them to your needs), while files in `lib` are considered library code. Read them to understand the API and what's happening behind the scences.
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
data-path={path.join('.')}
|
|
17
17
|
style="anchor-name: --{path.join('-')};"
|
|
18
18
|
>
|
|
19
|
-
<div class="cursor-trap
|
|
19
|
+
<div class="cursor-trap">
|
|
20
|
+
<div class="svedit-selectable"><br/></div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
20
23
|
<div class="property-content" contenteditable="false">{@render children()}</div>
|
|
21
24
|
</div>
|
|
22
25
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CustomProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/CustomProperty.svelte.js"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"CustomProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/CustomProperty.svelte.js"],"names":[],"mappings":";;;;;AA2BA,sFAA6D;yCAvBlB,cAAc"}
|
package/dist/Svedit.svelte
CHANGED
|
@@ -261,7 +261,10 @@
|
|
|
261
261
|
const is_collapsed = selection?.anchor_offset === selection?.focus_offset;
|
|
262
262
|
|
|
263
263
|
// console.log('onkeydown', e.key);
|
|
264
|
-
if (
|
|
264
|
+
if (
|
|
265
|
+
(e.key === 'ArrowRight' && e.altKey && e.ctrlKey && doc.selected_node) ||
|
|
266
|
+
(e.key === 'ArrowRight' && e.altKey && e.ctrlKey && e.shiftKey && doc.selected_node)
|
|
267
|
+
) {
|
|
265
268
|
const node = doc.selected_node;
|
|
266
269
|
const layout_count = doc.config.node_layouts[node.type];
|
|
267
270
|
|
|
@@ -272,7 +275,10 @@
|
|
|
272
275
|
tr.set([doc.selected_node?.id, 'layout'], next_layout);
|
|
273
276
|
doc.apply(tr);
|
|
274
277
|
}
|
|
275
|
-
} else if (
|
|
278
|
+
} else if (
|
|
279
|
+
(e.key === 'ArrowLeft' && e.altKey && e.ctrlKey && doc.selected_node) ||
|
|
280
|
+
(e.key === 'ArrowLeft' && e.altKey && e.ctrlKey && e.shiftKey && doc.selected_node)
|
|
281
|
+
) {
|
|
276
282
|
const node = doc.selected_node;
|
|
277
283
|
const layout_count = doc.config.node_layouts[node.type];
|
|
278
284
|
if (layout_count > 1 && node?.layout) {
|
|
@@ -282,7 +288,10 @@
|
|
|
282
288
|
doc.apply(tr);
|
|
283
289
|
console.log('layout / count / prev_layout', node.layout, layout_count, prev_layout);
|
|
284
290
|
}
|
|
285
|
-
} else if (
|
|
291
|
+
} else if (
|
|
292
|
+
(e.key === 'ArrowDown' && e.altKey && e.ctrlKey && doc.selected_node) ||
|
|
293
|
+
(e.key === 'ArrowDown' && e.altKey && e.ctrlKey && e.shiftKey && doc.selected_node )
|
|
294
|
+
) {
|
|
286
295
|
const node = doc.selected_node;
|
|
287
296
|
|
|
288
297
|
if (doc.selection.type !== 'node') {
|
|
@@ -301,9 +310,10 @@
|
|
|
301
310
|
doc.config.inserters[next_type](tr);
|
|
302
311
|
tr.set_selection(old_selection);
|
|
303
312
|
doc.apply(tr);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
313
|
+
} else if (
|
|
314
|
+
(e.key === 'ArrowUp' && e.altKey && e.ctrlKey && doc.selected_node) ||
|
|
315
|
+
(e.key === 'ArrowUp' && e.altKey && e.ctrlKey && e.shiftKey && doc.selected_node)
|
|
316
|
+
) {
|
|
307
317
|
const node = doc.selected_node;
|
|
308
318
|
|
|
309
319
|
if (doc.selection.type !== 'node') {
|
|
@@ -340,7 +350,7 @@
|
|
|
340
350
|
doc.redo();
|
|
341
351
|
e.preventDefault();
|
|
342
352
|
e.stopPropagation();
|
|
343
|
-
} else if (e.key === 'Enter' && (e.
|
|
353
|
+
} else if (e.key === 'Enter' && (e.altKey)) {
|
|
344
354
|
doc.apply(doc.tr.insert_text('\n'));
|
|
345
355
|
e.preventDefault();
|
|
346
356
|
e.stopPropagation();
|
|
@@ -483,6 +493,8 @@
|
|
|
483
493
|
|
|
484
494
|
let focus_root_path = focus_root.dataset.path.split('.');
|
|
485
495
|
let anchor_root_path = anchor_root.dataset.path.split('.');
|
|
496
|
+
let focus_node_depth = focus_root_path.length;
|
|
497
|
+
let anchor_node_depth = anchor_root_path.length;
|
|
486
498
|
|
|
487
499
|
// HACK: this works only for one level nesting - should be done recursively to work generally
|
|
488
500
|
if (focus_root_path.length > anchor_root_path.length) {
|
|
@@ -512,6 +524,27 @@
|
|
|
512
524
|
focus_offset += 1;
|
|
513
525
|
}
|
|
514
526
|
|
|
527
|
+
// EDGE CASE: Exclude first node when anchor_node is an afer-node-cursor-trap
|
|
528
|
+
// in a non-collapsed forward selection
|
|
529
|
+
if (
|
|
530
|
+
anchor_node.parentElement?.dataset.type === 'after-node-cursor-trap' &&
|
|
531
|
+
!is_backwards &&
|
|
532
|
+
anchor_offset !== focus_offset
|
|
533
|
+
) {
|
|
534
|
+
anchor_offset += 1;
|
|
535
|
+
}
|
|
536
|
+
// EDGE CASE: Exclude first node when focus_node is an afer-node-cursor-trap
|
|
537
|
+
// in a non-collapsed backward selection
|
|
538
|
+
else if (
|
|
539
|
+
focus_node.parentElement?.dataset.type === 'after-node-cursor-trap' &&
|
|
540
|
+
is_backwards &&
|
|
541
|
+
anchor_offset !== focus_offset &&
|
|
542
|
+
// EDGE CASE: Only do correction when drag started from a deeper or equally deep anchor node
|
|
543
|
+
anchor_node_depth >= focus_node_depth
|
|
544
|
+
) {
|
|
545
|
+
focus_offset += 1;
|
|
546
|
+
}
|
|
547
|
+
|
|
515
548
|
return {
|
|
516
549
|
type: 'node',
|
|
517
550
|
path: anchor_root_path.slice(0, -1),
|
|
@@ -520,7 +553,6 @@
|
|
|
520
553
|
};
|
|
521
554
|
}
|
|
522
555
|
|
|
523
|
-
|
|
524
556
|
/**
|
|
525
557
|
* Extracts a PropertySelection from the current DOM selection.
|
|
526
558
|
*
|
|
@@ -721,13 +753,13 @@
|
|
|
721
753
|
const selection = doc.selection;
|
|
722
754
|
// The element that holds the property
|
|
723
755
|
const el = canvas_ref.querySelector(`[data-path="${selection.path.join('.')}"][data-type="property"]`);
|
|
724
|
-
const
|
|
756
|
+
const cursor_trap_selectable = el.querySelector('.svedit-selectable');
|
|
725
757
|
|
|
726
758
|
const range = window.document.createRange();
|
|
727
759
|
const dom_selection = window.getSelection();
|
|
728
760
|
|
|
729
761
|
// Select the entire cursor trap element contents and collapse to start
|
|
730
|
-
range.selectNodeContents(
|
|
762
|
+
range.selectNodeContents(cursor_trap_selectable);
|
|
731
763
|
range.collapse(true); // Collapse to start position
|
|
732
764
|
dom_selection.removeAllRanges();
|
|
733
765
|
dom_selection.addRange(range);
|
|
@@ -930,7 +962,9 @@
|
|
|
930
962
|
background: var(--editing-fill-color);
|
|
931
963
|
}
|
|
932
964
|
|
|
933
|
-
|
|
934
|
-
|
|
965
|
+
@media not (pointer: coarse) {
|
|
966
|
+
.svedit-canvas.hide-selection :global(::selection) {
|
|
967
|
+
background: transparent;
|
|
968
|
+
}
|
|
935
969
|
}
|
|
936
970
|
</style>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"Svedit.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Svedit.svelte.js"],"names":[],"mappings":";;;;;;;AA66BA;;OAAqD;iCAj6BgE,cAAc"}
|
package/package.json
CHANGED
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
data-path={path.join('.')}
|
|
17
17
|
style="anchor-name: --{path.join('-')};"
|
|
18
18
|
>
|
|
19
|
-
<div class="cursor-trap
|
|
19
|
+
<div class="cursor-trap">
|
|
20
|
+
<div class="svedit-selectable"><br/></div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
20
23
|
<div class="property-content" contenteditable="false">{@render children()}</div>
|
|
21
24
|
</div>
|
|
22
25
|
|
package/src/lib/Svedit.svelte
CHANGED
|
@@ -261,7 +261,10 @@
|
|
|
261
261
|
const is_collapsed = selection?.anchor_offset === selection?.focus_offset;
|
|
262
262
|
|
|
263
263
|
// console.log('onkeydown', e.key);
|
|
264
|
-
if (
|
|
264
|
+
if (
|
|
265
|
+
(e.key === 'ArrowRight' && e.altKey && e.ctrlKey && doc.selected_node) ||
|
|
266
|
+
(e.key === 'ArrowRight' && e.altKey && e.ctrlKey && e.shiftKey && doc.selected_node)
|
|
267
|
+
) {
|
|
265
268
|
const node = doc.selected_node;
|
|
266
269
|
const layout_count = doc.config.node_layouts[node.type];
|
|
267
270
|
|
|
@@ -272,7 +275,10 @@
|
|
|
272
275
|
tr.set([doc.selected_node?.id, 'layout'], next_layout);
|
|
273
276
|
doc.apply(tr);
|
|
274
277
|
}
|
|
275
|
-
} else if (
|
|
278
|
+
} else if (
|
|
279
|
+
(e.key === 'ArrowLeft' && e.altKey && e.ctrlKey && doc.selected_node) ||
|
|
280
|
+
(e.key === 'ArrowLeft' && e.altKey && e.ctrlKey && e.shiftKey && doc.selected_node)
|
|
281
|
+
) {
|
|
276
282
|
const node = doc.selected_node;
|
|
277
283
|
const layout_count = doc.config.node_layouts[node.type];
|
|
278
284
|
if (layout_count > 1 && node?.layout) {
|
|
@@ -282,7 +288,10 @@
|
|
|
282
288
|
doc.apply(tr);
|
|
283
289
|
console.log('layout / count / prev_layout', node.layout, layout_count, prev_layout);
|
|
284
290
|
}
|
|
285
|
-
} else if (
|
|
291
|
+
} else if (
|
|
292
|
+
(e.key === 'ArrowDown' && e.altKey && e.ctrlKey && doc.selected_node) ||
|
|
293
|
+
(e.key === 'ArrowDown' && e.altKey && e.ctrlKey && e.shiftKey && doc.selected_node )
|
|
294
|
+
) {
|
|
286
295
|
const node = doc.selected_node;
|
|
287
296
|
|
|
288
297
|
if (doc.selection.type !== 'node') {
|
|
@@ -301,9 +310,10 @@
|
|
|
301
310
|
doc.config.inserters[next_type](tr);
|
|
302
311
|
tr.set_selection(old_selection);
|
|
303
312
|
doc.apply(tr);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
313
|
+
} else if (
|
|
314
|
+
(e.key === 'ArrowUp' && e.altKey && e.ctrlKey && doc.selected_node) ||
|
|
315
|
+
(e.key === 'ArrowUp' && e.altKey && e.ctrlKey && e.shiftKey && doc.selected_node)
|
|
316
|
+
) {
|
|
307
317
|
const node = doc.selected_node;
|
|
308
318
|
|
|
309
319
|
if (doc.selection.type !== 'node') {
|
|
@@ -340,7 +350,7 @@
|
|
|
340
350
|
doc.redo();
|
|
341
351
|
e.preventDefault();
|
|
342
352
|
e.stopPropagation();
|
|
343
|
-
} else if (e.key === 'Enter' && (e.
|
|
353
|
+
} else if (e.key === 'Enter' && (e.altKey)) {
|
|
344
354
|
doc.apply(doc.tr.insert_text('\n'));
|
|
345
355
|
e.preventDefault();
|
|
346
356
|
e.stopPropagation();
|
|
@@ -483,6 +493,8 @@
|
|
|
483
493
|
|
|
484
494
|
let focus_root_path = focus_root.dataset.path.split('.');
|
|
485
495
|
let anchor_root_path = anchor_root.dataset.path.split('.');
|
|
496
|
+
let focus_node_depth = focus_root_path.length;
|
|
497
|
+
let anchor_node_depth = anchor_root_path.length;
|
|
486
498
|
|
|
487
499
|
// HACK: this works only for one level nesting - should be done recursively to work generally
|
|
488
500
|
if (focus_root_path.length > anchor_root_path.length) {
|
|
@@ -512,6 +524,27 @@
|
|
|
512
524
|
focus_offset += 1;
|
|
513
525
|
}
|
|
514
526
|
|
|
527
|
+
// EDGE CASE: Exclude first node when anchor_node is an afer-node-cursor-trap
|
|
528
|
+
// in a non-collapsed forward selection
|
|
529
|
+
if (
|
|
530
|
+
anchor_node.parentElement?.dataset.type === 'after-node-cursor-trap' &&
|
|
531
|
+
!is_backwards &&
|
|
532
|
+
anchor_offset !== focus_offset
|
|
533
|
+
) {
|
|
534
|
+
anchor_offset += 1;
|
|
535
|
+
}
|
|
536
|
+
// EDGE CASE: Exclude first node when focus_node is an afer-node-cursor-trap
|
|
537
|
+
// in a non-collapsed backward selection
|
|
538
|
+
else if (
|
|
539
|
+
focus_node.parentElement?.dataset.type === 'after-node-cursor-trap' &&
|
|
540
|
+
is_backwards &&
|
|
541
|
+
anchor_offset !== focus_offset &&
|
|
542
|
+
// EDGE CASE: Only do correction when drag started from a deeper or equally deep anchor node
|
|
543
|
+
anchor_node_depth >= focus_node_depth
|
|
544
|
+
) {
|
|
545
|
+
focus_offset += 1;
|
|
546
|
+
}
|
|
547
|
+
|
|
515
548
|
return {
|
|
516
549
|
type: 'node',
|
|
517
550
|
path: anchor_root_path.slice(0, -1),
|
|
@@ -520,7 +553,6 @@
|
|
|
520
553
|
};
|
|
521
554
|
}
|
|
522
555
|
|
|
523
|
-
|
|
524
556
|
/**
|
|
525
557
|
* Extracts a PropertySelection from the current DOM selection.
|
|
526
558
|
*
|
|
@@ -721,13 +753,13 @@
|
|
|
721
753
|
const selection = doc.selection;
|
|
722
754
|
// The element that holds the property
|
|
723
755
|
const el = canvas_ref.querySelector(`[data-path="${selection.path.join('.')}"][data-type="property"]`);
|
|
724
|
-
const
|
|
756
|
+
const cursor_trap_selectable = el.querySelector('.svedit-selectable');
|
|
725
757
|
|
|
726
758
|
const range = window.document.createRange();
|
|
727
759
|
const dom_selection = window.getSelection();
|
|
728
760
|
|
|
729
761
|
// Select the entire cursor trap element contents and collapse to start
|
|
730
|
-
range.selectNodeContents(
|
|
762
|
+
range.selectNodeContents(cursor_trap_selectable);
|
|
731
763
|
range.collapse(true); // Collapse to start position
|
|
732
764
|
dom_selection.removeAllRanges();
|
|
733
765
|
dom_selection.addRange(range);
|
|
@@ -930,7 +962,9 @@
|
|
|
930
962
|
background: var(--editing-fill-color);
|
|
931
963
|
}
|
|
932
964
|
|
|
933
|
-
|
|
934
|
-
|
|
965
|
+
@media not (pointer: coarse) {
|
|
966
|
+
.svedit-canvas.hide-selection :global(::selection) {
|
|
967
|
+
background: transparent;
|
|
968
|
+
}
|
|
935
969
|
}
|
|
936
970
|
</style>
|