@quillmark/svelte 0.4.1 → 0.4.3
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.
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
// rest per codec"), and the read that decodes a resting value to `Content` addresses
|
|
66
66
|
// a field, never an element (`getContent`; borb-sh/quillmark#1243), so a string is
|
|
67
67
|
// what the row is handed and a string is what it commits. The prose element stays
|
|
68
|
-
// the `richtext` arm
|
|
68
|
+
// the `richtext` arm: `Content` after a bound load or a commit, the authored
|
|
69
|
+
// string through the transport door, lowered in the row for the same reason.
|
|
69
70
|
//
|
|
70
71
|
// An array declaring no `items` has text elements.
|
|
71
72
|
const control = $derived(items && items.type !== 'plaintext' ? controlKind(items) : 'text');
|
|
@@ -107,6 +108,7 @@
|
|
|
107
108
|
const els: Record<string, { focus: () => void } | undefined> = $state({});
|
|
108
109
|
let addEl: HTMLButtonElement | undefined = $state();
|
|
109
110
|
let rootEl: HTMLElement | undefined = $state();
|
|
111
|
+
let rowsEl: HTMLElement | undefined = $state();
|
|
110
112
|
|
|
111
113
|
// The awaited flush below is the only work that outlives a gesture here, so the
|
|
112
114
|
// span carries no cancellers: it is the liveness `focusAfterFlush` asks for.
|
|
@@ -165,6 +167,14 @@
|
|
|
165
167
|
// nothing about focusing it that the DOM does not already know.
|
|
166
168
|
rootEl?.querySelector<HTMLTextAreaElement>('.qm-array-row textarea')?.focus();
|
|
167
169
|
}
|
|
170
|
+
/** The box an arrival wash blooms in (`leaves.ts`, `core/bloom.ts`): the elements,
|
|
171
|
+
* not the header above them. This component owns the field's label, so the wrapper
|
|
172
|
+
* `Field` blooms every other control inside would wash the label here too. Empty,
|
|
173
|
+
* the box is `display: none` and the landing is answered by the focus the add
|
|
174
|
+
* affordance takes. */
|
|
175
|
+
export function washBox(): HTMLElement | undefined {
|
|
176
|
+
return rowsEl;
|
|
177
|
+
}
|
|
168
178
|
/** Take the caret to element `k`: what a landing on an element address resolves to
|
|
169
179
|
* (`leaves.ts`). The index resolves to the element's session id here, at the call,
|
|
170
180
|
* never carried as one — an index is stale the moment anything above it splices.
|
|
@@ -188,10 +198,15 @@
|
|
|
188
198
|
}
|
|
189
199
|
/** Whether element `k` reads empty to the user. A text element's committed value
|
|
190
200
|
* lags the input: a cleared field commits at `change`, not per keystroke
|
|
191
|
-
* ({@link TextField}); so the input's own value is the truth
|
|
192
|
-
* commits every edit, so the committed `Content` is
|
|
201
|
+
* ({@link TextField}); so the input's own value is the truth. A prose element
|
|
202
|
+
* commits every edit, so the committed `Content` is; an authored string, the
|
|
203
|
+
* transport-door rest, is empty when it has no characters. */
|
|
193
204
|
function elementEmpty(k: number, target: EventTarget | null): boolean {
|
|
194
|
-
if (control === 'prose')
|
|
205
|
+
if (control === 'prose') {
|
|
206
|
+
const el = arr[k];
|
|
207
|
+
if (typeof el === 'string') return el.length === 0;
|
|
208
|
+
return !(el as Content | undefined)?.text;
|
|
209
|
+
}
|
|
195
210
|
return target instanceof HTMLInputElement && !target.value;
|
|
196
211
|
}
|
|
197
212
|
/**
|
|
@@ -243,46 +258,48 @@
|
|
|
243
258
|
onclick={add}>{t.strings.arrayAdd}</button
|
|
244
259
|
>
|
|
245
260
|
</div>
|
|
246
|
-
{
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
261
|
+
<div class="qm-array-rows" class:empty={ids.length === 0} bind:this={rowsEl}>
|
|
262
|
+
{#each ids as id, k (id)}
|
|
263
|
+
<div class="qm-array-row">
|
|
264
|
+
{#if control === 'prose'}
|
|
265
|
+
<ProseArrayElement
|
|
266
|
+
bind:this={els[id]}
|
|
267
|
+
value={(arr[k] ?? emptyElement()) as Content | string}
|
|
268
|
+
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
269
|
+
onChange={(rt) => commitElement(k, rt)}
|
|
270
|
+
onKey={(e) => onElementKey(e, k)}
|
|
271
|
+
/>
|
|
272
|
+
{:else if control === 'object'}
|
|
273
|
+
<textarea
|
|
274
|
+
class="qm-input qm-json qm-focus-ring"
|
|
275
|
+
aria-label={label != null ? `${label} ${k + 1}` : undefined}
|
|
276
|
+
value={JSON.stringify(arr[k] ?? {})}
|
|
277
|
+
onchange={(e) => {
|
|
278
|
+
try {
|
|
279
|
+
commitElement(k, JSON.parse((e.currentTarget as HTMLTextAreaElement).value));
|
|
280
|
+
} catch {
|
|
281
|
+
/* keep prior value on invalid JSON */
|
|
282
|
+
}
|
|
283
|
+
}}
|
|
284
|
+
></textarea>
|
|
285
|
+
{:else}
|
|
286
|
+
<TextField
|
|
287
|
+
bind:this={els[id]}
|
|
288
|
+
value={String(arr[k] ?? '')}
|
|
289
|
+
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
290
|
+
onCommit={(v) => commitElement(k, v)}
|
|
291
|
+
onKey={(e) => onElementKey(e, k)}
|
|
292
|
+
/>
|
|
293
|
+
{/if}
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
class="qm-icon-btn qm-remove qm-focus-ring"
|
|
297
|
+
title={t.strings.arrayRemove}
|
|
298
|
+
onclick={() => remove(k)}><Icon name="minus" /></button
|
|
299
|
+
>
|
|
300
|
+
</div>
|
|
301
|
+
{/each}
|
|
302
|
+
</div>
|
|
286
303
|
</div>
|
|
287
304
|
|
|
288
305
|
<style>
|
|
@@ -302,6 +319,24 @@
|
|
|
302
319
|
justify-content: space-between;
|
|
303
320
|
gap: var(--_qm-space-2);
|
|
304
321
|
}
|
|
322
|
+
/* The elements, in a box of their own: the arrival wash blooms here rather than over
|
|
323
|
+
the wrapper `Field` hands this component (`washBox`), which is the label's box too.
|
|
324
|
+
Positioned for the wash's inset child, and rounded to the rung a row's own box
|
|
325
|
+
draws, the way `.qm-field-control` is for every other control.
|
|
326
|
+
|
|
327
|
+
`display: none` when there are no elements, so the header does not stand a gap above
|
|
328
|
+
an empty box; a wash over it then paints nothing, which is what an array with
|
|
329
|
+
nothing in it has to show. */
|
|
330
|
+
.qm-array-rows {
|
|
331
|
+
position: relative;
|
|
332
|
+
display: flex;
|
|
333
|
+
flex-direction: column;
|
|
334
|
+
gap: var(--_qm-space);
|
|
335
|
+
border-radius: var(--_qm-radius-inner);
|
|
336
|
+
}
|
|
337
|
+
.qm-array-rows.empty {
|
|
338
|
+
display: none;
|
|
339
|
+
}
|
|
305
340
|
/* A grid rather than a block: an `<input>` and a `<textarea>` are inline-level and
|
|
306
341
|
would sit on a baseline, standing the row a descender taller than the box the slab
|
|
307
342
|
measures itself against. `minmax(0, …)` because a long unbroken value grows an `auto`
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -117,7 +117,14 @@
|
|
|
117
117
|
// object on its first property.
|
|
118
118
|
let proseEl = $state<{ focus: () => void } | undefined>();
|
|
119
119
|
let dateEl = $state<{ focus: () => void } | undefined>();
|
|
120
|
-
let arrayEl = $state<
|
|
120
|
+
let arrayEl = $state<
|
|
121
|
+
| {
|
|
122
|
+
focus: () => void;
|
|
123
|
+
focusElement: (k: number) => void;
|
|
124
|
+
washBox: () => HTMLElement | undefined;
|
|
125
|
+
}
|
|
126
|
+
| undefined
|
|
127
|
+
>();
|
|
121
128
|
let objectEl = $state<{ focus: () => void } | undefined>();
|
|
122
129
|
function focusControl(): void {
|
|
123
130
|
const owner = proseEl ?? dateEl ?? arrayEl ?? objectEl;
|
|
@@ -135,7 +142,11 @@
|
|
|
135
142
|
|
|
136
143
|
/**
|
|
137
144
|
* This field's landing handle. The wrapper is the bloom host rather than the
|
|
138
|
-
* control: `bloomInside` appends an inset child and an `<input>` holds none.
|
|
145
|
+
* control: `bloomInside` appends an inset child and an `<input>` holds none. The
|
|
146
|
+
* label is outside it, and stays out of the wash: an arrival marks where the caret
|
|
147
|
+
* landed, which is the control. The one control that owns its label is the array, so
|
|
148
|
+
* that one names its own box (`ArrayField.washBox`), read at the bloom the way
|
|
149
|
+
* `focusElement` is read at the call.
|
|
139
150
|
*
|
|
140
151
|
* A prose leaf is absent here — it registers its own controller from inside
|
|
141
152
|
* `ProseField`, carrying the codec seam this handle has no half of — and reactive
|
|
@@ -150,10 +161,13 @@
|
|
|
150
161
|
if (field.control === 'prose' || !controlEl || !leaves) return;
|
|
151
162
|
const key = leafKey;
|
|
152
163
|
const registry = leaves;
|
|
164
|
+
const wrapper = controlEl;
|
|
153
165
|
registry.registerControl(key, {
|
|
154
166
|
focus: focusControl,
|
|
155
167
|
focusElement: field.control === 'array' ? focusElement : undefined,
|
|
156
|
-
el
|
|
168
|
+
get el() {
|
|
169
|
+
return arrayEl?.washBox() ?? wrapper;
|
|
170
|
+
}
|
|
157
171
|
});
|
|
158
172
|
return () => registry.unregisterControl(key);
|
|
159
173
|
});
|
|
@@ -8,17 +8,25 @@
|
|
|
8
8
|
by value (`writer.set(field, arrayWithElementReplaced)`). Anchors within an
|
|
9
9
|
element are dropped on that value write: acceptable for inline refs. Mounts
|
|
10
10
|
once per stable element id (no reset on the parent's re-derive).
|
|
11
|
+
|
|
12
|
+
A transport-door document rests each element as the authored string. A scalar
|
|
13
|
+
leaf reads that through `getContent`; `getContent` names a field, never an
|
|
14
|
+
element, so this row calls the boundary's `importMarkdown` itself. That is
|
|
15
|
+
the richtext codec, which is this row's `items` type; `decode` is the PM
|
|
16
|
+
mapping and assumes `Content`. A string here is the missing element read,
|
|
17
|
+
not a codec failure.
|
|
11
18
|
-->
|
|
12
19
|
<script lang="ts">
|
|
13
20
|
import { onMount } from 'svelte';
|
|
14
21
|
import { EditorState } from 'prosemirror-state';
|
|
15
22
|
import { EditorView } from 'prosemirror-view';
|
|
16
23
|
import { decode, pmToContent, inlineSchema, proseLeafPlugins } from '../core/codec/index.js';
|
|
24
|
+
import { core } from '../core/lifecycle.js';
|
|
17
25
|
import './controls.css';
|
|
18
26
|
import type { Content } from '@quillmark/wasm';
|
|
19
27
|
|
|
20
28
|
interface Props {
|
|
21
|
-
value: Content;
|
|
29
|
+
value: Content | string;
|
|
22
30
|
/** Accessible name for the editable region (the array has no per-element label). */
|
|
23
31
|
label?: string;
|
|
24
32
|
onChange: (rt: Content) => void;
|
|
@@ -44,7 +52,8 @@
|
|
|
44
52
|
// plugin stack a `createField` leaf mounts (shared `proseLeafPlugins`), minus
|
|
45
53
|
// the anchor-position plugin (element anchors are dropped on the array's value
|
|
46
54
|
// write, per the header). Its own `dispatchTransaction` hands `Content` up.
|
|
47
|
-
const
|
|
55
|
+
const rt = typeof value === 'string' ? core().importMarkdown(value) : value;
|
|
56
|
+
const pmDoc = decode(rt, inlineSchema);
|
|
48
57
|
const state = EditorState.create({
|
|
49
58
|
doc: pmDoc,
|
|
50
59
|
plugins: proseLeafPlugins(inlineSchema, { inline: true })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import './controls.css';
|
|
2
2
|
import type { Content } from '@quillmark/wasm';
|
|
3
3
|
interface Props {
|
|
4
|
-
value: Content;
|
|
4
|
+
value: Content | string;
|
|
5
5
|
/** Accessible name for the editable region (the array has no per-element label). */
|
|
6
6
|
label?: string;
|
|
7
7
|
onChange: (rt: Content) => void;
|