@quillmark/svelte 0.4.1 → 0.4.2
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.
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
const els: Record<string, { focus: () => void } | undefined> = $state({});
|
|
108
108
|
let addEl: HTMLButtonElement | undefined = $state();
|
|
109
109
|
let rootEl: HTMLElement | undefined = $state();
|
|
110
|
+
let rowsEl: HTMLElement | undefined = $state();
|
|
110
111
|
|
|
111
112
|
// The awaited flush below is the only work that outlives a gesture here, so the
|
|
112
113
|
// span carries no cancellers: it is the liveness `focusAfterFlush` asks for.
|
|
@@ -165,6 +166,14 @@
|
|
|
165
166
|
// nothing about focusing it that the DOM does not already know.
|
|
166
167
|
rootEl?.querySelector<HTMLTextAreaElement>('.qm-array-row textarea')?.focus();
|
|
167
168
|
}
|
|
169
|
+
/** The box an arrival wash blooms in (`leaves.ts`, `core/bloom.ts`): the elements,
|
|
170
|
+
* not the header above them. This component owns the field's label, so the wrapper
|
|
171
|
+
* `Field` blooms every other control inside would wash the label here too. Empty,
|
|
172
|
+
* the box is `display: none` and the landing is answered by the focus the add
|
|
173
|
+
* affordance takes. */
|
|
174
|
+
export function washBox(): HTMLElement | undefined {
|
|
175
|
+
return rowsEl;
|
|
176
|
+
}
|
|
168
177
|
/** Take the caret to element `k`: what a landing on an element address resolves to
|
|
169
178
|
* (`leaves.ts`). The index resolves to the element's session id here, at the call,
|
|
170
179
|
* never carried as one — an index is stale the moment anything above it splices.
|
|
@@ -243,46 +252,48 @@
|
|
|
243
252
|
onclick={add}>{t.strings.arrayAdd}</button
|
|
244
253
|
>
|
|
245
254
|
</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
|
-
|
|
255
|
+
<div class="qm-array-rows" class:empty={ids.length === 0} bind:this={rowsEl}>
|
|
256
|
+
{#each ids as id, k (id)}
|
|
257
|
+
<div class="qm-array-row">
|
|
258
|
+
{#if control === 'prose'}
|
|
259
|
+
<ProseArrayElement
|
|
260
|
+
bind:this={els[id]}
|
|
261
|
+
value={(arr[k] ?? emptyElement()) as Content}
|
|
262
|
+
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
263
|
+
onChange={(rt) => commitElement(k, rt)}
|
|
264
|
+
onKey={(e) => onElementKey(e, k)}
|
|
265
|
+
/>
|
|
266
|
+
{:else if control === 'object'}
|
|
267
|
+
<textarea
|
|
268
|
+
class="qm-input qm-json qm-focus-ring"
|
|
269
|
+
aria-label={label != null ? `${label} ${k + 1}` : undefined}
|
|
270
|
+
value={JSON.stringify(arr[k] ?? {})}
|
|
271
|
+
onchange={(e) => {
|
|
272
|
+
try {
|
|
273
|
+
commitElement(k, JSON.parse((e.currentTarget as HTMLTextAreaElement).value));
|
|
274
|
+
} catch {
|
|
275
|
+
/* keep prior value on invalid JSON */
|
|
276
|
+
}
|
|
277
|
+
}}
|
|
278
|
+
></textarea>
|
|
279
|
+
{:else}
|
|
280
|
+
<TextField
|
|
281
|
+
bind:this={els[id]}
|
|
282
|
+
value={String(arr[k] ?? '')}
|
|
283
|
+
label={label != null ? `${label} ${k + 1}` : undefined}
|
|
284
|
+
onCommit={(v) => commitElement(k, v)}
|
|
285
|
+
onKey={(e) => onElementKey(e, k)}
|
|
286
|
+
/>
|
|
287
|
+
{/if}
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
290
|
+
class="qm-icon-btn qm-remove qm-focus-ring"
|
|
291
|
+
title={t.strings.arrayRemove}
|
|
292
|
+
onclick={() => remove(k)}><Icon name="minus" /></button
|
|
293
|
+
>
|
|
294
|
+
</div>
|
|
295
|
+
{/each}
|
|
296
|
+
</div>
|
|
286
297
|
</div>
|
|
287
298
|
|
|
288
299
|
<style>
|
|
@@ -302,6 +313,24 @@
|
|
|
302
313
|
justify-content: space-between;
|
|
303
314
|
gap: var(--_qm-space-2);
|
|
304
315
|
}
|
|
316
|
+
/* The elements, in a box of their own: the arrival wash blooms here rather than over
|
|
317
|
+
the wrapper `Field` hands this component (`washBox`), which is the label's box too.
|
|
318
|
+
Positioned for the wash's inset child, and rounded to the rung a row's own box
|
|
319
|
+
draws, the way `.qm-field-control` is for every other control.
|
|
320
|
+
|
|
321
|
+
`display: none` when there are no elements, so the header does not stand a gap above
|
|
322
|
+
an empty box; a wash over it then paints nothing, which is what an array with
|
|
323
|
+
nothing in it has to show. */
|
|
324
|
+
.qm-array-rows {
|
|
325
|
+
position: relative;
|
|
326
|
+
display: flex;
|
|
327
|
+
flex-direction: column;
|
|
328
|
+
gap: var(--_qm-space);
|
|
329
|
+
border-radius: var(--_qm-radius-inner);
|
|
330
|
+
}
|
|
331
|
+
.qm-array-rows.empty {
|
|
332
|
+
display: none;
|
|
333
|
+
}
|
|
305
334
|
/* A grid rather than a block: an `<input>` and a `<textarea>` are inline-level and
|
|
306
335
|
would sit on a baseline, standing the row a descender taller than the box the slab
|
|
307
336
|
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
|
});
|