svedit 0.6.3 → 0.7.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 +117 -45
- package/dist/AnnotatedTextProperty.svelte +19 -16
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.d.ts +3 -3
- package/dist/Command.svelte.js +5 -5
- package/dist/CustomProperty.svelte +5 -4
- package/dist/Node.svelte +29 -14
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +29 -9
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/NodeCaret.svelte +71 -0
- package/dist/NodeCaret.svelte.d.ts +27 -0
- package/dist/NodeCaret.svelte.d.ts.map +1 -0
- package/dist/NodeGap.svelte +249 -0
- package/dist/NodeGap.svelte.d.ts +20 -0
- package/dist/NodeGap.svelte.d.ts.map +1 -0
- package/dist/NodeGapMarkers.svelte +456 -0
- package/dist/NodeGapMarkers.svelte.d.ts +12 -0
- package/dist/NodeGapMarkers.svelte.d.ts.map +1 -0
- package/dist/NodeSelectionMarkers.svelte +49 -0
- package/dist/NodeSelectionMarkers.svelte.d.ts +7 -0
- package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -0
- package/dist/Session.svelte.d.ts +6 -6
- package/dist/Session.svelte.d.ts.map +1 -1
- package/dist/Session.svelte.js +13 -69
- package/dist/Svedit.svelte +158 -90
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +1 -1
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +10 -70
- package/dist/doc_utils.d.ts +12 -0
- package/dist/doc_utils.d.ts.map +1 -1
- package/dist/doc_utils.js +61 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/node_gap_computation.svelte.d.ts +5 -0
- package/dist/node_gap_computation.svelte.d.ts.map +1 -0
- package/dist/node_gap_computation.svelte.js +621 -0
- package/dist/styles/svedit-colors.css +8 -0
- package/dist/transforms.svelte.d.ts.map +1 -1
- package/dist/transforms.svelte.js +7 -10
- package/dist/types.d.ts +56 -68
- package/dist/utils.d.ts +0 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +10 -10
- package/package.json +21 -21
- package/src/lib/AnnotatedTextProperty.svelte +19 -16
- package/src/lib/Command.svelte.js +5 -5
- package/src/lib/CustomProperty.svelte +5 -4
- package/src/lib/Node.svelte +29 -14
- package/src/lib/NodeArrayProperty.svelte +29 -9
- package/src/lib/NodeCaret.svelte +71 -0
- package/src/lib/NodeGap.svelte +249 -0
- package/src/lib/NodeGapMarkers.svelte +456 -0
- package/src/lib/NodeSelectionMarkers.svelte +49 -0
- package/src/lib/Session.svelte.js +13 -69
- package/src/lib/Svedit.svelte +158 -90
- package/src/lib/Transaction.svelte.js +10 -70
- package/src/lib/doc_utils.js +61 -2
- package/src/lib/index.js +4 -0
- package/src/lib/node_gap_computation.svelte.js +621 -0
- package/src/lib/styles/svedit-colors.css +8 -0
- package/src/lib/transforms.svelte.js +7 -10
- package/src/lib/types.d.ts +56 -68
- package/src/lib/utils.js +10 -10
package/dist/types.d.ts
CHANGED
|
@@ -93,59 +93,53 @@ export type PrimitiveType = ScalarType | ArrayType | RichType;
|
|
|
93
93
|
*/
|
|
94
94
|
export type PropertyType = PrimitiveType | ReferenceType;
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
* Converts a document node schema definition to its inferred JS type.
|
|
144
|
-
* Handles the {type: "..."} wrapper structure used in document schemas.
|
|
145
|
-
*/
|
|
146
|
-
export type DocumentNodeToJs<S extends NodeSchema> = { id: string; type: string } & {
|
|
147
|
-
[K in keyof S['properties']]: DocumentSchemaValueToJs<S['properties'][K]['type']>;
|
|
148
|
-
};
|
|
96
|
+
// TODO: We may later want more sophisticated type inference, so that typing
|
|
97
|
+
// e.g. `session.get('image')` auto-completes the node's properties based on
|
|
98
|
+
// the schema definition. The types below are a starting point for that.
|
|
99
|
+
//
|
|
100
|
+
// export type DocumentSchemaPrimitive =
|
|
101
|
+
// | 'string'
|
|
102
|
+
// | 'number'
|
|
103
|
+
// | 'boolean'
|
|
104
|
+
// | 'integer'
|
|
105
|
+
// | 'datetime'
|
|
106
|
+
// | 'string_array'
|
|
107
|
+
// | 'number_array'
|
|
108
|
+
// | 'boolean_array'
|
|
109
|
+
// | 'integer_array'
|
|
110
|
+
// | 'annotated_text'
|
|
111
|
+
// | 'node'
|
|
112
|
+
// | 'node_array';
|
|
113
|
+
//
|
|
114
|
+
// export type DocumentSchemaValueToJs<T> = T extends 'string'
|
|
115
|
+
// ? string
|
|
116
|
+
// : T extends 'number'
|
|
117
|
+
// ? number
|
|
118
|
+
// : T extends 'boolean'
|
|
119
|
+
// ? boolean
|
|
120
|
+
// : T extends 'integer'
|
|
121
|
+
// ? number
|
|
122
|
+
// : T extends 'datetime'
|
|
123
|
+
// ? string
|
|
124
|
+
// : T extends 'string_array'
|
|
125
|
+
// ? Array<string>
|
|
126
|
+
// : T extends 'number_array'
|
|
127
|
+
// ? Array<number>
|
|
128
|
+
// : T extends 'boolean_array'
|
|
129
|
+
// ? Array<boolean>
|
|
130
|
+
// : T extends 'integer_array'
|
|
131
|
+
// ? Array<number>
|
|
132
|
+
// : T extends 'annotated_text'
|
|
133
|
+
// ? AnnotatedText
|
|
134
|
+
// : T extends 'node'
|
|
135
|
+
// ? string
|
|
136
|
+
// : T extends 'node_array'
|
|
137
|
+
// ? Array<string>
|
|
138
|
+
// : never;
|
|
139
|
+
//
|
|
140
|
+
// export type DocumentNodeToJs<S extends NodeSchema> = { id: string; type: string } & {
|
|
141
|
+
// [K in keyof S['properties']]: DocumentSchemaValueToJs<S['properties'][K]['type']>;
|
|
142
|
+
// };
|
|
149
143
|
|
|
150
144
|
/**
|
|
151
145
|
* A property that stores an annotated text with required allow_newlines setting.
|
|
@@ -337,6 +331,8 @@ export type AnnotatedTextPropertyProps = {
|
|
|
337
331
|
class?: string;
|
|
338
332
|
/** A placeholder to be rendered for empty content */
|
|
339
333
|
placeholder?: string;
|
|
334
|
+
/** Rest props to spread onto the rendered element (e.g. href, target, etc.) */
|
|
335
|
+
[key: string]: any;
|
|
340
336
|
};
|
|
341
337
|
|
|
342
338
|
/**
|
|
@@ -351,6 +347,8 @@ export type CustomPropertyProps = {
|
|
|
351
347
|
class?: string;
|
|
352
348
|
/** The content of the custom property (e.g. an image) */
|
|
353
349
|
children: Snippet;
|
|
350
|
+
/** Rest props to spread onto the rendered element */
|
|
351
|
+
[key: string]: any;
|
|
354
352
|
};
|
|
355
353
|
|
|
356
354
|
/**
|
|
@@ -361,10 +359,10 @@ export type NodeArrayPropertyProps = {
|
|
|
361
359
|
path: DocumentPath;
|
|
362
360
|
/** Optional custom HTML tag */
|
|
363
361
|
tag?: string;
|
|
364
|
-
/** Optional custom HTML tag */
|
|
365
|
-
tag?: string;
|
|
366
362
|
/** The `class` attribute on the container element */
|
|
367
363
|
class?: string;
|
|
364
|
+
/** Rest props to spread onto the rendered element (e.g. href, target, etc.) */
|
|
365
|
+
[key: string]: any;
|
|
368
366
|
};
|
|
369
367
|
|
|
370
368
|
/**
|
|
@@ -379,22 +377,12 @@ export type NodeProps = {
|
|
|
379
377
|
class?: string;
|
|
380
378
|
/** The type-specific content of the node */
|
|
381
379
|
children: Snippet;
|
|
380
|
+
/** Rest props to spread onto the rendered element (e.g. href, target, etc.) */
|
|
381
|
+
[key: string]: any;
|
|
382
382
|
};
|
|
383
383
|
|
|
384
384
|
/**
|
|
385
|
-
* Props for the
|
|
386
|
-
*/
|
|
387
|
-
export type CustomPropertyProps = {
|
|
388
|
-
/** The full path to the property */
|
|
389
|
-
path: DocumentPath;
|
|
390
|
-
/** The `class` attribute on the content element */
|
|
391
|
-
class?: string;
|
|
392
|
-
/** The content of the custom property (e.g. an image) */
|
|
393
|
-
children: Snippet;
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Props for the CustomProperty component
|
|
385
|
+
* Props for the Svedit component
|
|
398
386
|
*/
|
|
399
387
|
export type SveditProps = {
|
|
400
388
|
/** The session instance */
|
package/dist/utils.d.ts
CHANGED
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.js"],"names":[],"mappings":"AAMA;;;GAGG;AACH,qCAFa,OAAO,CAanB;AAGD;;;GAGG;AACH,6CAFa,OAAO,CAUnB;AAED;;;;;;;;;;;;;GAaG;AACH,qCARW,MAAM,GACJ,MAAM,CASlB;AAID;;;;;;;;;;;;;;GAcG;AACH,iCATW,MAAM,SACN,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;;;;;;GAcG;AACH,gCATW,MAAM,SACN,MAAM,QACN,MAAM,GACJ,MAAM,CAYlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,gBACN,MAAM,GACJ,MAAM,CAmBlB;AAED;;;;;;;;;;;;;GAaG;AACH,0CARW,MAAM,eACN,MAAM,GACJ,MAAM,CAelB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,4DAZW,aAAa,eACb,MAAM,GACJ,CAAC,aAAa,EAAE,aAAa,CAAC,CA6C1C;AAED;;;;;;;;;;;;;GAaG;AACH,gDARW,aAAa,eACb,aAAa,GACX,aAAa,CA4CzB;AAED;;;;;;;;GAQG;AACH,qCANW,MAAM,GACJ,MAAM,CAUlB;AAED,uEAgCC;AAED;;;;;GAKG;AACH,gDAHW,SAAS,GACP,cAAc,GAAG,IAAI,CAWjC;AAED,gEAMC;mCAhVwE,cAAc;+BAAd,cAAc;oCAAd,cAAc"}
|
package/dist/utils.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* @import { Annotation, AnnotatedText, SelectionRange, Selection } from './types.d.ts';
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
const SEGMENTER = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Detect if the current browser is on a mobile device
|
|
7
9
|
* @returns {boolean} true if mobile browser, false otherwise
|
|
@@ -19,6 +21,7 @@ export function is_mobile_browser() {
|
|
|
19
21
|
);
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
// ‼️‼️‼️‼️‼️‼️ UNUSED UTILITY BELOW ‼️‼️‼️‼️‼️‼️
|
|
22
25
|
/**
|
|
23
26
|
* Detect if the current browser is Chrome on desktop
|
|
24
27
|
* @returns {boolean} true if Chrome desktop browser, false otherwise
|
|
@@ -48,10 +51,11 @@ export function is_chrome_desktop_browser() {
|
|
|
48
51
|
* get_char_length('👋🏽') // Returns: 1 (not 4 - skin tone modifier treated as single char)
|
|
49
52
|
*/
|
|
50
53
|
export function get_char_length(str) {
|
|
51
|
-
|
|
52
|
-
return [...segmenter.segment(str)].length;
|
|
54
|
+
return [...SEGMENTER.segment(str)].length;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
|
|
58
|
+
// ‼️‼️‼️‼️‼️‼️ UNUSED UTILITY BELOW ‼️‼️‼️‼️‼️‼️
|
|
55
59
|
/**
|
|
56
60
|
* Get a single character at the specified position (accounting for multi-byte characters)
|
|
57
61
|
*
|
|
@@ -68,8 +72,7 @@ export function get_char_length(str) {
|
|
|
68
72
|
* get_char_at('👋🏽', 0) // Returns: '👋🏽' (skin tone modifier included)
|
|
69
73
|
*/
|
|
70
74
|
export function get_char_at(str, index) {
|
|
71
|
-
const
|
|
72
|
-
const segments = [...segmenter.segment(str)];
|
|
75
|
+
const segments = [...SEGMENTER.segment(str)];
|
|
73
76
|
return segments[index].segment;
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -89,8 +92,7 @@ export function get_char_at(str, index) {
|
|
|
89
92
|
* char_slice('a👋🏽b', 1, 2) // Returns: '👋🏽' (skin tone modifier included)
|
|
90
93
|
*/
|
|
91
94
|
export function char_slice(str, start, end = undefined) {
|
|
92
|
-
const
|
|
93
|
-
const segments = [...segmenter.segment(str)];
|
|
95
|
+
const segments = [...SEGMENTER.segment(str)];
|
|
94
96
|
return segments
|
|
95
97
|
.slice(start, end)
|
|
96
98
|
.map((s) => s.segment)
|
|
@@ -112,8 +114,7 @@ export function char_slice(str, start, end = undefined) {
|
|
|
112
114
|
* utf16_to_char_offset("a😀b", 3) // Returns: 2 (position after emoji)
|
|
113
115
|
*/
|
|
114
116
|
export function utf16_to_char_offset(str, utf16_offset) {
|
|
115
|
-
const
|
|
116
|
-
const segments = [...segmenter.segment(str)];
|
|
117
|
+
const segments = [...SEGMENTER.segment(str)];
|
|
117
118
|
let char_offset = 0;
|
|
118
119
|
let utf16_count = 0;
|
|
119
120
|
|
|
@@ -142,8 +143,7 @@ export function utf16_to_char_offset(str, utf16_offset) {
|
|
|
142
143
|
* char_to_utf16_offset("a😀b", 2) // Returns: 3 (UTF-16 position after emoji)
|
|
143
144
|
*/
|
|
144
145
|
export function char_to_utf16_offset(str, char_offset) {
|
|
145
|
-
const
|
|
146
|
-
const segments = [...segmenter.segment(str)];
|
|
146
|
+
const segments = [...SEGMENTER.segment(str)];
|
|
147
147
|
let utf16_offset = 0;
|
|
148
148
|
|
|
149
149
|
for (let i = 0; i < Math.min(char_offset, segments.length); i++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svedit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev",
|
|
@@ -39,29 +39,29 @@
|
|
|
39
39
|
"svelte": "^5.0.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@eslint/compat": "^
|
|
43
|
-
"@eslint/js": "^
|
|
42
|
+
"@eslint/compat": "^2.0.3",
|
|
43
|
+
"@eslint/js": "^10.0.1",
|
|
44
44
|
"@sveltejs/adapter-auto": "^7.0.0",
|
|
45
|
-
"@sveltejs/kit": "^2.
|
|
46
|
-
"@sveltejs/package": "^2.5.
|
|
47
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
48
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
49
|
-
"@typescript-eslint/parser": "^8.
|
|
50
|
-
"@vitest/browser": "^4.0
|
|
51
|
-
"@vitest/browser-preview": "^4.0
|
|
52
|
-
"eslint": "^
|
|
45
|
+
"@sveltejs/kit": "^2.55.0",
|
|
46
|
+
"@sveltejs/package": "^2.5.7",
|
|
47
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
48
|
+
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
|
49
|
+
"@typescript-eslint/parser": "^8.57.0",
|
|
50
|
+
"@vitest/browser": "^4.1.0",
|
|
51
|
+
"@vitest/browser-preview": "^4.1.0",
|
|
52
|
+
"eslint": "^10.0.3",
|
|
53
53
|
"eslint-config-prettier": "^10.1.8",
|
|
54
|
-
"eslint-plugin-svelte": "^3.
|
|
55
|
-
"globals": "^
|
|
54
|
+
"eslint-plugin-svelte": "^3.15.1",
|
|
55
|
+
"globals": "^17.4.0",
|
|
56
56
|
"nanoid": "^5.1.6",
|
|
57
|
-
"prettier": "^3.
|
|
58
|
-
"prettier-plugin-svelte": "^3.
|
|
59
|
-
"publint": "^0.3.
|
|
60
|
-
"svelte": "^5.
|
|
61
|
-
"svelte-check": "^4.
|
|
62
|
-
"vite": "^
|
|
57
|
+
"prettier": "^3.8.1",
|
|
58
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
59
|
+
"publint": "^0.3.18",
|
|
60
|
+
"svelte": "^5.53.12",
|
|
61
|
+
"svelte-check": "^4.4.5",
|
|
62
|
+
"vite": "^8.0.0",
|
|
63
63
|
"typescript": "^5.9.3",
|
|
64
|
-
"vitest": "^4.0
|
|
65
|
-
"vitest-browser-svelte": "^2.0
|
|
64
|
+
"vitest": "^4.1.0",
|
|
65
|
+
"vitest-browser-svelte": "^2.1.0"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
const svedit = getContext('svedit');
|
|
8
8
|
|
|
9
9
|
/** @type {AnnotatedTextPropertyProps} */
|
|
10
|
-
let { path, class: css_class, placeholder = '', tag = 'div' } = $props();
|
|
10
|
+
let { path, class: css_class, placeholder = '', tag = 'div', style = '', ...rest } = $props();
|
|
11
11
|
|
|
12
12
|
let is_focused = $derived.by(() => {
|
|
13
13
|
return (
|
|
@@ -26,7 +26,12 @@
|
|
|
26
26
|
);
|
|
27
27
|
|
|
28
28
|
// Get selection highlight range if not inside an annotation
|
|
29
|
+
// Only render selection highlight when canvas is NOT focused.
|
|
30
|
+
// This avoids DOM mutations (splitting text nodes for highlight spans)
|
|
31
|
+
// while the user is actively selecting, which would cause selection
|
|
32
|
+
// feedback loops and scroll-to-focus issues.
|
|
29
33
|
let selection_highlight_range = $derived.by(() => {
|
|
34
|
+
if (svedit.canvas_focused) return null;
|
|
30
35
|
if (is_collapsed) return null;
|
|
31
36
|
if (!is_focused) return null;
|
|
32
37
|
if (svedit.session.active_annotation()) return null;
|
|
@@ -101,17 +106,19 @@
|
|
|
101
106
|
<!-- ATTENTION: The comments are needed to prevent unwanted text nodes with whitespace. -->
|
|
102
107
|
<!-- We need to use <br> so the element is reachable by Arrow Up/Down navigation. -->
|
|
103
108
|
<!-- But as soon as the element is focused, we get rid of the <br> as it causes issues with caret positioning. -->
|
|
104
|
-
<!-- Before, we were just hiding it on focus, but that caused the
|
|
109
|
+
<!-- Before, we were just hiding it on focus, but that caused the caret to disappear on Safari Desktop (tested with v26). -->
|
|
105
110
|
<!-- Edge Case: Shift Enter stops working if <br> is not present on a non-empty text property. -->
|
|
106
111
|
<svelte:element
|
|
107
112
|
this={tag}
|
|
108
113
|
data-type="text"
|
|
109
114
|
data-path={path.join('.')}
|
|
110
|
-
style="anchor-name: --{path.join('-')};"
|
|
115
|
+
style="anchor-name: --{path.join('-')};{style}"
|
|
111
116
|
class="text svedit-selectable {css_class}"
|
|
112
117
|
class:empty={is_empty}
|
|
113
118
|
class:focused={is_focused}
|
|
119
|
+
class:editable={svedit.editable}
|
|
114
120
|
{placeholder}
|
|
121
|
+
{...rest}
|
|
115
122
|
>
|
|
116
123
|
{#each fragments as fragment, index (index)}
|
|
117
124
|
{#if typeof fragment === 'string'}{fragment}{:else if fragment.type === 'selection_highlight'}<span
|
|
@@ -134,10 +141,6 @@
|
|
|
134
141
|
white-space: pre-wrap;
|
|
135
142
|
overflow-wrap: anywhere;
|
|
136
143
|
box-sizing: content-box;
|
|
137
|
-
|
|
138
|
-
&.heading1 {
|
|
139
|
-
text-wrap: var(--text-wrap);
|
|
140
|
-
}
|
|
141
144
|
}
|
|
142
145
|
/* We switch from ::before to ::after when the element is focused. So the the caret is always before the placeholder. */
|
|
143
146
|
[placeholder].empty:not(.focused)::before,
|
|
@@ -152,19 +155,19 @@
|
|
|
152
155
|
caret-color: transparent;
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
/* Disable text-transform when editable and focused so users see original text */
|
|
159
|
+
.text.editable.focused {
|
|
160
|
+
text-transform: none !important;
|
|
161
|
+
}
|
|
159
162
|
|
|
160
163
|
/* Dim the selection highlight when canvas loses native focus */
|
|
161
|
-
:global(.svedit-canvas:not(:focus)) .selection-highlight {
|
|
162
|
-
background: oklch(from var(--editing-fill
|
|
164
|
+
:global(.svedit-canvas:not(:focus-within)) .selection-highlight {
|
|
165
|
+
background: oklch(from var(--svedit-editing-fill) l 0 h / alpha);
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
/* Make a collapsed
|
|
166
|
-
:global(.svedit-canvas:not(:focus)) .selection-highlight:empty {
|
|
168
|
+
/* Make a collapsed caret visible */
|
|
169
|
+
:global(.svedit-canvas:not(:focus-within)) .selection-highlight:empty {
|
|
167
170
|
background: none;
|
|
168
|
-
outline: 0.5px solid oklch(from var(--editing-stroke
|
|
171
|
+
outline: 0.5px solid oklch(from var(--svedit-editing-stroke) l 0 h / alpha);
|
|
169
172
|
}
|
|
170
173
|
</style>
|
|
@@ -120,13 +120,13 @@ export class ToggleAnnotationCommand extends Command {
|
|
|
120
120
|
is_enabled() {
|
|
121
121
|
const { session, editable } = this.context;
|
|
122
122
|
const has_annotation = session.active_annotation(this.node_type);
|
|
123
|
-
const
|
|
123
|
+
const no_annotation_and_caret_not_collapsed =
|
|
124
124
|
!session.active_annotation() && !is_selection_collapsed(session.selection);
|
|
125
125
|
|
|
126
126
|
return (
|
|
127
127
|
editable &&
|
|
128
128
|
session.selection?.type === 'text' &&
|
|
129
|
-
(has_annotation ||
|
|
129
|
+
(has_annotation || no_annotation_and_caret_not_collapsed)
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
132
|
|
|
@@ -136,7 +136,7 @@ export class ToggleAnnotationCommand extends Command {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
|
-
* Command that adds a new line character at the current
|
|
139
|
+
* Command that adds a new line character at the current caret position.
|
|
140
140
|
* Only works in text selections where newlines are allowed.
|
|
141
141
|
* Disabled on mobile browsers where Shift+Enter has different behavior.
|
|
142
142
|
*/
|
|
@@ -159,7 +159,7 @@ export class AddNewLineCommand extends Command {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
|
-
* Command that breaks a text node at the
|
|
162
|
+
* Command that breaks a text node at the caret position.
|
|
163
163
|
* Creates a new node and splits the content between the current and new node.
|
|
164
164
|
* Only works in text selections.
|
|
165
165
|
*/
|
|
@@ -293,7 +293,7 @@ export class SelectAllCommand extends Command {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
/**
|
|
296
|
-
* Command that inserts a default node at the current
|
|
296
|
+
* Command that inserts a default node at the current caret position.
|
|
297
297
|
* Only works when a collapsed node selection is active.
|
|
298
298
|
*/
|
|
299
299
|
export class InsertDefaultNodeCommand extends Command {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const svedit = getContext('svedit');
|
|
6
6
|
|
|
7
7
|
/** @type {CustomPropertyProps} */
|
|
8
|
-
let { path, tag = 'div', class: css_class, children } = $props();
|
|
8
|
+
let { path, tag = 'div', class: css_class, children, style, ...rest } = $props();
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
11
|
<svelte:element
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
class={css_class}
|
|
14
14
|
data-type="property"
|
|
15
15
|
data-path={path.join('.')}
|
|
16
|
-
style="anchor-name: --{path.join('-')};"
|
|
16
|
+
style="anchor-name: --{path.join('-')};{style ? ` ${style}` : ''}"
|
|
17
|
+
{...rest}
|
|
17
18
|
>
|
|
18
19
|
{#if svedit.editable}
|
|
19
|
-
<div class="
|
|
20
|
+
<div class="property-selectable">
|
|
20
21
|
<div class="svedit-selectable"><br /></div>
|
|
21
22
|
</div>
|
|
22
23
|
{/if}
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
[data-type='property'] {
|
|
29
30
|
position: relative;
|
|
30
31
|
}
|
|
31
|
-
.
|
|
32
|
+
.property-selectable {
|
|
32
33
|
position: absolute;
|
|
33
34
|
top: 0;
|
|
34
35
|
left: 0;
|
package/src/lib/Node.svelte
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
|
+
import DefaultNodeGap from './NodeGap.svelte';
|
|
3
4
|
|
|
4
5
|
/** @import { NodeProps } from './types.d.ts'; */
|
|
5
6
|
|
|
6
7
|
const svedit = getContext('svedit');
|
|
7
|
-
let
|
|
8
|
+
let NodeGap = $derived(svedit.session.config.system_components?.NodeGap ?? DefaultNodeGap);
|
|
8
9
|
|
|
9
10
|
/** @type {NodeProps} */
|
|
10
|
-
let { path, children, tag = 'div', class: css_class } = $props();
|
|
11
|
+
let { path, children, tag = 'div', class: css_class, style = '', ...rest } = $props();
|
|
11
12
|
|
|
12
|
-
// NOTE: When the next to last path segment is a node_array property, the node is wrapped in a node_array
|
|
13
|
-
let is_inside_node_array = $derived(
|
|
14
|
-
path.length > 1 && svedit.session.inspect(path.slice(0, -1))?.type === 'node_array'
|
|
15
|
-
);
|
|
16
13
|
let node = $derived(svedit.session.get(path));
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
|
|
15
|
+
// NodeArrayProperty sets this context — its presence means we're in a node_array.
|
|
16
|
+
// Using context avoids expensive session.inspect() and session.get() calls
|
|
17
|
+
// that would create O(N) reactive subscriptions to the parent array.
|
|
18
|
+
const node_array_meta = getContext('node_array_meta');
|
|
19
|
+
const is_inside_node_array = !!node_array_meta;
|
|
20
|
+
let child_index = $derived(parseInt(String(path.at(-1)), 10));
|
|
21
|
+
let is_first_node_array_child = $derived(is_inside_node_array && child_index === 0);
|
|
22
|
+
let is_last_node_array_child = $derived(
|
|
23
|
+
is_inside_node_array && child_index === node_array_meta.length - 1
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Drives lazy anchor positioning for node gaps. Gaps are always in the
|
|
27
|
+
// DOM (stable structure for selection), but only anchor-positioned when
|
|
28
|
+
// near the viewport to avoid O(N) layout cost.
|
|
29
|
+
let is_near_viewport = $derived(
|
|
30
|
+
svedit.is_near_viewport?.(path) ?? true
|
|
19
31
|
);
|
|
20
32
|
</script>
|
|
21
33
|
|
|
@@ -26,19 +38,22 @@
|
|
|
26
38
|
data-node-id={node.id}
|
|
27
39
|
data-path={path.join('.')}
|
|
28
40
|
data-type="node"
|
|
29
|
-
style="anchor-name: --{path.join('-')};"
|
|
41
|
+
style="anchor-name: --{path.join('-')};{style}"
|
|
42
|
+
{...rest}
|
|
30
43
|
>
|
|
31
44
|
{#if svedit.editable && is_first_node_array_child}
|
|
32
|
-
<
|
|
45
|
+
<NodeGap {path} type="gap-before" positioned={is_near_viewport} />
|
|
33
46
|
{/if}
|
|
34
47
|
{@render children()}
|
|
35
48
|
{#if svedit.editable && is_inside_node_array}
|
|
36
|
-
<
|
|
49
|
+
<NodeGap {path} type="gap-after" last={is_last_node_array_child} positioned={is_near_viewport} />
|
|
37
50
|
{/if}
|
|
38
51
|
</svelte:element>
|
|
39
52
|
|
|
40
53
|
<style>
|
|
41
|
-
|
|
42
|
-
position
|
|
54
|
+
[data-type='node'] {
|
|
55
|
+
/** any other position than static will break the anchor positioning of node gaps and node gap-marker */
|
|
56
|
+
/* For developers who need to position their node with `position: absolute` or `position: relative`, they need to wrap their node in a div */
|
|
57
|
+
position: static !important;
|
|
43
58
|
}
|
|
44
|
-
</style>
|
|
59
|
+
</style>
|
|
@@ -1,24 +1,39 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { getContext } from 'svelte';
|
|
2
|
+
import { getContext, setContext } from 'svelte';
|
|
3
3
|
import UnknownNode from './UnknownNode.svelte';
|
|
4
4
|
import { snake_to_pascal } from './utils.js';
|
|
5
|
+
import DefaultNodeGap from './NodeGap.svelte';
|
|
6
|
+
import DefaultNodeGapMarkers from './NodeGapMarkers.svelte';
|
|
5
7
|
|
|
6
8
|
/** @import { NodeArrayPropertyProps } from './types.d.ts'; */
|
|
7
9
|
|
|
8
10
|
const svedit = getContext('svedit');
|
|
9
|
-
let
|
|
11
|
+
let NodeGap = $derived(svedit.session.config.system_components?.NodeGap ?? DefaultNodeGap);
|
|
12
|
+
let NodeGapMarkers = $derived(svedit.session.config.system_components?.NodeGapMarkers ?? DefaultNodeGapMarkers);
|
|
10
13
|
|
|
11
14
|
/** @type {NodeArrayPropertyProps} */
|
|
12
|
-
let { path, tag = 'div', class: css_class } = $props();
|
|
15
|
+
let { path, tag = 'div', class: css_class, style = '', ...rest } = $props();
|
|
13
16
|
|
|
14
17
|
let nodes = $derived(
|
|
15
18
|
svedit.session
|
|
16
19
|
.get(path)
|
|
17
20
|
.map(/** @param {string} node_id */ (node_id) => svedit.session.get(node_id))
|
|
18
21
|
);
|
|
19
|
-
</script>
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
// Expose array metadata to child Node.svelte instances via context,
|
|
24
|
+
// avoiding expensive session.inspect() / session.get() calls per node.
|
|
25
|
+
setContext('node_array_meta', {
|
|
26
|
+
get length() { return nodes.length; }
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
<!-- we use the anchor of node_array in Overlays.svelte to position the last insertion point in a horizontal layout based on the right edge of the container -->
|
|
30
|
+
<svelte:element
|
|
31
|
+
this={tag}
|
|
32
|
+
class={css_class}
|
|
33
|
+
data-type="node_array"
|
|
34
|
+
data-path={path.join('.')}
|
|
35
|
+
style="anchor-name: --{path.join('-')};{style ? ` ${style}` : ''}" {...rest}
|
|
36
|
+
>
|
|
22
37
|
{#if nodes.length === 0 && svedit.editable}
|
|
23
38
|
<!--
|
|
24
39
|
Experimental: We'll let .empty-node-array act like a node, so the existing
|
|
@@ -32,10 +47,12 @@
|
|
|
32
47
|
data-type="node"
|
|
33
48
|
style="anchor-name: --{[...path, 0].join(
|
|
34
49
|
'-'
|
|
35
|
-
)};
|
|
36
|
-
>
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
)}; min-height: 40px; min-width: 24px;"
|
|
51
|
+
></div>
|
|
52
|
+
<!-- Sibling (not child) of .empty-node-array so its .svedit-selectable
|
|
53
|
+
resolves anchor positioning against the shared containing block,
|
|
54
|
+
not the placeholder which inherits .node positioning styles. -->
|
|
55
|
+
<NodeGap path={[...path, 0]} type="gap-before" empty />
|
|
39
56
|
{/if}
|
|
40
57
|
{#each nodes as node, index (index)}
|
|
41
58
|
{@const Component = svedit.session.config.node_components[snake_to_pascal(node.type)]}
|
|
@@ -45,4 +62,7 @@
|
|
|
45
62
|
<UnknownNode path={[...path, index]} />
|
|
46
63
|
{/if}
|
|
47
64
|
{/each}
|
|
65
|
+
{#if svedit.editable && NodeGapMarkers}
|
|
66
|
+
<NodeGapMarkers {path} />
|
|
67
|
+
{/if}
|
|
48
68
|
</svelte:element>
|