svedit 0.5.0 → 0.6.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 +761 -120
- package/dist/AnnotatedTextProperty.svelte +122 -75
- package/dist/AnnotatedTextProperty.svelte.d.ts.map +1 -1
- package/dist/Command.svelte.d.ts +118 -0
- package/dist/Command.svelte.d.ts.map +1 -0
- package/dist/Command.svelte.js +314 -0
- package/dist/CustomProperty.svelte +31 -42
- package/dist/CustomProperty.svelte.d.ts.map +1 -1
- package/dist/KeyMapper.svelte.d.ts +36 -0
- package/dist/KeyMapper.svelte.d.ts.map +1 -0
- package/dist/KeyMapper.svelte.js +141 -0
- package/dist/Node.svelte +33 -35
- package/dist/Node.svelte.d.ts.map +1 -1
- package/dist/NodeArrayProperty.svelte +40 -37
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
- package/dist/{Document.svelte.d.ts → Session.svelte.d.ts} +76 -83
- package/dist/Session.svelte.d.ts.map +1 -0
- package/dist/Session.svelte.js +594 -0
- package/dist/Svedit.svelte +1352 -1512
- package/dist/Svedit.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.d.ts +114 -40
- package/dist/Transaction.svelte.d.ts.map +1 -1
- package/dist/Transaction.svelte.js +845 -661
- package/dist/UnknownNode.svelte +6 -6
- package/dist/UnknownNode.svelte.d.ts.map +1 -1
- package/dist/doc_utils.d.ts +127 -0
- package/dist/doc_utils.d.ts.map +1 -0
- package/dist/doc_utils.js +482 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +20 -4
- package/dist/{commands.svelte.d.ts → transforms.svelte.d.ts} +1 -2
- package/dist/transforms.svelte.d.ts.map +1 -0
- package/dist/transforms.svelte.js +155 -0
- package/dist/types.d.ts +256 -110
- package/dist/{util.d.ts → utils.d.ts} +22 -2
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +338 -0
- package/package.json +20 -18
- package/src/lib/AnnotatedTextProperty.svelte +122 -75
- package/src/lib/Command.svelte.js +314 -0
- package/src/lib/CustomProperty.svelte +31 -42
- package/src/lib/KeyMapper.svelte.js +141 -0
- package/src/lib/Node.svelte +33 -35
- package/src/lib/NodeArrayProperty.svelte +40 -37
- package/src/lib/Session.svelte.js +594 -0
- package/src/lib/Svedit.svelte +1352 -1512
- package/src/lib/Transaction.svelte.js +845 -661
- package/src/lib/UnknownNode.svelte +6 -6
- package/src/lib/doc_utils.js +482 -0
- package/src/lib/index.js +20 -4
- package/src/lib/transforms.svelte.js +155 -0
- package/src/lib/types.d.ts +256 -110
- package/src/lib/utils.js +338 -0
- package/dist/Document.svelte.d.ts.map +0 -1
- package/dist/Document.svelte.js +0 -833
- package/dist/commands.svelte.d.ts.map +0 -1
- package/dist/commands.svelte.js +0 -266
- package/dist/util.d.ts.map +0 -1
- package/dist/util.js +0 -269
- package/src/lib/Document.svelte.js +0 -833
- package/src/lib/commands.svelte.js +0 -266
- package/src/lib/util.js +0 -269
package/README.md
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
# Svedit
|
|
2
2
|
|
|
3
|
-
Svedit (think Svelte Edit) is a
|
|
3
|
+
Svedit (think Svelte Edit) is a tiny library for building editable websites in Svelte. You can model your content in JSON, render it with custom Svelte components, and (this is the kicker) site owners can **edit their site directly in the layout** — no CMS needed.
|
|
4
4
|
|
|
5
|
-
Try the [demo](https://svedit.
|
|
5
|
+
Try the [demo](https://svedit.dev).
|
|
6
6
|
|
|
7
7
|
## Why Svedit?
|
|
8
8
|
|
|
9
|
-
Because Svelte‘s reactivity system is **the perfect fit** for building super-lightweight content editing experiences.
|
|
10
|
-
|
|
11
|
-
In fact, they're so lightweight, you can use them to **make webpages in-place editable**, removing the need for an external Content Management System (CMS).
|
|
12
|
-
|
|
13
|
-
Svedit just gives you the gluing pieces around **defining a custom document model** and **mapping DOM selections** to the internal model and vice versa.
|
|
9
|
+
Because Svelte‘s reactivity system is **the perfect fit** for building super-lightweight content editing experiences. In fact, they're so lightweight, **your content is your editor** — no context switching between a backend and the live site. Svedit just gives you the gluing pieces around **defining a custom document model** and **mapping DOM selections** to the internal model and vice versa.
|
|
14
10
|
|
|
15
11
|
## Getting started
|
|
16
12
|
|
|
@@ -33,27 +29,45 @@ And run the development server:
|
|
|
33
29
|
npm run dev
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
Now make it your own. The next thing you probably want to do is define your own [node types](./src/routes/
|
|
32
|
+
Now make it your own. The next thing you probably want to do is define your own [node types](./src/routes/create_demo_session.js), add a [Toolbar](./src/routes/components/Toolbar.svelte), and render custom [Overlays](./src/routes/components/Overlays.svelte). For that just get inspired by the [Svedit demo code](./src/routes).
|
|
37
33
|
|
|
38
34
|
## Principles
|
|
39
35
|
|
|
40
|
-
**
|
|
41
|
-
|
|
42
|
-
**Convention over configuration:** We use conventions and assumptions to reduce configuration code and limit the number of ways something can go wrong. For instance, we assume that a node with a property named `content` of type `annotated_text` is considered kind `text`, while all other nodes are considered kind `node`. Text nodes have special behavior in the system for editing (e.g. they can be splitted and joined).
|
|
36
|
+
**Simplicity over completeness:** Svedit doesn't guess what your app needs or offer ready-made blocks. Instead, we keep the core lean and provide carefully crafted examples showing how to build anything on top — without compromising flexibility.
|
|
43
37
|
|
|
44
38
|
**White-box library:** We expose the internals of the library to allow you to customize and extend it to your needs. That means a little bit more work upfront, but in return lets you control "everything" — the toolbar, the overlays, or how fast the node cursor blinks.
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
**Chromeless canvas:** Svedit keeps the editing canvas chromeless, meaning there are no UI elements like toolbars or menus mingled with the content. You can interact with text directly, but everything else happens via tools shown in separate overlays or in the fixed toolbar.
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+
Svedit connects eight key pieces:
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
1. **Schema** - Define your content structure (node types, properties, annotations)
|
|
47
|
+
2. **Document** - An actual document, containing a `document_id` and a flat map of nodes that hold the content
|
|
48
|
+
3. **Session** - Manages the document, selection state, and history
|
|
49
|
+
4. **Transaction** - Groups multiple document operations (create, delete, set) into a single atomic unit with undo/redo support
|
|
50
|
+
5. **Transforms** - Higher-level composable functions that run inside a transaction (e.g., `break_text_node`, `join_text_node`) to modify the document.
|
|
51
|
+
6. **Config** - Maps node types to components, provides inserters and commands
|
|
52
|
+
7. **Components** - Render your content using Svelte (one component per node type)
|
|
53
|
+
8. **Commands** - User actions (bold text, insert node, undo/redo) that modify the session
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
**The flow:**
|
|
56
|
+
- Define a schema → create a session → provide config → render with `<Svedit>` component
|
|
57
|
+
- User interactions trigger commands → commands create transactions → which run transforms to modify the document → session applies the transaction → Svelte's reactivity updates the UI
|
|
58
|
+
- Selection state syncs bidirectionally with the DOM
|
|
59
|
+
|
|
60
|
+
## Schema
|
|
51
61
|
|
|
52
62
|
You can use a simple JSON-compatible schema definition language to enforce constraints on your documents. E.g. to make sure a page node always has a property body with references to nodes that are allowed within a page.
|
|
53
63
|
|
|
54
64
|
First off, everything is a node. The page is a node, and so is a paragraph, a list, a list item, a nav and a nav item.
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
Each node has a `kind` that determines its behavior:
|
|
67
|
+
- `document`: A top-level node accessible via a route (e.g. a page, event)
|
|
68
|
+
- `block`: A structured node that contains other nodes or properties
|
|
69
|
+
- `text`: A node with editable text content (can be split and joined)
|
|
70
|
+
- `annotation`: An inline annotation applied to text (bold, link, etc.)
|
|
57
71
|
|
|
58
72
|
Properties of nodes can hold values:
|
|
59
73
|
- `string`: A good old JavaScript string
|
|
@@ -124,138 +138,622 @@ const document_schema = {
|
|
|
124
138
|
};
|
|
125
139
|
```
|
|
126
140
|
|
|
141
|
+
## Document
|
|
127
142
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
A document is just a subsets of nodes, with a few rules:
|
|
143
|
+
A document is a plain JavaScript object (POJO) with a `document_id` (the entry point) and a `nodes` object containing all content nodes.
|
|
131
144
|
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
- Your document must not contain cyclic references for that reason
|
|
145
|
+
Rules:
|
|
146
|
+
- All nodes must be reachable from the document node (unreachable nodes are discarded)
|
|
147
|
+
- No cyclic references allowed
|
|
148
|
+
- Text content uses `{ text: '', annotations: [] }` format
|
|
137
149
|
|
|
138
150
|
Here's an example document:
|
|
151
|
+
|
|
139
152
|
```js
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
153
|
+
const doc = {
|
|
154
|
+
document_id: 'page_1',
|
|
155
|
+
nodes: {
|
|
156
|
+
nav_item_1: {
|
|
157
|
+
id: 'nav_item_1',
|
|
158
|
+
type: 'nav_item',
|
|
159
|
+
url: '/homepage',
|
|
160
|
+
label: 'Home'
|
|
161
|
+
},
|
|
162
|
+
nav_1: {
|
|
163
|
+
id: 'nav_1',
|
|
164
|
+
type: 'nav',
|
|
165
|
+
nav_items: ['nav_item_1']
|
|
166
|
+
},
|
|
167
|
+
paragraph_1: {
|
|
168
|
+
id: 'paragraph_1',
|
|
169
|
+
type: 'text',
|
|
170
|
+
layout: 1,
|
|
171
|
+
content: { text: 'Hello world.', annotations: [] }
|
|
172
|
+
},
|
|
173
|
+
list_item_1: {
|
|
174
|
+
id: 'list_item_1',
|
|
175
|
+
type: 'list_item',
|
|
176
|
+
content: { text: 'First list item', annotations: [] }
|
|
177
|
+
},
|
|
178
|
+
list_item_2: {
|
|
179
|
+
id: 'list_item_2',
|
|
180
|
+
type: 'list_item',
|
|
181
|
+
content: { text: 'Second list item', annotations: [] }
|
|
182
|
+
},
|
|
183
|
+
list_1: {
|
|
184
|
+
id: 'list_1',
|
|
185
|
+
type: 'list',
|
|
186
|
+
list_items: ['list_item_1', 'list_item_2']
|
|
187
|
+
},
|
|
188
|
+
page_1: {
|
|
189
|
+
id: 'page_1',
|
|
190
|
+
type: 'page',
|
|
191
|
+
body: ['nav_1', 'paragraph_1', 'list_1']
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
177
195
|
```
|
|
178
196
|
|
|
179
|
-
##
|
|
197
|
+
## Config
|
|
180
198
|
|
|
181
|
-
|
|
199
|
+
Documents need a config object that tells Svedit how to render and manipulate your content. See the full example in [`src/routes/create_demo_session.js`](src/routes/create_demo_session.js).
|
|
182
200
|
|
|
183
201
|
```js
|
|
184
|
-
const
|
|
185
|
-
//
|
|
186
|
-
generate_id:
|
|
187
|
-
|
|
202
|
+
const session_config = {
|
|
203
|
+
// ID generator for creating new nodes
|
|
204
|
+
generate_id: () => nanoid(),
|
|
205
|
+
|
|
206
|
+
// System components (NodeCursorTrap, Overlays)
|
|
207
|
+
system_components: { NodeCursorTrap, Overlays },
|
|
208
|
+
|
|
209
|
+
// Map node types to Svelte components
|
|
210
|
+
node_components: { Page, Text, Story, List, Button, ... },
|
|
211
|
+
|
|
212
|
+
// Functions that create and insert new nodes
|
|
213
|
+
inserters: {
|
|
214
|
+
text: (tr, content = {text: '', annotations: []}) => {
|
|
215
|
+
const text_id = nanoid();
|
|
216
|
+
tr.create({ id: text_id, type: 'text', content });
|
|
217
|
+
tr.insert_nodes([text_id]);
|
|
218
|
+
}
|
|
188
219
|
},
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
220
|
+
|
|
221
|
+
// Returns { commands, keymap } for the editor instance
|
|
222
|
+
create_commands_and_keymap: (context) => { ... },
|
|
223
|
+
|
|
224
|
+
// Optional: handle image paste events
|
|
225
|
+
handle_image_paste: (session, images) => { ... }
|
|
226
|
+
};
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Key config options:**
|
|
230
|
+
|
|
231
|
+
- **`generate_id`** - Function that generates unique IDs for new nodes
|
|
232
|
+
- **`node_components`** - Maps each node type from your schema to a Svelte component
|
|
233
|
+
- **`system_components`** - Provides custom NodeCursorTrap and Overlays components
|
|
234
|
+
- **`inserters`** - Functions that create blank nodes of each type and set up the selection
|
|
235
|
+
- **`create_commands_and_keymap`** - Factory function that creates commands and keybindings for an editor instance
|
|
236
|
+
- **`handle_image_paste`** - Optional handler for image paste events
|
|
237
|
+
|
|
238
|
+
The config is accessible throughout your app via `session.config`.
|
|
239
|
+
|
|
240
|
+
## Session
|
|
241
|
+
|
|
242
|
+
The `Session` class manages your content graph, selection state, and history. See [`src/lib/Session.svelte.js`](src/lib/Session.svelte.js) for the full API.
|
|
243
|
+
|
|
244
|
+
### Immutable state
|
|
245
|
+
|
|
246
|
+
Document content (`session.doc`) and selection (`session.selection`) are **immutable** with **copy-on-write** semantics. When a change is made, only the modified parts are copied — unchanged nodes keep their original references. This avoids the overhead of reactive proxies (using Svelte's `$state.raw`) since state is reassigned rather than mutated. Also, `console.log(session.get(some_node_id))` gives you a readable raw object, not a proxy.
|
|
247
|
+
|
|
248
|
+
### Creating a session
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
import { Session } from 'svedit';
|
|
252
|
+
|
|
253
|
+
const session = new Session(schema, doc, config);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Reading the graph
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
session.get(['page_1', 'body']) // => ['nav_1', 'paragraph_1', 'list_1']
|
|
260
|
+
session.get(['nav_1']) // => { id: 'nav_1', type: 'nav', ... }
|
|
261
|
+
session.get('nav_1') // => shorthand for above (single node ID)
|
|
262
|
+
session.inspect(['page_1', 'body']) // => { kind: 'property', type: 'node_array', node_types: [...] }
|
|
263
|
+
session.kind(node) // => 'text', 'block', or 'annotation'
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Selection and state
|
|
267
|
+
|
|
268
|
+
```js
|
|
269
|
+
session.selection // Current selection (text, node, or property)
|
|
270
|
+
session.selected_node // The currently selected node (derived)
|
|
271
|
+
session.active_annotation('strong') // Check if annotation is active at cursor
|
|
272
|
+
session.can_insert('paragraph') // Check if node type can be inserted
|
|
273
|
+
session.available_annotation_types // Annotation types allowed at current selection (derived)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Making changes
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
const tr = session.tr; // Create a transaction
|
|
280
|
+
tr.set(['nav_1', 'label'], 'Home');
|
|
281
|
+
tr.insert_nodes(['new_node_id']);
|
|
282
|
+
session.apply(tr); // Apply the transaction
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### History
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
session.can_undo // Boolean (derived)
|
|
289
|
+
session.can_redo // Boolean (derived)
|
|
290
|
+
session.undo()
|
|
291
|
+
session.redo()
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Detecting unsaved changes
|
|
295
|
+
|
|
296
|
+
Because document state is immutable, you can detect unsaved changes by comparing references. When a change is made, `session.doc` gets a new reference — unchanged documents keep the same reference.
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
let last_saved_doc = $state(null);
|
|
300
|
+
let has_unsaved_changes = $derived.by(() => {
|
|
301
|
+
if (!last_saved_doc) {
|
|
302
|
+
// No save yet — use undo history as indicator
|
|
303
|
+
return session.can_undo;
|
|
304
|
+
} else {
|
|
305
|
+
// Compare current doc reference against last saved
|
|
306
|
+
return last_saved_doc !== session.doc;
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
function save() {
|
|
311
|
+
// ... save to server ...
|
|
312
|
+
last_saved_doc = session.doc;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
This works because of Svedit's copy-on-write strategy: only modified parts of the document are copied, so reference equality is a reliable and efficient way to detect changes. You can use `has_unsaved_changes` to show/hide a save button, display a dirty indicator, or warn before navigating away.
|
|
317
|
+
|
|
318
|
+
### Utilities
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
session.doc.document_id // The document's root ID
|
|
322
|
+
session.generate_id() // Generate a new unique ID
|
|
323
|
+
session.config // Access the config object
|
|
324
|
+
session.validate_doc() // Validate all nodes against schema
|
|
325
|
+
session.traverse(node_id) // Get all nodes reachable from a node
|
|
326
|
+
session.select_parent() // Select parent of current selection
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Transforms
|
|
330
|
+
|
|
331
|
+
Transforms are pure functions that modify a transaction. They encapsulate common editing operations like breaking text nodes, joining nodes, or inserting new content.
|
|
332
|
+
|
|
333
|
+
Transforms take a transaction (`tr`) as their parameter and return `true` if successful or `false` if the transform cannot be applied (e.g., wrong selection type or invalid state).
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
// Example: break a text node at the cursor
|
|
337
|
+
import { break_text_node } from 'svedit';
|
|
338
|
+
|
|
339
|
+
const tr = session.tr;
|
|
340
|
+
const success = break_text_node(tr);
|
|
341
|
+
if (success) {
|
|
342
|
+
session.apply(tr);
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Built-in transforms
|
|
347
|
+
|
|
348
|
+
Svedit provides several core transforms in [`src/lib/transforms.svelte.js`](src/lib/transforms.svelte.js):
|
|
349
|
+
|
|
350
|
+
- `break_text_node(tr)` - Split a text node at the cursor position
|
|
351
|
+
- `join_text_node(tr)` - Join current text node with the previous one
|
|
352
|
+
- `insert_default_node(tr)` - Insert a new node at the current selection
|
|
353
|
+
|
|
354
|
+
### Composability
|
|
355
|
+
|
|
356
|
+
Transforms are composable. You can build higher-level transforms from lower-level ones:
|
|
357
|
+
|
|
358
|
+
```js
|
|
359
|
+
function custom_transform(tr) {
|
|
360
|
+
// Compose multiple transforms
|
|
361
|
+
if (!break_text_node(tr)) return false;
|
|
362
|
+
if (!insert_default_node(tr)) return false;
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Writing your own transforms
|
|
368
|
+
|
|
369
|
+
You're encouraged to write custom transforms for your application's specific needs. Keep them pure functions that operate on the transaction object:
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
function insert_heading(tr) {
|
|
373
|
+
const selection = tr.selection;
|
|
374
|
+
|
|
375
|
+
if (selection?.type !== 'node') return false;
|
|
376
|
+
|
|
377
|
+
// Create and insert a heading node
|
|
378
|
+
const heading_id = tr.generate_id();
|
|
379
|
+
tr.create({ id: heading_id, type: 'heading', content: { text: '', annotations: [] } });
|
|
380
|
+
tr.insert_nodes(selection.path, selection.anchor_offset, [heading_id]);
|
|
381
|
+
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Transaction
|
|
387
|
+
|
|
388
|
+
Transactions group multiple operations into atomic units that can be applied and undone as one. They provide the same read API as sessions (`tr.get()`, `tr.inspect()`, `tr.kind()`, `tr.generate_id()`), so transforms can query document state directly. See [`src/lib/Transaction.svelte.js`](src/lib/Transaction.svelte.js) for the full API.
|
|
389
|
+
|
|
390
|
+
### Basic usage
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
const tr = session.tr; // Create a new transaction
|
|
394
|
+
tr.set(['node_1', 'title'], 'New Title'); // Modify properties
|
|
395
|
+
session.apply(tr); // Apply atomically
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Node operations
|
|
399
|
+
|
|
400
|
+
```js
|
|
401
|
+
// Create a new node (must include all required properties from schema)
|
|
402
|
+
tr.create({ id: 'paragraph_1', type: 'paragraph', content: { text: '', annotations: [] } });
|
|
403
|
+
|
|
404
|
+
// Delete a node (cascades to unreferenced child nodes)
|
|
405
|
+
tr.delete('paragraph_26');
|
|
406
|
+
|
|
407
|
+
// Insert nodes at current node selection
|
|
408
|
+
tr.insert_nodes(['paragraph_1', 'list_1']);
|
|
409
|
+
|
|
410
|
+
// Build a subgraph from existing nodes (generates new IDs)
|
|
411
|
+
const new_node_id = tr.build('the_list', {
|
|
412
|
+
first_item: {
|
|
413
|
+
id: 'first_item',
|
|
414
|
+
type: 'list_item',
|
|
415
|
+
content: node.content
|
|
416
|
+
}
|
|
417
|
+
the_list: {
|
|
418
|
+
id: 'the_list',
|
|
419
|
+
type: 'list',
|
|
420
|
+
list_items: ['first_item']
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Text operations
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
// Insert text at cursor (replaces selection if expanded)
|
|
429
|
+
tr.insert_text('Hello');
|
|
430
|
+
|
|
431
|
+
// Toggle annotation on selected text
|
|
432
|
+
tr.annotate_text('strong');
|
|
433
|
+
tr.annotate_text('link', { href: 'https://example.com' });
|
|
434
|
+
|
|
435
|
+
// Delete selected text or nodes
|
|
436
|
+
tr.delete_selection();
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Selection
|
|
440
|
+
|
|
441
|
+
```js
|
|
442
|
+
// Set the selection after operations
|
|
443
|
+
tr.set_selection({
|
|
444
|
+
type: 'text',
|
|
445
|
+
path: ['node_1', 'content'],
|
|
446
|
+
anchor_offset: 0,
|
|
447
|
+
focus_offset: 5
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
All transaction methods return `this` for chaining:
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
tr.create(node)
|
|
455
|
+
.insert_nodes([node.id])
|
|
456
|
+
.set_selection(new_selection);
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## Commands
|
|
460
|
+
|
|
461
|
+
Commands provide a structured way to implement user actions. Commands are stateful and UI-aware, unlike transforms which are pure functions.
|
|
462
|
+
|
|
463
|
+
There are two types of commands in Svedit:
|
|
464
|
+
- **Document-scoped commands** - Bound to a specific Svedit instance/document and only active when that editor has focus
|
|
465
|
+
- **App-level commands** - Operate at the application level, independent of any specific document
|
|
466
|
+
|
|
467
|
+
Let's start with document-scoped commands, which are the foundation of the editing experience.
|
|
468
|
+
|
|
469
|
+
### Document-scoped commands
|
|
470
|
+
|
|
471
|
+
Document-scoped commands operate on a specific document and have access to its selection, content, and editing state through a context object.
|
|
472
|
+
|
|
473
|
+
#### Creating a document-scoped command
|
|
474
|
+
|
|
475
|
+
Extend the `Command` base class and implement the `is_enabled()` and `execute()` methods:
|
|
476
|
+
|
|
477
|
+
```js
|
|
478
|
+
import { Command } from 'svedit';
|
|
479
|
+
|
|
480
|
+
class ToggleStrongCommand extends Command {
|
|
481
|
+
is_enabled() {
|
|
482
|
+
return this.context.editable && this.context.session.selection?.type === 'text';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
execute() {
|
|
486
|
+
this.context.session.apply(this.context.session.tr.annotate_text('strong'));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
#### Document command context
|
|
492
|
+
|
|
493
|
+
Document-scoped commands receive a `context` object with access to the Svedit instance's state:
|
|
494
|
+
|
|
495
|
+
- `context.session` - The current session instance
|
|
496
|
+
- `context.editable` - Whether the editor is in edit mode
|
|
497
|
+
- `context.canvas_el` - The DOM element of the Svedit editor canvas
|
|
498
|
+
- `context.is_composing` - Whether IME composition is currently taking place
|
|
499
|
+
|
|
500
|
+
### Command lifecycle methods
|
|
501
|
+
|
|
502
|
+
**`is_enabled(): boolean`**
|
|
503
|
+
|
|
504
|
+
Determines if the command can currently be executed. This is automatically evaluated and exposed as the `disabled` derived property, which can be used to disable UI elements.
|
|
505
|
+
|
|
506
|
+
```js
|
|
507
|
+
is_enabled() {
|
|
508
|
+
return this.context.editable && this.context.session.selection?.type === 'text';
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**`execute(): void | Promise<void>`**
|
|
513
|
+
|
|
514
|
+
Executes the command's action. Can be synchronous or asynchronous.
|
|
515
|
+
|
|
516
|
+
```js
|
|
517
|
+
execute() {
|
|
518
|
+
const tr = this.context.session.tr;
|
|
519
|
+
tr.insert_text('Hello');
|
|
520
|
+
this.context.session.apply(tr);
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
#### Built-in document commands
|
|
525
|
+
|
|
526
|
+
Svedit provides several [core commands](src/lib/Command.svelte.js) out of the box:
|
|
527
|
+
|
|
528
|
+
- `UndoCommand` - Undo the last change
|
|
529
|
+
- `RedoCommand` - Redo the last undone change
|
|
530
|
+
- `SelectParentCommand` - Select the parent of the current selection
|
|
531
|
+
- `ToggleAnnotationCommand` - Toggle text annotations (bold, italic, etc.)
|
|
532
|
+
- `AddNewLineCommand` - Insert newline character in text
|
|
533
|
+
- `BreakTextNodeCommand` - Split text node at cursor
|
|
534
|
+
- `SelectAllCommand` - Progressively expand selection
|
|
535
|
+
- `InsertDefaultNodeCommand` - Insert a new node at cursor
|
|
536
|
+
|
|
537
|
+
#### Using document commands
|
|
538
|
+
|
|
539
|
+
Commands are created by passing them a context object from the Svedit component. See a complete example in [`src/routes/create_demo_session.js`](src/routes/create_demo_session.js) in the `create_commands_and_keymap` configuration function:
|
|
540
|
+
|
|
541
|
+
```js
|
|
542
|
+
create_commands_and_keymap: (context) => {
|
|
543
|
+
const commands = {
|
|
544
|
+
undo: new UndoCommand(context),
|
|
545
|
+
redo: new RedoCommand(context),
|
|
546
|
+
toggle_strong: new ToggleAnnotationCommand('strong', context),
|
|
547
|
+
toggle_emphasis: new ToggleAnnotationCommand('emphasis', context),
|
|
548
|
+
// ... more commands
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const keymap = define_keymap({
|
|
552
|
+
'meta+z,ctrl+z': [commands.undo],
|
|
553
|
+
'meta+b,ctrl+b': [commands.toggle_strong],
|
|
554
|
+
// ... more keybindings
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
return { commands, keymap };
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Bind commands to UI elements in your components:
|
|
562
|
+
|
|
563
|
+
```svelte
|
|
564
|
+
<button
|
|
565
|
+
disabled={document_commands.toggle_strong.disabled}
|
|
566
|
+
class:active={document_commands.toggle_strong.active}
|
|
567
|
+
onclick={() => document_commands.toggle_strong.execute()}>
|
|
568
|
+
Bold
|
|
569
|
+
</button>
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
#### Derived state in commands
|
|
573
|
+
|
|
574
|
+
Commands can have derived state for reactive UI binding. The `active` property in toggle commands is a common pattern:
|
|
575
|
+
|
|
576
|
+
```js
|
|
577
|
+
class ToggleEmphasisCommand extends Command {
|
|
578
|
+
// Automatically recomputes when annotation state changes
|
|
579
|
+
active = $derived(this.context.session.active_annotation('emphasis'));
|
|
580
|
+
|
|
581
|
+
is_enabled() {
|
|
582
|
+
return this.context.editable && this.context.session.selection?.type === 'text';
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
execute() {
|
|
586
|
+
this.context.session.apply(this.context.session.tr.annotate_text('emphasis'));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
The `disabled` property is automatically derived from `is_enabled()` on all commands.
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
#### DOM access in commands
|
|
595
|
+
|
|
596
|
+
Commands can access the DOM through the context or global APIs:
|
|
597
|
+
|
|
598
|
+
```js
|
|
599
|
+
class FocusNextSelectableCommand extends Command {
|
|
600
|
+
execute() {
|
|
601
|
+
const selectables = this.context.canvas_el.querySelectorAll('.svedit-selectable');
|
|
602
|
+
const next = selectables[0]; // Find next based on current selection
|
|
603
|
+
const path = next.closest('[data-path]').dataset.path.split('.');
|
|
604
|
+
this.context.session.selection = { type: 'text', path, anchor_offset: 0, focus_offset: 0 };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### App-level commands and scope hierarchy
|
|
610
|
+
|
|
611
|
+
While document-scoped commands operate on a specific Svedit instance, app-level commands operate at the application level and handle concerns like saving, loading, switching between edit/view modes, or managing multiple documents.
|
|
612
|
+
|
|
613
|
+
#### Understanding the scope stack
|
|
614
|
+
|
|
615
|
+
Svedit uses a scope hierarchy (scope stack) to manage which commands are active at any given time:
|
|
616
|
+
|
|
617
|
+
1. **App-level scope** (top level) - Commands that are always available, independent of document focus
|
|
618
|
+
2. **Document-level scope** (per Svedit instance) - Commands bound to a specific document/editor
|
|
619
|
+
|
|
620
|
+
When a Svedit instance gains focus:
|
|
621
|
+
- The previous document's scope is **popped** from the stack (its commands become inactive)
|
|
622
|
+
- The newly focused document's scope is **pushed** onto the stack (its commands become active)
|
|
623
|
+
|
|
624
|
+
This means commands automatically work with the correct document based on focus.
|
|
625
|
+
|
|
626
|
+
#### Creating app-level commands
|
|
627
|
+
|
|
628
|
+
App-level commands have their own context, separate from any specific document:
|
|
629
|
+
|
|
630
|
+
```js
|
|
631
|
+
import { Command } from 'svedit';
|
|
632
|
+
|
|
633
|
+
class SaveCommand extends Command {
|
|
634
|
+
is_enabled() {
|
|
635
|
+
return this.context.editable;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async execute() {
|
|
639
|
+
await this.context.save_all_documents();
|
|
640
|
+
this.context.show_notification('All changes saved');
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
class ToggleEditModeCommand extends Command {
|
|
645
|
+
is_enabled() {
|
|
646
|
+
return !this.context.editable;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
execute() {
|
|
650
|
+
this.context.editable = true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
#### App-level context
|
|
656
|
+
|
|
657
|
+
The app-level context contains application-wide state and methods:
|
|
658
|
+
|
|
659
|
+
```js
|
|
660
|
+
const app_context = {
|
|
661
|
+
get editable() {
|
|
662
|
+
return editable; // App-level editable state
|
|
192
663
|
},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
Button,
|
|
196
|
-
Text,
|
|
197
|
-
Story,
|
|
198
|
-
List,
|
|
199
|
-
ListItem,
|
|
200
|
-
ImageGrid,
|
|
201
|
-
ImageGridItem,
|
|
202
|
-
Hero
|
|
664
|
+
set editable(value) {
|
|
665
|
+
editable = value;
|
|
203
666
|
},
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
text: 4,
|
|
207
|
-
list: 5,
|
|
208
|
-
list_item: 1,
|
|
667
|
+
get session() {
|
|
668
|
+
return session;
|
|
209
669
|
},
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
inserters: {
|
|
213
|
-
text: function(tr, content = {text: '', annotations: []}, layout = 1) {
|
|
214
|
-
const new_text = {
|
|
215
|
-
id: nanoid(),
|
|
216
|
-
type: 'text',
|
|
217
|
-
layout,
|
|
218
|
-
content
|
|
219
|
-
};
|
|
220
|
-
tr.create(new_text);
|
|
221
|
-
tr.insert_nodes([new_text.id]);
|
|
222
|
-
},
|
|
670
|
+
get app_el() {
|
|
671
|
+
return app_el;
|
|
223
672
|
}
|
|
224
673
|
};
|
|
674
|
+
|
|
675
|
+
const app_commands = {
|
|
676
|
+
save: new SaveCommand(app_context),
|
|
677
|
+
toggle_edit: new ToggleEditCommand(app_context)
|
|
678
|
+
};
|
|
225
679
|
```
|
|
226
680
|
|
|
227
|
-
##
|
|
681
|
+
## Scope-aware keyboard shortcuts
|
|
228
682
|
|
|
229
|
-
The
|
|
683
|
+
The KeyMapper manages keyboard shortcuts using a scope-based stack system. Scopes are tried from top to bottom (most recent to least recent), allowing more specific keymaps to override general ones.
|
|
684
|
+
|
|
685
|
+
### Basic usage
|
|
230
686
|
|
|
231
687
|
```js
|
|
232
|
-
|
|
688
|
+
import { KeyMapper, define_keymap } from 'svedit';
|
|
689
|
+
|
|
690
|
+
const key_mapper = new KeyMapper();
|
|
691
|
+
|
|
692
|
+
// Define a keymap
|
|
693
|
+
const keymap = define_keymap({
|
|
694
|
+
'meta+z,ctrl+z': [document_commands.undo],
|
|
695
|
+
'meta+b,ctrl+b': [document_commands.bold],
|
|
696
|
+
'enter': [document_commands.break_text_node]
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// Push the keymap onto the scope stack
|
|
700
|
+
key_mapper.push_scope(keymap);
|
|
701
|
+
|
|
702
|
+
// Handle keydown events
|
|
703
|
+
window.addEventListener('keydown', (event) => {
|
|
704
|
+
key_mapper.handle_keydown(event);
|
|
705
|
+
});
|
|
233
706
|
```
|
|
234
707
|
|
|
235
|
-
|
|
708
|
+
### Key syntax
|
|
709
|
+
|
|
710
|
+
- **Multiple modifiers**: `meta+shift+z`, `ctrl+alt+k`
|
|
711
|
+
- **Cross-platform**: `meta+z,ctrl+z` (tries Meta+Z first, then Ctrl+Z)
|
|
712
|
+
- **Modifiers**: `meta`, `ctrl`, `alt`, `shift`
|
|
713
|
+
- **Keys**: Any key name (e.g., `a`, `enter`, `escape`, `arrowup`)
|
|
714
|
+
|
|
715
|
+
### Command arrays
|
|
716
|
+
|
|
717
|
+
Commands are wrapped in arrays to support fallback behavior:
|
|
236
718
|
|
|
237
719
|
```js
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
720
|
+
define_keymap({
|
|
721
|
+
'meta+b,ctrl+b': [
|
|
722
|
+
document_commands.bold, // Try this first
|
|
723
|
+
document_commands.fallback // Use this if first is disabled
|
|
724
|
+
]
|
|
725
|
+
});
|
|
242
726
|
```
|
|
243
727
|
|
|
244
|
-
|
|
728
|
+
### Scope stack
|
|
245
729
|
|
|
246
|
-
|
|
247
|
-
multiple ops that are applied in a single step and are undo/redo-able.
|
|
730
|
+
Use `push_scope()` and `pop_scope()` to manage different keyboard contexts:
|
|
248
731
|
|
|
249
732
|
```js
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
733
|
+
// App-level keymap (always active)
|
|
734
|
+
const app_keymap = define_keymap({
|
|
735
|
+
'meta+s,ctrl+s': [app_commands.save],
|
|
736
|
+
'meta+n,ctrl+n': [app_commands.new_document]
|
|
737
|
+
});
|
|
738
|
+
key_mapper.push_scope(app_keymap);
|
|
739
|
+
|
|
740
|
+
// Document-level keymap (active when editor has focus)
|
|
741
|
+
const doc_keymap = define_keymap({
|
|
742
|
+
'meta+z,ctrl+z': [document_commands.undo],
|
|
743
|
+
'meta+b,ctrl+b': [document_commands.bold]
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// When editor gains focus:
|
|
747
|
+
key_mapper.push_scope(doc_keymap);
|
|
748
|
+
|
|
749
|
+
// When editor loses focus:
|
|
750
|
+
key_mapper.pop_scope();
|
|
256
751
|
```
|
|
257
752
|
|
|
258
|
-
|
|
753
|
+
The KeyMapper tries scopes from top to bottom, so push more specific keymaps last.
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
## Selection
|
|
259
757
|
|
|
260
758
|
Selections are at the heart of Svedit. There are just three types of selections:
|
|
261
759
|
|
|
@@ -295,16 +793,156 @@ Selections are at the heart of Svedit. There are just three types of selections:
|
|
|
295
793
|
}
|
|
296
794
|
```
|
|
297
795
|
|
|
298
|
-
You can access the current selection through `
|
|
796
|
+
You can access the current selection through `session.selection` anytime. And you can programmatically set the selection using `session.selection = new_selection`.
|
|
299
797
|
|
|
300
798
|
## Rendering
|
|
301
799
|
|
|
302
800
|
Now you can start making your Svelte pages in-place editable by wrapping your design inside the `<Svedit>` component.
|
|
303
801
|
|
|
802
|
+
```svelte
|
|
803
|
+
<Svedit {session} path={[session.doc.document_id]} editable={true} />
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
## Node components
|
|
807
|
+
|
|
808
|
+
Node components are Svelte components that render specific node types in your document. Each node component receives a `path` prop and uses the `<Node>` wrapper component along with property components to render the node's content.
|
|
809
|
+
|
|
810
|
+
### Basic structure
|
|
811
|
+
|
|
812
|
+
A typical node component follows this pattern:
|
|
813
|
+
|
|
814
|
+
```svelte
|
|
815
|
+
<script>
|
|
816
|
+
import { Node, AnnotatedTextProperty } from 'svedit';
|
|
817
|
+
let { path } = $props();
|
|
818
|
+
</script>
|
|
819
|
+
|
|
820
|
+
<Node {path}>
|
|
821
|
+
<div class="my-node">
|
|
822
|
+
<AnnotatedTextProperty path={[...path, 'content']} />
|
|
823
|
+
</div>
|
|
824
|
+
</Node>
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### The `<Node>` wrapper
|
|
828
|
+
|
|
829
|
+
Every node component must wrap its content in the `<Node>` component. This wrapper:
|
|
830
|
+
- Registers the node with the editor
|
|
831
|
+
- Handles selection and cursor behavior
|
|
832
|
+
- Provides the foundation for editing interactions
|
|
833
|
+
|
|
834
|
+
### Property components
|
|
835
|
+
|
|
836
|
+
Svedit provides specialized components for rendering different property types:
|
|
837
|
+
|
|
838
|
+
**`<AnnotatedTextProperty>`** - For editable text content with inline formatting:
|
|
839
|
+
|
|
840
|
+
```svelte
|
|
841
|
+
<AnnotatedTextProperty
|
|
842
|
+
tag="p"
|
|
843
|
+
class="body"
|
|
844
|
+
path={[...path, 'content']}
|
|
845
|
+
placeholder="Enter text here"
|
|
846
|
+
/>
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
**`<NodeArrayProperty>`** - For container properties that hold multiple nodes:
|
|
850
|
+
|
|
851
|
+
```svelte
|
|
852
|
+
<NodeArrayProperty
|
|
853
|
+
class="list-items"
|
|
854
|
+
path={[...path, 'list_items']}
|
|
855
|
+
/>
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
**`<CustomProperty>`** - For custom properties like images or other non-text content:
|
|
859
|
+
|
|
860
|
+
```svelte
|
|
861
|
+
<CustomProperty class="image-wrapper" path={[...path, 'image']}>
|
|
862
|
+
<div contenteditable="false">
|
|
863
|
+
<img src={node.image} alt={node.title.text} />
|
|
864
|
+
</div>
|
|
865
|
+
</CustomProperty>
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### Accessing node data
|
|
869
|
+
|
|
870
|
+
Use the Svedit context to access node data:
|
|
871
|
+
|
|
872
|
+
```svelte
|
|
873
|
+
<script>
|
|
874
|
+
import { getContext } from 'svelte';
|
|
875
|
+
const svedit = getContext('svedit');
|
|
876
|
+
|
|
877
|
+
let { path } = $props();
|
|
878
|
+
let node = $derived(svedit.session.get(path));
|
|
879
|
+
let layout = $derived(node.layout || 1);
|
|
880
|
+
</script>
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
### Example: Text component
|
|
884
|
+
|
|
885
|
+
Here's a complete example of a text node component that supports multiple layouts:
|
|
886
|
+
|
|
887
|
+
```svelte
|
|
888
|
+
<script>
|
|
889
|
+
import { getContext } from 'svelte';
|
|
890
|
+
import { Node, AnnotatedTextProperty } from 'svedit';
|
|
891
|
+
|
|
892
|
+
const svedit = getContext('svedit');
|
|
893
|
+
let { path } = $props();
|
|
894
|
+
let node = $derived(svedit.session.get(path));
|
|
895
|
+
let layout = $derived(node.layout || 1);
|
|
896
|
+
let tag = $derived(layout === 1 ? 'p' : `h${layout - 1}`);
|
|
897
|
+
</script>
|
|
898
|
+
|
|
899
|
+
<Node {path}>
|
|
900
|
+
<div class="text layout-{layout}">
|
|
901
|
+
<AnnotatedTextProperty
|
|
902
|
+
{tag}
|
|
903
|
+
class="body"
|
|
904
|
+
path={[...path, 'content']}
|
|
905
|
+
placeholder="Enter text"
|
|
906
|
+
/>
|
|
907
|
+
</div>
|
|
908
|
+
</Node>
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
### Example: List component
|
|
912
|
+
|
|
913
|
+
A simple list component that renders child items:
|
|
914
|
+
|
|
915
|
+
```svelte
|
|
916
|
+
<script>
|
|
917
|
+
import { Node, NodeArrayProperty } from 'svedit';
|
|
918
|
+
let { path } = $props();
|
|
919
|
+
</script>
|
|
920
|
+
|
|
921
|
+
<Node {path}>
|
|
922
|
+
<div class="list">
|
|
923
|
+
<NodeArrayProperty path={[...path, 'list_items']} />
|
|
924
|
+
</div>
|
|
925
|
+
</Node>
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
### Registering node components
|
|
929
|
+
|
|
930
|
+
Node components are registered in the document config's `node_components` map:
|
|
931
|
+
|
|
304
932
|
```js
|
|
305
|
-
|
|
933
|
+
const session_config = {
|
|
934
|
+
node_components: {
|
|
935
|
+
Text,
|
|
936
|
+
Story,
|
|
937
|
+
List,
|
|
938
|
+
ListItem,
|
|
939
|
+
// ... other components
|
|
940
|
+
}
|
|
941
|
+
}
|
|
306
942
|
```
|
|
307
943
|
|
|
944
|
+
The key in this map corresponds to the node's `type` property in the schema. Note that the component name should match the node type name. For example, a node with `type: "list_item"` will look for a component registered as `ListItem` in the `node_components` map.
|
|
945
|
+
|
|
308
946
|
## Mastering contenteditable
|
|
309
947
|
|
|
310
948
|
Svedit relies on the contenteditable attribute to make elements editable. The below example shows you
|
|
@@ -319,6 +957,9 @@ a simplified version of the markup of `<NodeCursorTrap>` and why it is implement
|
|
|
319
957
|
Adding a ​ (or any character) here will lead to 2 cursor
|
|
320
958
|
positions (one before, and one after the character)
|
|
321
959
|
|
|
960
|
+
Using <wbr> will make it only addressable for ArrowLeft and ArrowRight, but not ArrowUp and ArrowDown.
|
|
961
|
+
And using <span></span> will not make it addressable at all.
|
|
962
|
+
|
|
322
963
|
Svedit uses this behavior for node-cursor-traps, and when an
|
|
323
964
|
<AnnotatedTextProperty> is empty.
|
|
324
965
|
-->
|
|
@@ -341,11 +982,11 @@ Further things to consider:
|
|
|
341
982
|
- 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`>)
|
|
342
983
|
- `<AnnotatedTextProperty>` and `<CustomProperty>` must not be wrapped in `contenteditable="false"` to work properly.
|
|
343
984
|
- 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.
|
|
344
|
-
- 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.
|
|
985
|
+
- 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 (when `svedit.editable` is `false` ).
|
|
345
986
|
|
|
346
987
|
## Full API docs?
|
|
347
988
|
|
|
348
|
-
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
|
|
989
|
+
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 scenes.
|
|
349
990
|
|
|
350
991
|
## Developing Svedit
|
|
351
992
|
|
|
@@ -373,7 +1014,7 @@ Find my contact details [here](https://editable.website).
|
|
|
373
1014
|
|
|
374
1015
|
## Beta version
|
|
375
1016
|
|
|
376
|
-
It's early. Expect bugs. Expect missing features. Expect the need for more work on your part to make this
|
|
1017
|
+
It's still early. Expect bugs. Expect missing features. Expect the need for more work on your part to make this fit for your use case.
|
|
377
1018
|
|
|
378
1019
|
## Credits
|
|
379
1020
|
|