@quillmark/svelte 0.22.0 → 0.23.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/dist/core/codec/cap.d.ts +21 -0
- package/dist/core/codec/cap.js +63 -0
- package/dist/core/codec/field.d.ts +7 -0
- package/dist/core/codec/field.js +8 -1
- package/dist/core/codec/index.d.ts +2 -0
- package/dist/core/codec/index.js +1 -0
- package/dist/visual/ArrayField.svelte +2 -0
- package/dist/visual/Field.svelte +13 -1
- package/dist/visual/FieldLabel.svelte +15 -1
- package/dist/visual/FieldLabel.svelte.d.ts +4 -0
- package/dist/visual/ObjectField.svelte +12 -2
- package/dist/visual/ProseField.svelte +10 -1
- package/dist/visual/ProseField.svelte.d.ts +3 -0
- package/dist/visual/ProseValue.svelte +7 -2
- package/dist/visual/ProseValue.svelte.d.ts +3 -0
- package/dist/visual/strings.d.ts +6 -0
- package/dist/visual/strings.js +1 -0
- package/dist/visual/structure.d.ts +7 -3
- package/dist/visual/structure.js +19 -4
- package/package.json +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Node as PMNode } from 'prosemirror-model';
|
|
2
|
+
import { Plugin } from 'prosemirror-state';
|
|
3
|
+
/** A leaf's character cap: the schema's `max:`, and whether the count drops trailing
|
|
4
|
+
* whitespace, which a one-line `text` does and nothing else. */
|
|
5
|
+
export interface TextCap {
|
|
6
|
+
max: number;
|
|
7
|
+
trimEnd?: boolean;
|
|
8
|
+
/** The count, at mount and after every change to the document. */
|
|
9
|
+
onCount?: (count: number) => void;
|
|
10
|
+
}
|
|
11
|
+
/** The characters `doc` holds as `FieldSchema::char_count` counts them: Unicode scalar
|
|
12
|
+
* values of the plain projection, islands counting nothing and trailing line breaks
|
|
13
|
+
* none. */
|
|
14
|
+
export declare function capCount(doc: PMNode, trimEnd?: boolean): number;
|
|
15
|
+
/**
|
|
16
|
+
* Refuses a transaction leaving the leaf past `max`, so a keystroke at the cap lands
|
|
17
|
+
* nothing, and cuts a paste to what fits. Past the cap, a transaction passes where it
|
|
18
|
+
* leaves the count no higher, so a value stored over the cap still edits down; undo and
|
|
19
|
+
* redo pass, each restoring a state the leaf already held.
|
|
20
|
+
*/
|
|
21
|
+
export declare function capPlugin(cap: TextCap): Plugin;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { isHistoryTransaction } from 'prosemirror-history';
|
|
2
|
+
import { Plugin } from 'prosemirror-state';
|
|
3
|
+
import { usvLength } from './decode.js';
|
|
4
|
+
import { scanDoc } from './encode.js';
|
|
5
|
+
import { ISLAND_SLOT } from './islands.js';
|
|
6
|
+
/** The characters `doc` holds as `FieldSchema::char_count` counts them: Unicode scalar
|
|
7
|
+
* values of the plain projection, islands counting nothing and trailing line breaks
|
|
8
|
+
* none. */
|
|
9
|
+
export function capCount(doc, trimEnd = false) {
|
|
10
|
+
const text = scanDoc(doc).text.replaceAll(ISLAND_SLOT, '');
|
|
11
|
+
return usvLength(trimEnd ? text.trimEnd() : text.replace(/[\n\r]+$/, ''));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Refuses a transaction leaving the leaf past `max`, so a keystroke at the cap lands
|
|
15
|
+
* nothing, and cuts a paste to what fits. Past the cap, a transaction passes where it
|
|
16
|
+
* leaves the count no higher, so a value stored over the cap still edits down; undo and
|
|
17
|
+
* redo pass, each restoring a state the leaf already held.
|
|
18
|
+
*/
|
|
19
|
+
export function capPlugin(cap) {
|
|
20
|
+
const count = (doc) => capCount(doc, cap.trimEnd);
|
|
21
|
+
return new Plugin({
|
|
22
|
+
filterTransaction: (tr) => {
|
|
23
|
+
if (!tr.docChanged || isHistoryTransaction(tr))
|
|
24
|
+
return true;
|
|
25
|
+
const after = count(tr.doc);
|
|
26
|
+
return after <= cap.max || after <= count(tr.before);
|
|
27
|
+
},
|
|
28
|
+
props: {
|
|
29
|
+
handlePaste: (view, _event, slice) => {
|
|
30
|
+
const { state } = view;
|
|
31
|
+
const pasted = () => state.tr.replaceSelection(slice);
|
|
32
|
+
if (count(pasted().doc) <= cap.max)
|
|
33
|
+
return false;
|
|
34
|
+
const tr = pasted();
|
|
35
|
+
const start = tr.mapping.map(state.selection.from, -1);
|
|
36
|
+
const end = tr.mapping.map(state.selection.to, 1);
|
|
37
|
+
const fits = (cut) => count(pasted().delete(cut, end).doc) <= cap.max;
|
|
38
|
+
if (!fits(start))
|
|
39
|
+
return true;
|
|
40
|
+
let lo = start;
|
|
41
|
+
let hi = end;
|
|
42
|
+
while (hi - lo > 1) {
|
|
43
|
+
const mid = (lo + hi) >> 1;
|
|
44
|
+
if (fits(mid))
|
|
45
|
+
lo = mid;
|
|
46
|
+
else
|
|
47
|
+
hi = mid;
|
|
48
|
+
}
|
|
49
|
+
view.dispatch(tr.delete(lo, end).scrollIntoView().setMeta('uiEvent', 'paste'));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
view: (v) => {
|
|
54
|
+
cap.onCount?.(count(v.state.doc));
|
|
55
|
+
return {
|
|
56
|
+
update: (next, prev) => {
|
|
57
|
+
if (next.state.doc !== prev.doc)
|
|
58
|
+
cap.onCount?.(count(next.state.doc));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Plugin } from 'prosemirror-state';
|
|
|
3
3
|
import { EditorView } from 'prosemirror-view';
|
|
4
4
|
import type { Document, Content, Addr, Quill } from '@quillmark/wasm';
|
|
5
5
|
import type { EditorErrorHandler } from '../errors.js';
|
|
6
|
+
import { type TextCap } from './cap.js';
|
|
6
7
|
import { type TableChromeStrings } from './table-view.js';
|
|
7
8
|
import { type SlashState, type SlashStrings } from './slash.js';
|
|
8
9
|
/** Options for {@link createField}. */
|
|
@@ -75,6 +76,9 @@ export interface CreateFieldOpts {
|
|
|
75
76
|
* a body's invitation. The initial value; {@link FieldController.setPlaceholder}
|
|
76
77
|
* moves it after mount. Empty/absent shows no ghost. */
|
|
77
78
|
placeholder?: string;
|
|
79
|
+
/** The schema's `max:` on the field: a character count no edit takes the leaf past
|
|
80
|
+
* (`cap.ts`). */
|
|
81
|
+
cap?: TextCap;
|
|
78
82
|
/** The placeholder goes at the leaf's first edit: the edit answers the leaf, and
|
|
79
83
|
* emptied it holds an empty answer, which prints empty. Absent, the placeholder
|
|
80
84
|
* returns whenever the leaf is empty. */
|
|
@@ -224,5 +228,8 @@ export declare function proseLeafPlugins(schema: Schema, opts: {
|
|
|
224
228
|
placeholder?: () => string | undefined;
|
|
225
229
|
/** {@link CreateFieldOpts.placeholderUntilEdit}. */
|
|
226
230
|
placeholderUntilEdit?: boolean;
|
|
231
|
+
/** {@link CreateFieldOpts.cap}. Ahead of every other paste handler, which a paste
|
|
232
|
+
* it cuts never reaches. */
|
|
233
|
+
cap?: TextCap;
|
|
227
234
|
afterHistory?: Plugin[];
|
|
228
235
|
}): Plugin[];
|
package/dist/core/codec/field.js
CHANGED
|
@@ -24,6 +24,7 @@ import { reportError, errorMessage } from '../errors.js';
|
|
|
24
24
|
import { decode, fitsLeaf } from './decode.js';
|
|
25
25
|
import { usvToPM, pmToUsv, buildLineIndex, lineIndexOf } from './positions.js';
|
|
26
26
|
import { lower, pmToContent, scanContent, scanDoc, contentEdit } from './encode.js';
|
|
27
|
+
import { capPlugin } from './cap.js';
|
|
27
28
|
import { islandPastePlugin } from './islands.js';
|
|
28
29
|
import { anchorsFromContent } from './marks.js';
|
|
29
30
|
import { contentEqual, createReconciler } from './reconcile.js';
|
|
@@ -282,6 +283,7 @@ export function createField(opts) {
|
|
|
282
283
|
// given one later; the plugin draws nothing while the text is empty.
|
|
283
284
|
placeholder: () => placeholderText,
|
|
284
285
|
placeholderUntilEdit: opts.placeholderUntilEdit,
|
|
286
|
+
cap: opts.cap,
|
|
285
287
|
afterHistory: [anchorPlugin(seededAnchors)]
|
|
286
288
|
})
|
|
287
289
|
});
|
|
@@ -547,7 +549,12 @@ export function createField(opts) {
|
|
|
547
549
|
* its author typed without a second rule saying so.
|
|
548
550
|
*/
|
|
549
551
|
export function proseLeafPlugins(schema, opts) {
|
|
550
|
-
const list = [
|
|
552
|
+
const list = [
|
|
553
|
+
history(),
|
|
554
|
+
...(opts.cap ? [capPlugin(opts.cap)] : []),
|
|
555
|
+
...(opts.afterHistory ?? []),
|
|
556
|
+
linebreakPlugin(schema)
|
|
557
|
+
];
|
|
551
558
|
if (opts.slash)
|
|
552
559
|
list.push(slashPlugin(opts.slash, opts.slashHint));
|
|
553
560
|
if (!opts.noInputRules)
|
|
@@ -2,6 +2,8 @@ export { rangeAnchor } from './anchor.js';
|
|
|
2
2
|
export type { RangeAnchor } from './anchor.js';
|
|
3
3
|
export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
|
|
4
4
|
export type { CreateFieldOpts, FieldController, LeafViews } from './field.js';
|
|
5
|
+
export { capCount } from './cap.js';
|
|
6
|
+
export type { TextCap } from './cap.js';
|
|
5
7
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
6
8
|
export type { TableChromeStrings } from './table-view.js';
|
|
7
9
|
export { DEFAULT_SLASH_STRINGS } from './slash.js';
|
package/dist/core/codec/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import './prose.css';
|
|
|
14
14
|
export { rangeAnchor } from './anchor.js';
|
|
15
15
|
// The prose leaf.
|
|
16
16
|
export { createField, emptyContent, heldAttributes, isEmptyContent, proseAttributes, proseLeafPlugins } from './field.js';
|
|
17
|
+
export { capCount } from './cap.js';
|
|
17
18
|
// The table island's chrome vocabulary: the visual tier's `strings` set extends it,
|
|
18
19
|
// so the island's wording is overridden beside every other key.
|
|
19
20
|
export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
literalText,
|
|
75
75
|
rowSummary,
|
|
76
76
|
schemaAt,
|
|
77
|
+
textCap,
|
|
77
78
|
type ArrayLayout
|
|
78
79
|
} from './structure.js';
|
|
79
80
|
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
@@ -800,6 +801,7 @@
|
|
|
800
801
|
content={() => readAt([k])}
|
|
801
802
|
value={arr[k]}
|
|
802
803
|
plaintext={items != null && literalText(items)}
|
|
804
|
+
cap={textCap(items)}
|
|
803
805
|
label={rowName(k)}
|
|
804
806
|
onChange={(rt) => commitElement(k, rt)}
|
|
805
807
|
onKey={(e) => onElementKey(e, k)}
|
package/dist/visual/Field.svelte
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
printedText,
|
|
41
41
|
renderedBoolean,
|
|
42
42
|
stringifyGhost,
|
|
43
|
+
textCap,
|
|
43
44
|
titleText
|
|
44
45
|
} from './structure.js';
|
|
45
46
|
import { wording } from './strings.js';
|
|
@@ -163,6 +164,14 @@
|
|
|
163
164
|
// reference must vanish with it: `aria-describedby` pointing at nothing describes
|
|
164
165
|
// nothing, and silently.
|
|
165
166
|
const describedBy = $derived(field.description ? domIds.description : undefined);
|
|
167
|
+
// A capped prose leaf reports its count, and the count describes it too: a keystroke
|
|
168
|
+
// the cap refuses changes nothing else a reader can perceive.
|
|
169
|
+
let chars = $state<number>();
|
|
170
|
+
const cap = $derived(field.control === 'prose' ? textCap(field.schema) : undefined);
|
|
171
|
+
const countId = $derived(`${domIds.control}-count`);
|
|
172
|
+
const proseDescribedBy = $derived(
|
|
173
|
+
cap ? [describedBy, countId].filter(Boolean).join(' ') : describedBy
|
|
174
|
+
);
|
|
166
175
|
// Two controls own their label row, the add chip and the held count sharing it.
|
|
167
176
|
const ownsLabel = $derived(field.control === 'array' || field.control === 'matrix');
|
|
168
177
|
const flat = $derived(flatObject(field.schema));
|
|
@@ -285,6 +294,8 @@
|
|
|
285
294
|
descriptionId={domIds.description}
|
|
286
295
|
{onActivate}
|
|
287
296
|
description={field.description}
|
|
297
|
+
count={cap && chars != null ? t.strings.textCount(chars, cap.max) : undefined}
|
|
298
|
+
{countId}
|
|
288
299
|
/>
|
|
289
300
|
{/if}
|
|
290
301
|
<!-- The control and what hangs under it, in one grid cell: the diagnostics stack
|
|
@@ -305,7 +316,8 @@
|
|
|
305
316
|
placeholder={none}
|
|
306
317
|
emptyUnsets={optional}
|
|
307
318
|
labelledBy={domIds.label}
|
|
308
|
-
{
|
|
319
|
+
describedBy={proseDescribedBy}
|
|
320
|
+
cap={cap && { ...cap, onCount: (n) => (chars = n) }}
|
|
309
321
|
{leafKey}
|
|
310
322
|
{onFocus}
|
|
311
323
|
{onCaretMove}
|
|
@@ -40,8 +40,13 @@
|
|
|
40
40
|
onActivate?: () => void;
|
|
41
41
|
/** Schema `description`, or undefined: the affordance renders only when set. */
|
|
42
42
|
description?: string;
|
|
43
|
+
/** A capped field's character count, drawn at the row's end. */
|
|
44
|
+
count?: string;
|
|
45
|
+
/** The count's own id, for the control's `aria-describedby`. */
|
|
46
|
+
countId?: string;
|
|
43
47
|
}
|
|
44
|
-
let { label, controlId, id, descriptionId, onActivate, description }: Props =
|
|
48
|
+
let { label, controlId, id, descriptionId, onActivate, description, count, countId }: Props =
|
|
49
|
+
$props();
|
|
45
50
|
</script>
|
|
46
51
|
|
|
47
52
|
<div class="qm-field-label-row">
|
|
@@ -68,6 +73,9 @@
|
|
|
68
73
|
<FieldHint {description} {label} describedBy={descriptionId} />
|
|
69
74
|
<span class="qm-visually-hidden" id={descriptionId}>{description}</span>
|
|
70
75
|
{/if}
|
|
76
|
+
{#if count != null}
|
|
77
|
+
<span class="qm-field-count" id={countId}>{count}</span>
|
|
78
|
+
{/if}
|
|
71
79
|
</div>
|
|
72
80
|
|
|
73
81
|
<style>
|
|
@@ -91,6 +99,12 @@
|
|
|
91
99
|
color: var(--_qm-ink-label);
|
|
92
100
|
min-width: 0;
|
|
93
101
|
}
|
|
102
|
+
.qm-field-count {
|
|
103
|
+
margin-inline-start: auto;
|
|
104
|
+
font-size: var(--_qm-text-label);
|
|
105
|
+
color: var(--_qm-ink-label);
|
|
106
|
+
font-variant-numeric: tabular-nums;
|
|
107
|
+
}
|
|
94
108
|
/* A label that lands focus says so. Only when it has somewhere to land: an
|
|
95
109
|
`ObjectField` property's label names a subform, not one control. */
|
|
96
110
|
.qm-field-label.targeted {
|
|
@@ -11,6 +11,10 @@ interface Props {
|
|
|
11
11
|
onActivate?: () => void;
|
|
12
12
|
/** Schema `description`, or undefined: the affordance renders only when set. */
|
|
13
13
|
description?: string;
|
|
14
|
+
/** A capped field's character count, drawn at the row's end. */
|
|
15
|
+
count?: string;
|
|
16
|
+
/** The count's own id, for the control's `aria-describedby`. */
|
|
17
|
+
countId?: string;
|
|
14
18
|
}
|
|
15
19
|
declare const FieldLabel: import("svelte").Component<Props, {}, "">;
|
|
16
20
|
type FieldLabel = ReturnType<typeof FieldLabel>;
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
placeFields,
|
|
52
52
|
printedText,
|
|
53
53
|
renderedBoolean,
|
|
54
|
-
shortCell
|
|
54
|
+
shortCell,
|
|
55
|
+
textCap
|
|
55
56
|
} from './structure.js';
|
|
56
57
|
import { splitDeep, unrouted, type DeepDiagnostic } from './diagnostics.js';
|
|
57
58
|
import type { LandingBox } from './leaves.js';
|
|
@@ -136,6 +137,8 @@
|
|
|
136
137
|
}: Props = $props();
|
|
137
138
|
|
|
138
139
|
const t = wording();
|
|
140
|
+
/** A capped prose cell's character count, by property. */
|
|
141
|
+
const counts: Record<string, number> = $state({});
|
|
139
142
|
const entries = $derived(Object.entries(properties ?? {}));
|
|
140
143
|
const obj = $derived((value ?? {}) as Record<string, unknown>);
|
|
141
144
|
const has = (key: string): boolean => Object.hasOwn(properties ?? {}, key);
|
|
@@ -298,6 +301,8 @@
|
|
|
298
301
|
(`packable`). A block prose cell spans it too, and keeps its label row, as
|
|
299
302
|
does a placed cell `placeFields` gives the whole row. -->
|
|
300
303
|
{@const block = kind === 'prose' && !shortCell(sub)}
|
|
304
|
+
{@const cap = kind === 'prose' ? textCap(sub) : undefined}
|
|
305
|
+
{@const countId = cap && ids ? `${ids.control}-count` : undefined}
|
|
301
306
|
<div
|
|
302
307
|
class={[
|
|
303
308
|
'qm-object-prop',
|
|
@@ -316,6 +321,10 @@
|
|
|
316
321
|
descriptionId={describes}
|
|
317
322
|
onActivate={labelable ? undefined : () => focusProp(key)}
|
|
318
323
|
description={sub.description}
|
|
324
|
+
count={cap && counts[key] != null
|
|
325
|
+
? t.strings.textCount(counts[key], cap.max)
|
|
326
|
+
: undefined}
|
|
327
|
+
{countId}
|
|
319
328
|
/>
|
|
320
329
|
{/if}
|
|
321
330
|
{#if kind === 'enum'}
|
|
@@ -385,7 +394,8 @@
|
|
|
385
394
|
{block}
|
|
386
395
|
label={named}
|
|
387
396
|
labelledBy={ids?.label}
|
|
388
|
-
describedBy={describes}
|
|
397
|
+
describedBy={countId ? [describes, countId].filter(Boolean).join(' ') : describes}
|
|
398
|
+
cap={cap && { ...cap, onCount: (n) => (counts[key] = n) }}
|
|
389
399
|
onChange={(rt) =>
|
|
390
400
|
commitProp(key, optionalCell(sub) && isEmptyContent(rt) ? undefined : rt)}
|
|
391
401
|
onKey={onCellKey ? (e) => onCellKey(e, key) : undefined}
|
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
-->
|
|
7
7
|
<script lang="ts">
|
|
8
8
|
import { untrack } from 'svelte';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createField,
|
|
11
|
+
type FieldController,
|
|
12
|
+
type SlashState,
|
|
13
|
+
type TextCap
|
|
14
|
+
} from '../core/codec/index.js';
|
|
10
15
|
import type { LeafRegistry } from './leaves.js';
|
|
11
16
|
import SlashMenu from './SlashMenu.svelte';
|
|
12
17
|
import { wording } from './strings.js';
|
|
@@ -48,6 +53,8 @@
|
|
|
48
53
|
placeholderUntilEdit?: boolean;
|
|
49
54
|
/** An emptied leaf removes the field (`createField`). */
|
|
50
55
|
emptyUnsets?: boolean;
|
|
56
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
57
|
+
cap?: TextCap;
|
|
51
58
|
/** Registry identity, stamped on the DOM node so a remount is visible as one. */
|
|
52
59
|
leafKey: string;
|
|
53
60
|
onFocus?: (addr: Addr) => void;
|
|
@@ -77,6 +84,7 @@
|
|
|
77
84
|
placeholder,
|
|
78
85
|
placeholderUntilEdit,
|
|
79
86
|
emptyUnsets,
|
|
87
|
+
cap,
|
|
80
88
|
leafKey,
|
|
81
89
|
onFocus,
|
|
82
90
|
onCaretMove,
|
|
@@ -127,6 +135,7 @@
|
|
|
127
135
|
placeholder,
|
|
128
136
|
placeholderUntilEdit,
|
|
129
137
|
emptyUnsets,
|
|
138
|
+
cap,
|
|
130
139
|
tableStrings: () => t.strings,
|
|
131
140
|
slashStrings: () => t.strings,
|
|
132
141
|
onSlash: (next) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TextCap } from '../core/codec/index.js';
|
|
1
2
|
import type { LeafRegistry } from './leaves.js';
|
|
2
3
|
import type { Document, Quill, Addr, Content, Diagnostic } from '@quillmark/wasm';
|
|
3
4
|
import type { EditorErrorHandler } from '../core/errors.js';
|
|
@@ -35,6 +36,8 @@ interface Props {
|
|
|
35
36
|
placeholderUntilEdit?: boolean;
|
|
36
37
|
/** An emptied leaf removes the field (`createField`). */
|
|
37
38
|
emptyUnsets?: boolean;
|
|
39
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
40
|
+
cap?: TextCap;
|
|
38
41
|
/** Registry identity, stamped on the DOM node so a remount is visible as one. */
|
|
39
42
|
leafKey: string;
|
|
40
43
|
onFocus?: (addr: Addr) => void;
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
pmToUsv,
|
|
61
61
|
usvToPM,
|
|
62
62
|
valueEqual,
|
|
63
|
-
type LeafViews
|
|
63
|
+
type LeafViews,
|
|
64
|
+
type TextCap
|
|
64
65
|
} from '../core/codec/index.js';
|
|
65
66
|
import { valueLeafSeat } from './leaves.js';
|
|
66
67
|
import { wording } from './strings.js';
|
|
@@ -99,6 +100,8 @@
|
|
|
99
100
|
/** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
|
|
100
101
|
* unset. Read per decoration pass. */
|
|
101
102
|
placeholder?: string;
|
|
103
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
104
|
+
cap?: TextCap;
|
|
102
105
|
onChange: (rt: Content) => void;
|
|
103
106
|
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
104
107
|
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
|
@@ -115,6 +118,7 @@
|
|
|
115
118
|
labelledBy,
|
|
116
119
|
describedBy,
|
|
117
120
|
placeholder,
|
|
121
|
+
cap,
|
|
118
122
|
onChange,
|
|
119
123
|
onKey
|
|
120
124
|
}: Props = $props();
|
|
@@ -190,7 +194,8 @@
|
|
|
190
194
|
? []
|
|
191
195
|
: proseLeafPlugins(schema, {
|
|
192
196
|
inline,
|
|
193
|
-
placeholder: () => placeholder
|
|
197
|
+
placeholder: () => placeholder,
|
|
198
|
+
cap
|
|
194
199
|
})
|
|
195
200
|
});
|
|
196
201
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TextCap } from '../core/codec/index.js';
|
|
1
2
|
import type { Content } from '@quillmark/wasm';
|
|
2
3
|
interface Props {
|
|
3
4
|
/** This leaf's content (the parent's boundary read), `undefined` where nothing is
|
|
@@ -31,6 +32,8 @@ interface Props {
|
|
|
31
32
|
/** The empty leaf's ghost, a subform cell's: the `none` an optional cell prints
|
|
32
33
|
* unset. Read per decoration pass. */
|
|
33
34
|
placeholder?: string;
|
|
35
|
+
/** The declared `max:`, a character count no edit takes the leaf past. */
|
|
36
|
+
cap?: TextCap;
|
|
34
37
|
onChange: (rt: Content) => void;
|
|
35
38
|
/** Raw keydown, for a container whose own keys run through this leaf: the
|
|
36
39
|
* array repeater's Enter/Backspace (`ArrayField`). Fires before the view's own
|
package/dist/visual/strings.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface VisualStrings extends TableChromeStrings, SlashStrings {
|
|
|
35
35
|
* spells the separator.
|
|
36
36
|
*/
|
|
37
37
|
arrayCount: (count: number, max: number) => string;
|
|
38
|
+
/**
|
|
39
|
+
* The count beside the label of a `text` or `richtext` field declaring `max:`, the
|
|
40
|
+
* carrier of a keystroke the cap refuses: `12 / 40`. Parametric, so a translator
|
|
41
|
+
* spells the separator.
|
|
42
|
+
*/
|
|
43
|
+
textCount: (count: number, max: number) => string;
|
|
38
44
|
/**
|
|
39
45
|
* A collapsed array element with nothing to summarize itself by: the label and the
|
|
40
46
|
* 1-based index, which is what its accessible name already says. Parametric, so
|
package/dist/visual/strings.js
CHANGED
|
@@ -41,6 +41,7 @@ export const DEFAULT_VISUAL_STRINGS = {
|
|
|
41
41
|
arrayDrag: 'Drag to move',
|
|
42
42
|
arrayRowAction: (action, row) => `${action} ${row}`,
|
|
43
43
|
arrayCount: (count, max) => `${count} / ${max}`,
|
|
44
|
+
textCount: (count, max) => `${count} / ${max}`,
|
|
44
45
|
elementUntitled: (label, index) => `${label} ${index}`,
|
|
45
46
|
enumUnsetTag: 'Default',
|
|
46
47
|
enumUnsetTagNone: 'Unset',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Content, type PathStep, type PayloadItem, type QuillCardSchema, type QuillFieldSchema, type QuillFieldType, type ResolvedField, type Resolved } from '@quillmark/wasm';
|
|
2
|
+
import type { TextCap } from '../core/codec/index.js';
|
|
2
3
|
/** The control a field type maps to (VISUAL_EDITOR §"Structure mirrors the schema"). */
|
|
3
4
|
export type ControlKind = 'prose' | 'text' | 'enum' | 'variant' | 'number' | 'boolean' | 'date' | 'array' | 'object' | 'matrix';
|
|
4
5
|
/** The container controls: the ones a nested address walks into and a deep
|
|
@@ -161,6 +162,8 @@ export declare function baseType(f: QuillFieldSchema): QuillFieldType;
|
|
|
161
162
|
export declare function oneLine(f: QuillFieldSchema): boolean;
|
|
162
163
|
/** Whether a content field's codec is the literal one, its value text with no marks: `text`. */
|
|
163
164
|
export declare function literalText(f: QuillFieldSchema): boolean;
|
|
165
|
+
/** The character cap a content field's `max:` declares, `undefined` on any other. */
|
|
166
|
+
export declare function textCap(f: QuillFieldSchema | undefined): TextCap | undefined;
|
|
164
167
|
/**
|
|
165
168
|
* A scalar `default:` as the text an unset control holds, where it prints: as
|
|
166
169
|
* declared, `undefined` for none, a blank or a non-scalar. A default of spaces prints
|
|
@@ -175,9 +178,10 @@ export declare function printedText(v: unknown): string | undefined;
|
|
|
175
178
|
* a blank or a non-scalar.
|
|
176
179
|
*/
|
|
177
180
|
export declare function declaredContent(v: unknown, markdown: boolean): Content | undefined;
|
|
178
|
-
/** A field's `{#each}` key: its name and what a mounted leaf reads once, its type
|
|
179
|
-
* {@link oneLine} and its elements'. A retype or a quill swap that changes
|
|
180
|
-
* under a name remounts the field, where a kept leaf would commit on the old
|
|
181
|
+
/** A field's `{#each}` key: its name and what a mounted leaf reads once, its type,
|
|
182
|
+
* {@link oneLine} and `max:`, and its elements'. A retype or a quill swap that changes
|
|
183
|
+
* any of them under a name remounts the field, where a kept leaf would commit on the old
|
|
184
|
+
* schema. */
|
|
181
185
|
export declare function mountKey(name: string, f: QuillFieldSchema): string;
|
|
182
186
|
/** Map a field schema to its control. An array's element control is this over `items`;
|
|
183
187
|
* a type this build does not know takes the text input. */
|
package/dist/visual/structure.js
CHANGED
|
@@ -93,6 +93,12 @@ export function oneLine(f) {
|
|
|
93
93
|
export function literalText(f) {
|
|
94
94
|
return baseType(f) === 'text';
|
|
95
95
|
}
|
|
96
|
+
/** The character cap a content field's `max:` declares, `undefined` on any other. */
|
|
97
|
+
export function textCap(f) {
|
|
98
|
+
if (f?.max == null || controlKind(f) !== 'prose')
|
|
99
|
+
return undefined;
|
|
100
|
+
return { max: f.max, trimEnd: literalText(f) && oneLine(f) };
|
|
101
|
+
}
|
|
96
102
|
/**
|
|
97
103
|
* A scalar `default:` as the text an unset control holds, where it prints: as
|
|
98
104
|
* declared, `undefined` for none, a blank or a non-scalar. A default of spaces prints
|
|
@@ -126,12 +132,21 @@ export function declaredContent(v, markdown) {
|
|
|
126
132
|
islands: []
|
|
127
133
|
};
|
|
128
134
|
}
|
|
129
|
-
/** A field's `{#each}` key: its name and what a mounted leaf reads once, its type
|
|
130
|
-
* {@link oneLine} and its elements'. A retype or a quill swap that changes
|
|
131
|
-
* under a name remounts the field, where a kept leaf would commit on the old
|
|
135
|
+
/** A field's `{#each}` key: its name and what a mounted leaf reads once, its type,
|
|
136
|
+
* {@link oneLine} and `max:`, and its elements'. A retype or a quill swap that changes
|
|
137
|
+
* any of them under a name remounts the field, where a kept leaf would commit on the old
|
|
138
|
+
* schema. */
|
|
132
139
|
export function mountKey(name, f) {
|
|
133
140
|
const items = f.items;
|
|
134
|
-
return
|
|
141
|
+
return [
|
|
142
|
+
name,
|
|
143
|
+
f.type,
|
|
144
|
+
oneLine(f),
|
|
145
|
+
f.max ?? '',
|
|
146
|
+
items?.type ?? '',
|
|
147
|
+
items ? oneLine(items) : '',
|
|
148
|
+
items?.max ?? ''
|
|
149
|
+
].join('\u0000');
|
|
135
150
|
}
|
|
136
151
|
/** Map a field schema to its control. An array's element control is this over `items`;
|
|
137
152
|
* a type this build does not know takes the text input. */
|