@quillmark/svelte 0.12.0 → 0.13.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.
Files changed (42) hide show
  1. package/dist/core/codec/decode.d.ts +17 -0
  2. package/dist/core/codec/decode.js +57 -4
  3. package/dist/core/codec/field.d.ts +64 -15
  4. package/dist/core/codec/field.js +201 -65
  5. package/dist/core/codec/index.d.ts +3 -3
  6. package/dist/core/codec/index.js +3 -3
  7. package/dist/core/codec/keymap.d.ts +5 -4
  8. package/dist/core/codec/keymap.js +5 -4
  9. package/dist/core/codec/prose.css +42 -0
  10. package/dist/core/codec/schema.d.ts +24 -5
  11. package/dist/core/codec/schema.js +40 -9
  12. package/dist/core/errors.d.ts +3 -2
  13. package/dist/core/theme.css +9 -6
  14. package/dist/preview/paint.js +21 -17
  15. package/dist/visual/ArrayField.svelte +66 -22
  16. package/dist/visual/ArrayField.svelte.d.ts +2 -0
  17. package/dist/visual/BooleanField.svelte +28 -6
  18. package/dist/visual/BooleanField.svelte.d.ts +2 -0
  19. package/dist/visual/Card.svelte +3 -3
  20. package/dist/visual/DateField.svelte +66 -31
  21. package/dist/visual/EnumField.svelte +27 -9
  22. package/dist/visual/EnumField.svelte.d.ts +5 -0
  23. package/dist/visual/Field.svelte +45 -10
  24. package/dist/visual/FormatPopover.svelte +4 -3
  25. package/dist/visual/MatrixField.svelte +3 -2
  26. package/dist/visual/NumberField.svelte +31 -14
  27. package/dist/visual/NumberField.svelte.d.ts +5 -1
  28. package/dist/visual/ObjectField.svelte +62 -15
  29. package/dist/visual/ProseField.svelte +53 -23
  30. package/dist/visual/ProseField.svelte.d.ts +15 -3
  31. package/dist/visual/ProseValue.svelte +102 -46
  32. package/dist/visual/ProseValue.svelte.d.ts +17 -5
  33. package/dist/visual/TextField.svelte +35 -18
  34. package/dist/visual/TextField.svelte.d.ts +7 -0
  35. package/dist/visual/VariantField.svelte +1 -0
  36. package/dist/visual/VisualEditorInner.svelte +24 -24
  37. package/dist/visual/controls.css +16 -5
  38. package/dist/visual/strings.d.ts +8 -1
  39. package/dist/visual/strings.js +2 -0
  40. package/dist/visual/structure.d.ts +77 -43
  41. package/dist/visual/structure.js +132 -57
  42. package/package.json +2 -2
@@ -17,3 +17,20 @@ export declare function usvLength(s: string): number;
17
17
  export declare function renderContent(rt: Content, schema: Schema): Node;
18
18
  /** Decode a `Content` to a PM document under `schema`. */
19
19
  export declare function decode(rt: Content, schema: Schema): PMNode;
20
+ /** Whether `rt` decodes under an inline schema with nothing but whitespace dropped:
21
+ * upstream's `inline` rule, or one plain line and the empty one a trailing `\n`
22
+ * opens. That newline is the one a YAML `|` scalar keeps in a `plaintext` value,
23
+ * which upstream refuses (`trailingNewline`) and the inline decode drops. Marks are
24
+ * the schema's own business (`plaintextSchema` declares none). */
25
+ export declare function fitsInline(rt: Content): boolean;
26
+ /** Whether `rt` decodes under `plainSchema` losing nothing but its marks, which the
27
+ * next commit heals (CODEC §Inline mode): upstream's `isPlain`, asked of the content
28
+ * with them set aside. */
29
+ export declare function fitsPlain(rt: Content): boolean;
30
+ /** Whether `rt` decodes under the schema a leaf so declared mounts, losing nothing its
31
+ * first commit would store: {@link fitsInline} narrowed, {@link fitsPlain} plain, and
32
+ * anything on the block schema. What a leaf held over the rest reads. */
33
+ export declare function fitsLeaf(rt: Content, leaf: {
34
+ inline: boolean;
35
+ plaintext: boolean;
36
+ }): boolean;
@@ -10,7 +10,8 @@
10
10
  import { DOMSerializer } from 'prosemirror-model';
11
11
  import { ISLAND_SLOT, isBlockIsland } from './islands.js';
12
12
  import { descriptorOf, markKey, pmMarkFromContent } from './marks.js';
13
- import { hasMarks, isInlineSchema, takesLineBreak } from './schema.js';
13
+ import { core } from '../lifecycle.js';
14
+ import { hasMarks, isBlockSchema, isInlineSchema, takesLineBreak } from './schema.js';
14
15
  /** Code points of `s` (USV units): the iteration granularity the content speaks. */
15
16
  export function codePoints(s) {
16
17
  return Array.from(s);
@@ -54,6 +55,9 @@ export function decode(rt, schema) {
54
55
  if (isInlineSchema(schema)) {
55
56
  return decodeInline(rt, schema, lineTexts, starts, marks, cursor);
56
57
  }
58
+ if (!isBlockSchema(schema)) {
59
+ return decodePlain(rt, schema, lineTexts, starts, marks, cursor);
60
+ }
57
61
  // Build leaves (fold `continues` runs), then nest by container prefix.
58
62
  const leaves = [];
59
63
  for (let i = 0; i < rt.lines.length; i++) {
@@ -69,13 +73,46 @@ export function decode(rt, schema) {
69
73
  const blocks = groupBlocks(schema, leaves, 0, marks, cursor);
70
74
  return schema.nodes.doc.create(null, blocks.length ? blocks : schema.nodes.paragraph.create());
71
75
  }
72
- /** Inline / plaintext decode: one paragraph, containers and islands stripped. */
76
+ /** Whether `rt` decodes under an inline schema with nothing but whitespace dropped:
77
+ * upstream's `inline` rule, or one plain line and the empty one a trailing `\n`
78
+ * opens. That newline is the one a YAML `|` scalar keeps in a `plaintext` value,
79
+ * which upstream refuses (`trailingNewline`) and the inline decode drops. Marks are
80
+ * the schema's own business (`plaintextSchema` declares none). */
81
+ export function fitsInline(rt) {
82
+ if (core().isInline(rt))
83
+ return true;
84
+ const plain = (line) => line.kind === 'para' && line.containers.length === 0;
85
+ return (rt.islands.length === 0 &&
86
+ rt.lines.length === 2 &&
87
+ rt.lines.every(plain) &&
88
+ !rt.lines[1].continues &&
89
+ rt.text.endsWith('\n'));
90
+ }
91
+ /** Whether `rt` decodes under `plainSchema` losing nothing but its marks, which the
92
+ * next commit heals (CODEC §Inline mode): upstream's `isPlain`, asked of the content
93
+ * with them set aside. */
94
+ export function fitsPlain(rt) {
95
+ return core().isPlain({ ...rt, marks: [] });
96
+ }
97
+ /** Whether `rt` decodes under the schema a leaf so declared mounts, losing nothing its
98
+ * first commit would store: {@link fitsInline} narrowed, {@link fitsPlain} plain, and
99
+ * anything on the block schema. What a leaf held over the rest reads. */
100
+ export function fitsLeaf(rt, leaf) {
101
+ if (leaf.inline)
102
+ return fitsInline(rt);
103
+ return !leaf.plaintext || fitsPlain(rt);
104
+ }
105
+ /** Inline decode: one paragraph, containers and islands stripped. */
73
106
  function decodeInline(rt, schema, lineTexts, starts, marks, cursor) {
74
107
  // An inline field is single-line; join any stray lines with a space (no
75
- // hard_break node exists in this schema). Islands are not representable inline:
76
- // the slot char is dropped and its entry skipped.
108
+ // hard_break node exists in this schema). The empty line a trailing `\n` opens is
109
+ // dropped rather than joined, so a first edit stores no space nobody typed. Islands
110
+ // are not representable inline: the slot char is dropped and its entry skipped.
77
111
  const inline = [];
112
+ const last = lineTexts.length - 1;
78
113
  for (let i = 0; i < lineTexts.length; i++) {
114
+ if (i > 0 && i === last && lineTexts[i] === '')
115
+ break;
79
116
  if (i > 0)
80
117
  inline.push(schema.text(' '));
81
118
  inline.push(...buildInline(schema, lineTexts[i], starts[i], marks, cursor, true));
@@ -83,6 +120,22 @@ function decodeInline(rt, schema, lineTexts, starts, marks, cursor) {
83
120
  const para = schema.nodes.paragraph.create(null, inline);
84
121
  return schema.nodes.doc.create(null, para);
85
122
  }
123
+ /** Plain decode: a `continues` line is a `hard_break` in the paragraph above and every
124
+ * other line opens one. A line's kind and containers and every island have no node
125
+ * here and are dropped, so a content {@link fitsPlain} refuses opens flattened. */
126
+ function decodePlain(rt, schema, lineTexts, starts, marks, cursor) {
127
+ const paras = [];
128
+ for (let i = 0; i < rt.lines.length; i++) {
129
+ const inline = buildInline(schema, lineTexts[i] ?? '', starts[i], marks, cursor, true);
130
+ const open = paras[paras.length - 1];
131
+ if (rt.lines[i].continues && open)
132
+ open.push(schema.nodes.hard_break.create(), ...inline);
133
+ else
134
+ paras.push(inline);
135
+ }
136
+ const blocks = paras.map((inline) => schema.nodes.paragraph.create(null, inline));
137
+ return schema.nodes.doc.create(null, blocks.length ? blocks : schema.nodes.paragraph.create());
138
+ }
86
139
  /** Nest a run of leaves that share a `depth`-length container prefix into blocks. */
87
140
  function groupBlocks(schema, leaves, depth, marks, cursor) {
88
141
  const out = [];
@@ -1,4 +1,4 @@
1
- import type { Schema } from 'prosemirror-model';
1
+ import { type Schema } from 'prosemirror-model';
2
2
  import { Plugin } from 'prosemirror-state';
3
3
  import { EditorView } from 'prosemirror-view';
4
4
  import type { Document, Content, Addr, Quill } from '@quillmark/wasm';
@@ -14,11 +14,24 @@ export interface CreateFieldOpts {
14
14
  quill: Quill;
15
15
  addr: Addr;
16
16
  container: HTMLElement;
17
- /** Constrained single-textblock schema (a `richtext(inline)` field). */
17
+ /** Constrained single-textblock schema (a field declaring `inline`). */
18
18
  inline?: boolean;
19
- /** The mark-free inline schema (a `plaintext` field): literal text, no formatting
20
- * and no anchors. Implies `inline`. */
19
+ /** A mark-free schema (a `plaintext` field): literal text, no formatting and no
20
+ * anchors, in paragraphs and hard breaks or, with `inline`, one textblock. */
21
21
  plaintext?: boolean;
22
+ /** The id of the note the caller draws inside `container` for a held leaf →
23
+ * `aria-describedby` while it holds. */
24
+ heldNoteId?: string;
25
+ /**
26
+ * Fired when the leaf holds or releases, and at mount when it mounts held. A leaf
27
+ * declaring `inline`, or a `plaintext` one, is held over a value its schema cannot
28
+ * hold (`fitsLeaf`, upstream's `isInline` and `isPlain`): the decode would join lines
29
+ * and drop containers and islands for the first commit to store. A held leaf draws
30
+ * its content on the block schema, read-only, and commits nothing. Judged of the value
31
+ * each mount and re-hydrate reads, so {@link FieldController.applyExternal} is what
32
+ * re-evaluates it.
33
+ */
34
+ onHold?(held: boolean): void;
22
35
  /** Suppress the markdown-shorthand input rules. */
23
36
  noInputRules?: boolean;
24
37
  /** The island chrome's wording, read live (per render) so a consumer swapping
@@ -46,11 +59,28 @@ export interface CreateFieldOpts {
46
59
  labelledBy?: string;
47
60
  /** The parked `description` → `aria-describedby`; announced after the name. */
48
61
  describedBy?: string;
49
- /** Ghost text shown on the empty leaf: the resolved `default:` a field ghosts,
50
- * or a body's invitation. The initial value; {@link
51
- * FieldController.setPlaceholder} moves it after mount. Empty/absent
52
- * shows no ghost. */
62
+ /**
63
+ * The resolved `default:` where it prints: the content an unset field's leaf holds,
64
+ * marked `data-default` on the editable for the default rung. Nothing is written
65
+ * until an edit, which commits the whole leaf as authored: the default taken, with
66
+ * the edit in it. The initial value; {@link FieldController.setFallback} moves it
67
+ * after mount.
68
+ */
69
+ fallback?: Content;
70
+ /** Ghost text shown on the empty leaf: the `none` an unset optional field prints, or
71
+ * a body's invitation. The initial value; {@link FieldController.setPlaceholder}
72
+ * moves it after mount. Empty/absent shows no ghost. */
53
73
  placeholder?: string;
74
+ /** The placeholder is what the unset field prints, so it goes at the leaf's first
75
+ * edit: the edit answers the field, and emptied the leaf holds an empty answer, which
76
+ * prints empty. Absent, the placeholder is an invitation (a body's) and returns
77
+ * whenever the leaf is empty. */
78
+ placeholderUntilEdit?: boolean;
79
+ /** Ghost text shown on the empty leaf until its first edit: an unset field's
80
+ * `example:`, at rest where there is no placeholder and in the placeholder's stead
81
+ * while the leaf holds the focus. The initial value; {@link FieldController.setExample}
82
+ * moves it after mount. */
83
+ example?: string;
54
84
  onFocus?(addr: Addr): void;
55
85
  /** Fired with the new USV caret after an edit or a selection move. */
56
86
  onCaretMove?(addr: Addr, pos: number): void;
@@ -60,7 +90,8 @@ export interface CreateFieldOpts {
60
90
  * move, so a host driving a recompile off it recompiles on every arrow key.
61
91
  *
62
92
  * Fires for every commit regardless of which branch it took (`applyChange`, the
63
- * first-edit `overwrite`, the fallback `overwrite`) and for an anchor mutation,
93
+ * first-edit `overwrite`, the fallback `overwrite`, a `plaintext` leaf's typed
94
+ * write) and for an anchor mutation,
64
95
  * which changes no text. A commit that failed outright (`commit-lost`) does not
65
96
  * fire it: nothing landed.
66
97
  */
@@ -75,7 +106,8 @@ export interface FieldController {
75
106
  readonly el: HTMLElement;
76
107
  /** Place the caret at USV `pos` (preview onPick → usvToPM → here). */
77
108
  setCaret(pos: number): void;
78
- /** External content change → re-hydrate this leaf (gated by reconcile). */
109
+ /** External content change → re-hydrate this leaf (gated by reconcile), and
110
+ * re-evaluate its hold against the value now stored. */
79
111
  applyExternal(): void;
80
112
  /**
81
113
  * Move the empty-leaf ghost after mount: a card retyped to another kind takes
@@ -86,6 +118,12 @@ export interface FieldController {
86
118
  * a transaction would fire `onCaretMove` at a moment the caret did not move.
87
119
  */
88
120
  setPlaceholder(text: string | undefined): void;
121
+ /** Move the example ghost after mount, the way {@link setPlaceholder} moves the
122
+ * other one. */
123
+ setExample(text: string | undefined): void;
124
+ /** Move the default an unset leaf holds after mount: a retype or a quill swap
125
+ * re-seats an unset leaf on the new kind's default, and leaves a written one be. */
126
+ setFallback(content: Content | undefined): void;
89
127
  focus(): void;
90
128
  /** The current stored content for this addr (for tests / reconcile). */
91
129
  getContent(): Content;
@@ -146,10 +184,17 @@ export declare function proseAttributes(opts: {
146
184
  labelledBy?: string;
147
185
  describedBy?: string;
148
186
  }): Record<string, string> | undefined;
187
+ /** A held leaf's attributes, described by its note as well. A non-editable view is no
188
+ * textbox to assistive tech or to Tab, so a held leaf states both itself. */
189
+ export declare function heldAttributes(opts: {
190
+ label?: string;
191
+ labelledBy?: string;
192
+ describedBy?: string;
193
+ }, noteId: string | undefined): Record<string, string>;
149
194
  export declare function createField(opts: CreateFieldOpts): FieldController;
150
195
  /**
151
196
  * The prose-leaf plugin stack (VISUAL_EDITOR §Surface): shared by
152
- * {@link createField} and the by-value inline editor (`ProseValue`), so the two
197
+ * {@link createField} and the by-value editor (`ProseValue`), so the two
153
198
  * never fork the keymap/plugin ordering. History first, then any leaf-specific
154
199
  * plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
155
200
  * a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
@@ -158,10 +203,10 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
158
203
  * shapes: the gap cursor, {@link pastAtomPlugin}, and the island paste pass
159
204
  * (`islands.ts`).
160
205
  *
161
- * Every mark-shaped plugin reads the schema rather than a flag: over
162
- * `plaintextSchema` the shorthand rules build nothing (each is guarded on its mark
163
- * type) and the toggles bind no key, so a `**bold**` keeps the delimiters its author
164
- * typed without a second rule saying so.
206
+ * Every mark-shaped plugin reads the schema rather than a flag: over either plaintext
207
+ * schema the shorthand rules build nothing (each is guarded on its mark type) and the
208
+ * toggles bind no key, so a `**bold**` keeps the delimiters its author typed without
209
+ * a second rule saying so.
165
210
  */
166
211
  export declare function proseLeafPlugins(schema: Schema, opts: {
167
212
  inline: boolean;
@@ -173,5 +218,9 @@ export declare function proseLeafPlugins(schema: Schema, opts: {
173
218
  * ({@link FieldController.setPlaceholder}), and a re-hydration rebuilds this
174
219
  * stack, so the plugin must ask rather than hold. */
175
220
  placeholder?: () => string | undefined;
221
+ /** {@link CreateFieldOpts.placeholderUntilEdit}. */
222
+ placeholderUntilEdit?: boolean;
223
+ /** The focused leaf's ghost, read live for the same reason. */
224
+ example?: () => string | undefined;
176
225
  afterHistory?: Plugin[];
177
226
  }): Plugin[];