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/README.md
CHANGED
|
@@ -286,6 +286,44 @@ 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
|
+
- Never apply `position: relative` to the direct parent of `<AnnotatedTextProperty>`, it will cause a [weird Safari bug](https://bsky.app/profile/michaelaufreiter.com/post/3lxvdqyxc622s) to destroy the DOM.
|
|
325
|
+
- Never use an `<a>` tag inside a `contenteditable="true"` element, as it will cause unexpected behavior. Make it a `<div>` while editing, and an `<a>` in read-only mode.
|
|
326
|
+
|
|
289
327
|
## Full API docs?
|
|
290
328
|
|
|
291
329
|
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.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
|
+
import { char_slice, get_char_length } from './util.js';
|
|
3
4
|
|
|
4
|
-
/** @import { AnnotatedStringPropertyProps, AnnotationFragment } from './types.d.ts'; */
|
|
5
|
+
/** @import { AnnotatedStringPropertyProps, Annotation, AnnotationFragment } from './types.d.ts'; */
|
|
5
6
|
|
|
6
7
|
const svedit = getContext('svedit');
|
|
7
8
|
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
/**
|
|
20
21
|
* Converts text with annotations into renderable fragments for display.
|
|
21
22
|
* @param {string} text - The plain text content
|
|
22
|
-
* @param {Array<
|
|
23
|
+
* @param {Array<Annotation>} annotations - Array of annotations where each is [start_offset, end_offset, type, options?] (minimum 3 elements)
|
|
23
24
|
* @returns {Array<string|AnnotationFragment>} Array of fragments - strings for plain text, AnnotationFragment objects for annotated content
|
|
24
25
|
*/
|
|
25
26
|
function render_annotated_string(text, annotations) {
|
|
@@ -27,16 +28,16 @@
|
|
|
27
28
|
let last_index = 0;
|
|
28
29
|
|
|
29
30
|
// Sort annotations by start_offset
|
|
30
|
-
const sorted_annotations =
|
|
31
|
+
const sorted_annotations = [...annotations].sort((a, b) => a[0] - b[0]);
|
|
31
32
|
|
|
32
33
|
for (let [index, annotation] of sorted_annotations.entries()) {
|
|
33
|
-
// Add text before the annotation
|
|
34
|
+
// Add text before the annotation using character-aware slicing
|
|
34
35
|
if (annotation[0] > last_index) {
|
|
35
|
-
fragments.push(text
|
|
36
|
+
fragments.push(char_slice(text, last_index, annotation[0]));
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
// Add the annotated string
|
|
39
|
-
const annotated_content = text
|
|
39
|
+
// Add the annotated string using character-aware slicing
|
|
40
|
+
const annotated_content = char_slice(text, annotation[0], annotation[1]);
|
|
40
41
|
fragments.push({
|
|
41
42
|
type: annotation[2],
|
|
42
43
|
content: annotated_content,
|
|
@@ -47,9 +48,9 @@
|
|
|
47
48
|
last_index = annotation[1];
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
// Add any remaining text after the last annotation
|
|
51
|
-
if (last_index < text
|
|
52
|
-
fragments.push(text
|
|
51
|
+
// Add any remaining text after the last annotation using character-aware slicing
|
|
52
|
+
if (last_index < get_char_length(text)) {
|
|
53
|
+
fragments.push(char_slice(text, last_index));
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
return fragments;
|
|
@@ -66,35 +67,37 @@
|
|
|
66
67
|
}
|
|
67
68
|
</script>
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
-->{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
70
|
+
{#key plain_text}
|
|
71
|
+
<!-- ATTENTION: The comments are needed to prevent unwanted text nodes with whitespace. -->
|
|
72
|
+
<div
|
|
73
|
+
data-type="text"
|
|
74
|
+
data-path={path.join('.')}
|
|
75
|
+
style="anchor-name: --{path.join('-')};"
|
|
76
|
+
class="text svedit-selectable {css_class}"
|
|
77
|
+
class:empty={get_char_length(plain_text) === 0 && !(svedit.is_composing && is_focused)}
|
|
78
|
+
class:focused={is_focused}
|
|
79
|
+
placeholder={placeholder}
|
|
80
|
+
>
|
|
81
|
+
{#each fragments as fragment, index (index)}
|
|
82
|
+
{#if typeof fragment === 'string'}<!--
|
|
83
|
+
-->{fragment}<!--
|
|
84
|
+
-->{:else if fragment.type === 'emphasis'}<!--
|
|
85
|
+
--><em>{fragment.content}</em><!--
|
|
86
|
+
-->{:else if fragment.type === 'strong'}<!--
|
|
87
|
+
--><strong>{fragment.content}</strong><!--
|
|
88
|
+
-->{:else if fragment.type === 'link'}<!--
|
|
89
|
+
--><a
|
|
90
|
+
onclick={handle_link_click}
|
|
91
|
+
style="anchor-name: --{path.join('-') + '-' + fragment.annotation_index};"
|
|
92
|
+
href={fragment.data.href}
|
|
93
|
+
target={fragment.data.target || '_self'}>{fragment.content}</a><!--
|
|
94
|
+
-->{:else}<!--
|
|
95
|
+
-->{fragment.content}<!--
|
|
96
|
+
-->{/if}
|
|
97
|
+
{/each}<!--
|
|
98
|
+
--><br>
|
|
99
|
+
</div>
|
|
100
|
+
{/key}
|
|
98
101
|
|
|
99
102
|
<style>
|
|
100
103
|
.text {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnnotatedStringProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AnnotatedStringProperty.svelte.js"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"AnnotatedStringProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AnnotatedStringProperty.svelte.js"],"names":[],"mappings":";;;;;AAwFA,+HAAsE"}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { getContext } from 'svelte';
|
|
3
|
+
|
|
2
4
|
/** @import { CustomPropertyProps } from './types.d.ts'; */
|
|
3
5
|
|
|
6
|
+
const svedit = getContext('svedit');
|
|
7
|
+
|
|
4
8
|
/** @type {CustomPropertyProps} */
|
|
5
9
|
let {
|
|
6
10
|
path,
|
|
@@ -9,16 +13,23 @@
|
|
|
9
13
|
} = $props();
|
|
10
14
|
</script>
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
{#if svedit.editable}
|
|
17
|
+
<!-- NOTE: It's super crucial that there is no extra whitespace, to not introduce additional cursor positions in contenteditable -->
|
|
18
|
+
<div
|
|
19
|
+
class="property {css_class}"
|
|
20
|
+
data-type="property"
|
|
21
|
+
data-path={path.join('.')}
|
|
22
|
+
style="anchor-name: --{path.join('-')};"
|
|
23
|
+
>
|
|
24
|
+
<div class="cursor-trap">
|
|
25
|
+
<div class="svedit-selectable"><br/></div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="property-content" contenteditable="false">{@render children()}</div>
|
|
29
|
+
</div>
|
|
30
|
+
{:else}
|
|
31
|
+
{@render children()}
|
|
32
|
+
{/if}
|
|
22
33
|
|
|
23
34
|
<style>
|
|
24
35
|
.property {
|
|
@@ -3,6 +3,5 @@ type CustomProperty = {
|
|
|
3
3
|
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
4
|
$set?(props: Partial<CustomPropertyProps>): void;
|
|
5
5
|
};
|
|
6
|
-
declare const CustomProperty: import("svelte").Component<CustomPropertyProps, {}, "">;
|
|
7
|
-
import type { CustomPropertyProps } from './types.d.ts';
|
|
6
|
+
declare const CustomProperty: import("svelte").Component<import("./types.d.ts").CustomPropertyProps, {}, "">;
|
|
8
7
|
//# sourceMappingURL=CustomProperty.svelte.d.ts.map
|
|
@@ -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":";;;;;AAqCA,6GAA6D"}
|
|
@@ -153,7 +153,7 @@ export default class Document {
|
|
|
153
153
|
* @returns {Annotation|null}
|
|
154
154
|
*/
|
|
155
155
|
active_annotation(annotation_type: string): Annotation | null;
|
|
156
|
-
get_selected_plain_text():
|
|
156
|
+
get_selected_plain_text(): string;
|
|
157
157
|
get_selected_nodes(): any;
|
|
158
158
|
move_node_cursor(direction: any): void;
|
|
159
159
|
expand_node_selection(direction: any): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Document.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Document.svelte.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Document.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Document.svelte.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH;;;;;;;GAOG;AACH,uCAJ+E,CAAC,SAAlE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;IAAC,IAAI,EAAE,uBAAuB,CAAA;CAAC,CAAC,CAAE,UACnE,CAAC,GACC,CAAC,CAIb;AAED;;;;GAIG;AACH,wCAHW,MAAM,GACJ,IAAI,IAAI,aAAa,CAQjC;AAED;;;;GAIG;AACH,2DAHW,YAAY,GAAG,iBAAiB,GAC9B,MAAM,GAAG,IAAI,CASzB;AAED;;;;GAIG;AACH,0DAHW,cAAc,QAexB;AAuGD;IAgBE;;;;;;OAMG;IACH,oBANW,cAAc,kBACd,kBAAkB,0BAE1B;QAA4B,SAAS,GAA7B,SAAS;QACK,MAAM,GAApB,GAAG;KACb,EAmBA;IAxCD,oCAAoC;IACpC,WADW,SAAS,GAAG,SAAS,CACX;IACrB,YAAkB;IAClB,iBAAuB;IACvB,WAAiB;IACjB,eAAqB;IACrB,sBAA2B;IAG3B,kBAA6C;IAC7C,kBAAkE;IAGnE,mBAAmD;IAahD,uBAAoB;IAgBtB,mBAMC;IAGF,yBAkBC;IAGA,yBAWC;IAED;;;;OAIG;IACH,oBAHW,GAAG,QAKb;IAED;;;;OAIG;IACH,UAFa,WAAW,CASvB;IAED;;;;OAIG;IACH,mBAFW,WAAW,QAkBrB;IAED,aAWC;IAED,aAaC;IAED;;;;;;;;;;;;;;;;;;;OAmBG;IACH,UAlBW,YAAY,GAAC,MAAM,GACjB,GAAG,CAuDf;IAED;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,cAHW,YAAY,GACV;QAAC,IAAI,EAAE,UAAU,GAAC,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC,CAuBzD;IAED;;;;;OAKG;IACH,WAHW,GAAG,GACD,MAAM,GAAC,MAAM,CAQzB;IAED;;;;;;OAMG;IACH,mCAHW,MAAM,GACJ,UAAU,GAAC,IAAI,CAoB3B;IAGD,kCAOC;IAED,0BAOC;IAED,uCAsBC;IAED,4CASC;IAED,sBAmCC;IAED;;;;MAaC;IAKD,8BA4BC;IAED;;;OAGG;IACH,WAFa,kBAAkB,CAI9B;IAID,qNAWC;IAGD,uCAkBC;IAGD;;;OAGG;IACH,8BAHW,MAAM,GACJ,MAAM,EAAE,CASpB;CACF;6CA5pBS,cAAc;mCAAd,cAAc;kCAAd,cAAc;uCAAd,cAAc;oCAAd,cAAc;+BAAd,cAAc;wBAfA,yBAAyB;kCAevC,cAAc;gCAAd,cAAc;wCAAd,cAAc;4BAAd,cAAc"}
|
package/dist/Document.svelte.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Transaction from './Transaction.svelte.js';
|
|
2
|
+
import { char_slice } from './util.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @import {
|
|
@@ -239,7 +240,9 @@ export default class Document {
|
|
|
239
240
|
return node_id ? this.get(node_id) : null;
|
|
240
241
|
} else {
|
|
241
242
|
// we are assuming we are either in a text or property (=custom) selection
|
|
242
|
-
const
|
|
243
|
+
const node_id = this.selection?.path?.slice(0, -1);
|
|
244
|
+
if (!node_id) return null;
|
|
245
|
+
const owner_node = this.get(node_id);
|
|
243
246
|
return owner_node;
|
|
244
247
|
}
|
|
245
248
|
}
|
|
@@ -489,7 +492,7 @@ export default class Document {
|
|
|
489
492
|
const start = Math.min(this.selection.anchor_offset, this.selection.focus_offset);
|
|
490
493
|
const end = Math.max(this.selection.anchor_offset, this.selection.focus_offset);
|
|
491
494
|
const annotated_string = this.get(this.selection.path);
|
|
492
|
-
return annotated_string[0]
|
|
495
|
+
return char_slice(annotated_string[0], start, end);
|
|
493
496
|
}
|
|
494
497
|
|
|
495
498
|
get_selected_nodes() {
|
package/dist/Node.svelte
CHANGED
|
@@ -18,21 +18,25 @@
|
|
|
18
18
|
let is_first_node_array_child = $derived(is_inside_node_array && parseInt(String(path.at(-1)), 10) === 0);
|
|
19
19
|
</script>
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
{#if svedit.editable}
|
|
22
|
+
<div
|
|
23
|
+
class="node"
|
|
24
|
+
data-node-id={node.id}
|
|
25
|
+
data-path={path.join('.')}
|
|
26
|
+
data-type="node"
|
|
27
|
+
style="anchor-name: --{path.join('-')};"
|
|
28
|
+
>
|
|
29
|
+
{#if is_first_node_array_child}
|
|
30
|
+
<NodeCursorTrap {path} type="position-zero-cursor-trap" />
|
|
31
|
+
{/if}
|
|
32
|
+
{@render children()}
|
|
33
|
+
{#if is_inside_node_array}
|
|
34
|
+
<NodeCursorTrap {path} type="after-node-cursor-trap" />
|
|
35
|
+
{/if}
|
|
36
|
+
</div>
|
|
37
|
+
{:else}
|
|
31
38
|
{@render children()}
|
|
32
|
-
|
|
33
|
-
<NodeCursorTrap {path} type="after-node-cursor-trap" />
|
|
34
|
-
{/if}
|
|
35
|
-
</div>
|
|
39
|
+
{/if}
|
|
36
40
|
|
|
37
41
|
<style>
|
|
38
42
|
div {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Node.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Node.svelte.js"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"Node.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Node.svelte.js"],"names":[],"mappings":";;;;;AA2CA,yFAAmD"}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
20
|
<div class={css_class} data-type="node_array" data-path={path.join('.')}>
|
|
21
|
-
{#if nodes.length === 0}
|
|
21
|
+
{#if nodes.length === 0 && svedit.editable}
|
|
22
22
|
<!--
|
|
23
23
|
Experimental: We'll let .empty-node-array act like a node, so the existing
|
|
24
24
|
code paths for selection mapping will work as expected.
|