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 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 &ZeroWidthSpace; (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">&ZeroWidthSpace;</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 svedit-selectable"><br/></div>
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":";;;;;AAwBA,sFAA6D;yCApBlB,cAAc"}
1
+ {"version":3,"file":"CustomProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/CustomProperty.svelte.js"],"names":[],"mappings":";;;;;AA2BA,sFAA6D;yCAvBlB,cAAc"}
@@ -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 (e.key === 'ArrowRight' && e.altKey && e.ctrlKey && doc.selected_node) {
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 (e.key === 'ArrowLeft' && e.altKey && e.ctrlKey && doc.selected_node) {
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 (e.key === 'ArrowDown' && e.altKey && e.ctrlKey && doc.selected_node) {
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
- } else if (e.key === 'ArrowUp' && e.altKey && e.ctrlKey && doc.selected_node) {
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.shiftKey)) {
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 cursor_trap = el.querySelector('.cursor-trap');
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(cursor_trap);
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
- .svedit-canvas.hide-selection :global(::selection) {
934
- background: transparent;
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":";;;;;;;AA64BA;;OAAqD;iCAj4BgE,cAAc"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svedit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -16,7 +16,10 @@
16
16
  data-path={path.join('.')}
17
17
  style="anchor-name: --{path.join('-')};"
18
18
  >
19
- <div class="cursor-trap svedit-selectable"><br/></div>
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
 
@@ -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 (e.key === 'ArrowRight' && e.altKey && e.ctrlKey && doc.selected_node) {
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 (e.key === 'ArrowLeft' && e.altKey && e.ctrlKey && doc.selected_node) {
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 (e.key === 'ArrowDown' && e.altKey && e.ctrlKey && doc.selected_node) {
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
- } else if (e.key === 'ArrowUp' && e.altKey && e.ctrlKey && doc.selected_node) {
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.shiftKey)) {
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 cursor_trap = el.querySelector('.cursor-trap');
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(cursor_trap);
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
- .svedit-canvas.hide-selection :global(::selection) {
934
- background: transparent;
965
+ @media not (pointer: coarse) {
966
+ .svedit-canvas.hide-selection :global(::selection) {
967
+ background: transparent;
968
+ }
935
969
  }
936
970
  </style>