svedit 0.1.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/LICENSE +21 -0
- package/README.md +323 -0
- package/dist/AnnotatedStringProperty.svelte +126 -0
- package/dist/AnnotatedStringProperty.svelte.d.ts +7 -0
- package/dist/AnnotatedStringProperty.svelte.d.ts.map +1 -0
- package/dist/CustomProperty.svelte +40 -0
- package/dist/CustomProperty.svelte.d.ts +8 -0
- package/dist/CustomProperty.svelte.d.ts.map +1 -0
- package/dist/Document.svelte.d.ts +191 -0
- package/dist/Document.svelte.d.ts.map +1 -0
- package/dist/Document.svelte.js +681 -0
- package/dist/Node.svelte +41 -0
- package/dist/Node.svelte.d.ts +7 -0
- package/dist/Node.svelte.d.ts.map +1 -0
- package/dist/NodeArrayProperty.svelte +45 -0
- package/dist/NodeArrayProperty.svelte.d.ts +7 -0
- package/dist/NodeArrayProperty.svelte.d.ts.map +1 -0
- package/dist/Svedit.svelte +936 -0
- package/dist/Svedit.svelte.d.ts +12 -0
- package/dist/Svedit.svelte.d.ts.map +1 -0
- package/dist/Transaction.svelte.d.ts +198 -0
- package/dist/Transaction.svelte.d.ts.map +1 -0
- package/dist/Transaction.svelte.js +577 -0
- package/dist/UnknownNode.svelte +12 -0
- package/dist/UnknownNode.svelte.d.ts +12 -0
- package/dist/UnknownNode.svelte.d.ts.map +1 -0
- package/dist/commands.svelte.d.ts +5 -0
- package/dist/commands.svelte.d.ts.map +1 -0
- package/dist/commands.svelte.js +261 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/types.d.ts +264 -0
- package/dist/util.d.ts +4 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +78 -0
- package/package.json +65 -0
- package/src/lib/AnnotatedStringProperty.svelte +126 -0
- package/src/lib/CustomProperty.svelte +40 -0
- package/src/lib/Document.svelte.js +681 -0
- package/src/lib/Node.svelte +41 -0
- package/src/lib/NodeArrayProperty.svelte +45 -0
- package/src/lib/Svedit.svelte +936 -0
- package/src/lib/Transaction.svelte.js +577 -0
- package/src/lib/UnknownNode.svelte +12 -0
- package/src/lib/commands.svelte.js +261 -0
- package/src/lib/index.js +14 -0
- package/src/lib/types.d.ts +264 -0
- package/src/lib/util.js +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Michael Aufreiter, Johannes Mutter.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# Svedit
|
|
2
|
+
|
|
3
|
+
Svedit (think Svelte Edit) is a tiny library for building **rich content editors** with Svelte 5. You can model your content in JSON, render it with custom Svelte components, and edit it directly in the layout.
|
|
4
|
+
|
|
5
|
+
Try the [demo](https://svedit.vercel.app).
|
|
6
|
+
|
|
7
|
+
## Why Svedit?
|
|
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.
|
|
14
|
+
|
|
15
|
+
## Getting started
|
|
16
|
+
|
|
17
|
+
Clone the bare-bones `hello-svedit` repository:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/michael/hello-svedit
|
|
21
|
+
cd hello-svedit
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Install dependencies:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
And run the development server:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Now make it your own. The next thing you probably want to do is define your own [node types](./src/routes/create_demo_doc.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
|
+
|
|
38
|
+
## Principles
|
|
39
|
+
|
|
40
|
+
**Chromeless canvas:** We keep the canvas chromeless, meaning there's no UI elements like toolbars or menus mingled with the content. You can interact with text directly, but everything else happens via tools are shown in separate overlays or in the fixed toolbar.
|
|
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 `string` or `annotated_string` 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).
|
|
43
|
+
|
|
44
|
+
**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
|
+
|
|
46
|
+
## Graph data model
|
|
47
|
+
|
|
48
|
+
Svedit documents are represented in a simple JSON-based graph data model. There's a globally addressable space, a graph of content nodes if you want. This allows you to share pieces of content not only in the same document, but across multiple documents. E.g. you could share a navigation bar, while still being able to edit it in place (while changes will affect all places they are used).
|
|
49
|
+
|
|
50
|
+
## Schema definitions
|
|
51
|
+
|
|
52
|
+
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
|
+
|
|
54
|
+
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
|
+
|
|
56
|
+
A top-level node that is accessible via a route we internally call a `document` (e.g. a page, event, etc.)
|
|
57
|
+
|
|
58
|
+
Properties of nodes can hold values:
|
|
59
|
+
- `string`: A good old JavaScript string
|
|
60
|
+
- `number`: Just like a number in JavaScript
|
|
61
|
+
- `integer`: A number for which Number.isInteger(number) returns true
|
|
62
|
+
- `boolean`: true or false
|
|
63
|
+
- `string_array`: An array of good old JavaScript strings
|
|
64
|
+
- `integer_array`: An array of integers
|
|
65
|
+
- `number_array`: An array of numbers
|
|
66
|
+
- `annotated_string`: a plain text string, but with annotations (bold, italic, link etc.)
|
|
67
|
+
|
|
68
|
+
Or references:
|
|
69
|
+
- `node`: References a single node (e.g. an image node can reference a global asset node)
|
|
70
|
+
- `node_array`: References a sequence of nodes (e.g. page.body references paragraph and list nodes)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const document_schema = {
|
|
75
|
+
page: {
|
|
76
|
+
body: {
|
|
77
|
+
type: 'node_array',
|
|
78
|
+
node_types: ['nav', 'paragraph', 'list'],
|
|
79
|
+
default_node_type: 'paragraph',
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
paragraph: {
|
|
83
|
+
content: { type: 'annotated_string' }
|
|
84
|
+
},
|
|
85
|
+
list_item: {
|
|
86
|
+
content: { type: 'annotated_string' },
|
|
87
|
+
},
|
|
88
|
+
list: {
|
|
89
|
+
list_items: {
|
|
90
|
+
type: 'node_array',
|
|
91
|
+
node_types: ['list_item'],
|
|
92
|
+
default_node_type: 'list_item',
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
nav: {
|
|
96
|
+
nav_items: {
|
|
97
|
+
type: 'node_array',
|
|
98
|
+
node_types: ['nav_item'],
|
|
99
|
+
default_node_type: 'nav_item',
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
nav_item: {
|
|
103
|
+
url: { type: 'string' },
|
|
104
|
+
label: { type: 'string' },
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## Document serialization format (to drive an editing session in the browser)
|
|
111
|
+
|
|
112
|
+
A document is just a subsets of nodes, with a few rules:
|
|
113
|
+
|
|
114
|
+
- there must be a node (the document node) with the id of the document as an entry point (e.g. page_1)
|
|
115
|
+
- so the document is a node itself, with references to the underlying content (which live in separate nodes)
|
|
116
|
+
- all other nodes need to be traversible from that root node (unlinked nodes will be discarded on a save)
|
|
117
|
+
- in the serialization format the nodes need to be ordered, so that nodes that are referenced, are already defined (makes it easier to initialize the document)
|
|
118
|
+
- Your document must not contain cyclic references for that reason
|
|
119
|
+
|
|
120
|
+
Here's an example document:
|
|
121
|
+
```js
|
|
122
|
+
const serialized_doc = [
|
|
123
|
+
{
|
|
124
|
+
id: 'nav_item_1',
|
|
125
|
+
type: 'nav_item',
|
|
126
|
+
url: '/homepage',
|
|
127
|
+
label: 'Home',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'nav_1',
|
|
131
|
+
type: 'nav',
|
|
132
|
+
nav_items: ['nav_item_1'],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'paragraph_1',
|
|
136
|
+
content: ['Hello world.', []],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'list_item_1',
|
|
140
|
+
type: 'list_item',
|
|
141
|
+
content: ['first list item', []],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'list_item_2',
|
|
145
|
+
type: 'list_item',
|
|
146
|
+
content: ['second list item', []],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 'list_1',
|
|
150
|
+
type: 'list',
|
|
151
|
+
list_items: ['list_item_1', 'list_item_2'],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'page_1',
|
|
155
|
+
type: 'page',
|
|
156
|
+
body: ['nav_1', 'paragraph_1', 'list_1'],
|
|
157
|
+
},
|
|
158
|
+
]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Configuration
|
|
162
|
+
|
|
163
|
+
For Svedit to work, you also need to provide an app-specific config object, always available via doc.config for introspection.
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
const document_config = {
|
|
167
|
+
// Provide a custom id generator (ideally a UUID to avoid collisions)
|
|
168
|
+
generate_id: function() {
|
|
169
|
+
return nanoid();
|
|
170
|
+
},
|
|
171
|
+
system_components: {
|
|
172
|
+
NodeCursorTrap,
|
|
173
|
+
Overlay,
|
|
174
|
+
},
|
|
175
|
+
node_components: {
|
|
176
|
+
Page,
|
|
177
|
+
Button,
|
|
178
|
+
Text,
|
|
179
|
+
Story,
|
|
180
|
+
List,
|
|
181
|
+
ListItem,
|
|
182
|
+
ImageGrid,
|
|
183
|
+
ImageGridItem,
|
|
184
|
+
Hero
|
|
185
|
+
},
|
|
186
|
+
// TEMPORARY: Determines how many layouts are available for each node type
|
|
187
|
+
node_layouts: {
|
|
188
|
+
text: 4,
|
|
189
|
+
list: 5,
|
|
190
|
+
list_item: 1,
|
|
191
|
+
},
|
|
192
|
+
// Custom functions to insert new "blank" nodes and setting the selection
|
|
193
|
+
// depending on the intended behavior.
|
|
194
|
+
inserters: {
|
|
195
|
+
text: function(tr, content = ['', []], layout = 1) {
|
|
196
|
+
const new_text = {
|
|
197
|
+
id: nanoid(),
|
|
198
|
+
type: 'text',
|
|
199
|
+
layout,
|
|
200
|
+
content
|
|
201
|
+
};
|
|
202
|
+
tr.insert_nodes([new_text]);
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Document API
|
|
209
|
+
|
|
210
|
+
The Document API is central to Svedit. First you need to create a Document instance.
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
const doc = new Document(document_schema, serialized_doc, { config: document_config });
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
To read/traverse and write the document graph:
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
// get the body (=array of node ids)
|
|
220
|
+
const body = doc.get(['page_1', 'body']); // => ['nav_1', 'paragraph_1', 'list_1']
|
|
221
|
+
console.log($state.snapshot(body));
|
|
222
|
+
const nav = doc.get(['nav_1']) // => { id: 'nav_1', type: 'nav', nav_items: ['document_nav_item_1'] }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Transaction API
|
|
226
|
+
|
|
227
|
+
Documents need to be changed through transactions, which can consist of one or
|
|
228
|
+
multiple ops that are applied in a single step and are undo/redo-able.
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
console.log('nav.nav_items before:', $state.snapshot(nav.nav_items));
|
|
232
|
+
const tr = doc.tr; // creates a new transaction
|
|
233
|
+
const new_nav_items = nav.nav_items.slice(0, -1);
|
|
234
|
+
tr.set(['nav_1', 'nav_items'], new_nav_items);
|
|
235
|
+
doc.apply(tr); // applies the transaction to the document
|
|
236
|
+
console.log('nav.nav_items after:', $state.snapshot(nav.nav_items));
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Selections
|
|
240
|
+
|
|
241
|
+
Selections are at the heart of Svedit. There are just three types of selections:
|
|
242
|
+
|
|
243
|
+
1. **Text Selection**: A text selection spans across a range of characters in a string. E.g. the below example has a collapsed cursor at position 1 in a text property 'content'.
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
{
|
|
247
|
+
type: 'text',
|
|
248
|
+
path: ['page_1234', 'body', 0, 'content'],
|
|
249
|
+
anchor_offset: 1,
|
|
250
|
+
focus_offset: 1
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
2. **Node Selection**: A node selection spans across a range of nodes inside a node_array. The below example selects the nodes at index 3 and 4.
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
{
|
|
258
|
+
type: 'node',
|
|
259
|
+
path: ['page_1234', 'body'],
|
|
260
|
+
anchor_offset: 2,
|
|
261
|
+
focus_offset: 4
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
3. **Property Selection**: A property selection addresses one particular property of a node.
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
{
|
|
269
|
+
type: "property",
|
|
270
|
+
path: [
|
|
271
|
+
"page_1",
|
|
272
|
+
"body",
|
|
273
|
+
11,
|
|
274
|
+
"image"
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
You can access the current selection through `doc.selection` anytime. And you can programmatically set the selection using `doc.set_selection(new_selection)`.
|
|
280
|
+
|
|
281
|
+
## Rendering
|
|
282
|
+
|
|
283
|
+
Now you can start making your Svelte pages in-place editable by wrapping your design inside the `<Svedit>` component.
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
<Svedit {doc} path={[doc.document_id]} editable={true} />
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Full API docs?
|
|
290
|
+
|
|
291
|
+
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.
|
|
292
|
+
|
|
293
|
+
## Developing Svedit
|
|
294
|
+
|
|
295
|
+
Once you've cloned the Svedit repository and installed dependencies with `npm install`, start a development server:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
npm run dev
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Building
|
|
302
|
+
|
|
303
|
+
To create a production version of your app:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
npm run build
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
You can preview the production build with `npm run preview`.
|
|
310
|
+
|
|
311
|
+
## Contributing
|
|
312
|
+
|
|
313
|
+
At the very moment, the best way to help is to donate or to sponsor us, so we can buy time to work on this exclusively for a couple of more months. Please get in touch personally.
|
|
314
|
+
|
|
315
|
+
Find my contact details [here](https://editable.website).
|
|
316
|
+
|
|
317
|
+
## Beta version
|
|
318
|
+
|
|
319
|
+
It's early. Expect bugs. Expect missing features. Expect the need for more work on your part to make this work for your use case.
|
|
320
|
+
|
|
321
|
+
## Credits
|
|
322
|
+
|
|
323
|
+
Svedit led by [Michael Aufreiter](https://michaelaufreiter.com) with guidance and support from [Johannes Mutter](https://mutter.co).
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { getContext } from 'svelte';
|
|
3
|
+
|
|
4
|
+
/** @import { AnnotatedStringPropertyProps, AnnotationFragment } from './types.d.ts'; */
|
|
5
|
+
|
|
6
|
+
const svedit = getContext('svedit');
|
|
7
|
+
|
|
8
|
+
/** @type {AnnotatedStringPropertyProps} */
|
|
9
|
+
let {
|
|
10
|
+
path,
|
|
11
|
+
class: css_class,
|
|
12
|
+
placeholder = '',
|
|
13
|
+
} = $props();
|
|
14
|
+
|
|
15
|
+
let is_focused = $derived.by(() => {
|
|
16
|
+
return svedit.doc.selection?.type === 'text' && path.join('.') === svedit.doc.selection?.path.join('.');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Converts text with annotations into renderable fragments for display.
|
|
21
|
+
* @param {string} text - The plain text content
|
|
22
|
+
* @param {Array<[number, number, string, ...any]>} annotations - Array of annotations where each is [start_offset, end_offset, type, options?] (minimum 3 elements)
|
|
23
|
+
* @returns {Array<string|AnnotationFragment>} Array of fragments - strings for plain text, AnnotationFragment objects for annotated content
|
|
24
|
+
*/
|
|
25
|
+
function render_annotated_string(text, annotations) {
|
|
26
|
+
let fragments = [];
|
|
27
|
+
let last_index = 0;
|
|
28
|
+
|
|
29
|
+
// Sort annotations by start_offset
|
|
30
|
+
const sorted_annotations = $state.snapshot(annotations).sort((a, b) => a[0] - b[0]);
|
|
31
|
+
|
|
32
|
+
for (let [index, annotation] of sorted_annotations.entries()) {
|
|
33
|
+
// Add text before the annotation
|
|
34
|
+
if (annotation[0] > last_index) {
|
|
35
|
+
fragments.push(text.slice(last_index, annotation[0]));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Add the annotated string
|
|
39
|
+
const annotated_content = text.slice(annotation[0], annotation[1]);
|
|
40
|
+
fragments.push({
|
|
41
|
+
type: annotation[2],
|
|
42
|
+
content: annotated_content,
|
|
43
|
+
annotation_index: index,
|
|
44
|
+
data: annotation[3]
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
last_index = annotation[1];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add any remaining text after the last annotation
|
|
51
|
+
if (last_index < text.length) {
|
|
52
|
+
fragments.push(text.slice(last_index));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return fragments;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let fragments = $derived(render_annotated_string(svedit.doc.get(path)[0], svedit.doc.get(path)[1]));
|
|
59
|
+
let plain_text = $derived(svedit.doc.get(path)[0]);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {MouseEvent} e - The click event
|
|
63
|
+
*/
|
|
64
|
+
function handle_link_click(e) {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<!-- ATTENTION: The comments are needed to prevent unwanted text nodes with whitespace. -->
|
|
70
|
+
<div
|
|
71
|
+
data-type="text"
|
|
72
|
+
data-path={path.join('.')}
|
|
73
|
+
style="anchor-name: --{path.join('-')};"
|
|
74
|
+
class="text svedit-selectable {css_class}"
|
|
75
|
+
class:empty={plain_text.length === 0}
|
|
76
|
+
class:focused={is_focused}
|
|
77
|
+
placeholder={placeholder}
|
|
78
|
+
>
|
|
79
|
+
{#each fragments as fragment, index (index)}
|
|
80
|
+
{#if typeof fragment === 'string'}<!--
|
|
81
|
+
-->{fragment}<!--
|
|
82
|
+
-->{:else if fragment.type === 'emphasis'}<!--
|
|
83
|
+
--><em>{fragment.content}</em><!--
|
|
84
|
+
-->{:else if fragment.type === 'strong'}<!--
|
|
85
|
+
--><strong>{fragment.content}</strong><!--
|
|
86
|
+
-->{:else if fragment.type === 'link'}<!--
|
|
87
|
+
--><a
|
|
88
|
+
onclick={handle_link_click}
|
|
89
|
+
style="anchor-name: --{path.join('-') + '-' + fragment.annotation_index};"
|
|
90
|
+
href={fragment.data.href}
|
|
91
|
+
target={fragment.data.target || '_self'}>{fragment.content}</a><!--
|
|
92
|
+
-->{:else}<!--
|
|
93
|
+
-->{fragment.content}<!--
|
|
94
|
+
-->{/if}
|
|
95
|
+
{/each}<!--
|
|
96
|
+
--><br>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<style>
|
|
100
|
+
.text {
|
|
101
|
+
white-space: pre-wrap;
|
|
102
|
+
overflow-wrap: anywhere;
|
|
103
|
+
box-sizing: content-box;
|
|
104
|
+
|
|
105
|
+
&.heading1 {
|
|
106
|
+
text-wrap: var(--text-wrap);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
[placeholder].empty::before {
|
|
111
|
+
content: attr(placeholder);
|
|
112
|
+
pointer-events: none;
|
|
113
|
+
color: color-mix(in oklch, currentcolor 50%, transparent);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* We can safely hide the <br> element when the placeholder is empty and focused. */
|
|
117
|
+
/* No longer the cursor will be rendered after the placeholder when focused. */
|
|
118
|
+
[placeholder].empty.focused br {
|
|
119
|
+
display:none;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.text.focused {
|
|
123
|
+
background: none;
|
|
124
|
+
outline: 1px dashed var(--editing-stroke-color);
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export default AnnotatedStringProperty;
|
|
2
|
+
type AnnotatedStringProperty = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<AnnotatedStringPropertyProps>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const AnnotatedStringProperty: import("svelte").Component<import("./types.d.ts").AnnotatedStringPropertyProps, {}, "">;
|
|
7
|
+
//# sourceMappingURL=AnnotatedStringProperty.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnnotatedStringProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AnnotatedStringProperty.svelte.js"],"names":[],"mappings":";;;;;AAoFA,+HAAsE"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/** @import { CustomPropertyProps } from './types.d.ts'; */
|
|
3
|
+
|
|
4
|
+
/** @type {CustomPropertyProps} */
|
|
5
|
+
let {
|
|
6
|
+
path,
|
|
7
|
+
class: css_class,
|
|
8
|
+
children,
|
|
9
|
+
} = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<!-- NOTE: It's super crucial that there is no extra whitespace, to not introduce additional cursor positions in contenteditable -->
|
|
13
|
+
<div
|
|
14
|
+
class="property {css_class}"
|
|
15
|
+
data-type="property"
|
|
16
|
+
data-path={path.join('.')}
|
|
17
|
+
style="anchor-name: --{path.join('-')};"
|
|
18
|
+
>
|
|
19
|
+
<div class="cursor-trap svedit-selectable"><br/></div>
|
|
20
|
+
<div class="property-content" contenteditable="false">{@render children()}</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
.property {
|
|
25
|
+
position: relative;
|
|
26
|
+
}
|
|
27
|
+
.cursor-trap {
|
|
28
|
+
position: absolute;
|
|
29
|
+
top: 0;
|
|
30
|
+
left: 0;
|
|
31
|
+
right: 0;
|
|
32
|
+
bottom: 0;
|
|
33
|
+
z-index: 1;
|
|
34
|
+
outline: none;
|
|
35
|
+
}
|
|
36
|
+
.property-content {
|
|
37
|
+
width: 100%;
|
|
38
|
+
height: 100%;
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export default CustomProperty;
|
|
2
|
+
type CustomProperty = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<CustomPropertyProps>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const CustomProperty: import("svelte").Component<CustomPropertyProps, {}, "">;
|
|
7
|
+
import type { CustomPropertyProps } from './types.d.ts';
|
|
8
|
+
//# sourceMappingURL=CustomProperty.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CustomProperty.svelte.d.ts","sourceRoot":"","sources":["../src/lib/CustomProperty.svelte.js"],"names":[],"mappings":";;;;;AAwBA,sFAA6D;yCApBlB,cAAc"}
|