@quillmark/svelte 0.4.0 → 0.4.1
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/decode.d.ts +2 -7
- package/dist/core/codec/decode.js +9 -5
- package/dist/core/codec/field.d.ts +6 -7
- package/dist/core/codec/field.js +21 -21
- package/dist/core/codec/index.d.ts +1 -1
- package/dist/core/codec/index.js +3 -2
- package/dist/core/codec/inputrules.js +17 -18
- package/dist/core/codec/schema.d.ts +15 -6
- package/dist/core/codec/schema.js +30 -17
- package/dist/visual/Card.svelte +4 -0
- package/dist/visual/FieldHint.svelte +14 -0
- package/dist/visual/FormatPopover.svelte +12 -1
- package/dist/visual/ProseArrayElement.svelte +1 -1
- package/package.json +1 -1
|
@@ -4,10 +4,6 @@ import type { Content } from '@quillmark/wasm';
|
|
|
4
4
|
export declare function codePoints(s: string): string[];
|
|
5
5
|
/** USV length of `s` (code points, not UTF-16 units). */
|
|
6
6
|
export declare function usvLength(s: string): number;
|
|
7
|
-
interface DecodeOpts {
|
|
8
|
-
/** Strip all marks (a `plaintext` field). */
|
|
9
|
-
plaintext?: boolean;
|
|
10
|
-
}
|
|
11
7
|
/**
|
|
12
8
|
* A `Content` as read-only DOM: decode under `schema`, then the nodes' own `toDOM`.
|
|
13
9
|
* The rendering half of the codec's job with no editing attached (no PM view, no
|
|
@@ -18,7 +14,6 @@ interface DecodeOpts {
|
|
|
18
14
|
* `DOMSerializer.fromSchema` memoizes on the schema, so repeat calls build no
|
|
19
15
|
* serializer.
|
|
20
16
|
*/
|
|
21
|
-
export declare function renderContent(rt: Content, schema: Schema
|
|
17
|
+
export declare function renderContent(rt: Content, schema: Schema): Node;
|
|
22
18
|
/** Decode a `Content` to a PM document under `schema`. */
|
|
23
|
-
export declare function decode(rt: Content, schema: Schema
|
|
24
|
-
export {};
|
|
19
|
+
export declare function decode(rt: Content, schema: Schema): PMNode;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { DOMSerializer } from 'prosemirror-model';
|
|
11
11
|
import { ISLAND_SLOT } from './islands.js';
|
|
12
12
|
import { descriptorOf, markKey, pmMarkFromContent } from './marks.js';
|
|
13
|
-
import { isInlineSchema } from './schema.js';
|
|
13
|
+
import { hasMarks, isInlineSchema } from './schema.js';
|
|
14
14
|
import { isAnchorMark, isCodeLine, isHeadingLine, isListItemContainer } from '@quillmark/wasm';
|
|
15
15
|
/** Code points of `s` (USV units): the iteration granularity the content speaks. */
|
|
16
16
|
export function codePoints(s) {
|
|
@@ -33,11 +33,11 @@ export function usvLength(s) {
|
|
|
33
33
|
* `DOMSerializer.fromSchema` memoizes on the schema, so repeat calls build no
|
|
34
34
|
* serializer.
|
|
35
35
|
*/
|
|
36
|
-
export function renderContent(rt, schema
|
|
37
|
-
return DOMSerializer.fromSchema(schema).serializeFragment(decode(rt, schema
|
|
36
|
+
export function renderContent(rt, schema) {
|
|
37
|
+
return DOMSerializer.fromSchema(schema).serializeFragment(decode(rt, schema).content);
|
|
38
38
|
}
|
|
39
39
|
/** Decode a `Content` to a PM document under `schema`. */
|
|
40
|
-
export function decode(rt, schema
|
|
40
|
+
export function decode(rt, schema) {
|
|
41
41
|
const lineTexts = rt.text.split('\n');
|
|
42
42
|
// Per-line USV start: line i begins after all earlier lines and their `\n`s.
|
|
43
43
|
const starts = [];
|
|
@@ -46,7 +46,11 @@ export function decode(rt, schema, opts = {}) {
|
|
|
46
46
|
starts.push(acc);
|
|
47
47
|
acc += usvLength(lineTexts[i]) + 1; // +1 for the `\n` boundary
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
// A mark-free schema (`plaintextSchema`) has nothing to apply them with, and a
|
|
50
|
+
// plaintext field that acquired one anyway — an older build's popover, a hand-
|
|
51
|
+
// written document — is exactly the content that must open and then heal on the
|
|
52
|
+
// next commit, not throw here.
|
|
53
|
+
const marks = hasMarks(schema) ? rt.marks.filter((m) => !isAnchorMark(m)) : [];
|
|
50
54
|
const cursor = { i: 0, rt };
|
|
51
55
|
if (isInlineSchema(schema)) {
|
|
52
56
|
return decodeInline(rt, schema, lineTexts, starts, marks, cursor);
|
|
@@ -16,7 +16,8 @@ export interface CreateFieldOpts {
|
|
|
16
16
|
container: HTMLElement;
|
|
17
17
|
/** Constrained single-textblock schema (a `richtext(inline)` field). */
|
|
18
18
|
inline?: boolean;
|
|
19
|
-
/**
|
|
19
|
+
/** The mark-free inline schema (a `plaintext` field): literal text, no formatting
|
|
20
|
+
* and no anchors. Implies `inline`. */
|
|
20
21
|
plaintext?: boolean;
|
|
21
22
|
/** Suppress the markdown-shorthand input rules. */
|
|
22
23
|
noInputRules?: boolean;
|
|
@@ -145,15 +146,13 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
145
146
|
* last the two that answer to a caret or a selection beside a block: the gap cursor
|
|
146
147
|
* and {@link pastAtomPlugin}.
|
|
147
148
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* ones it would add: skip all.
|
|
149
|
+
* Every mark-shaped plugin reads the schema rather than a flag: over
|
|
150
|
+
* `plaintextSchema` the shorthand rules build nothing (each is guarded on its mark
|
|
151
|
+
* type) and the toggles bind no key, so a `**bold**` keeps the delimiters its author
|
|
152
|
+
* typed without a second rule saying so.
|
|
153
153
|
*/
|
|
154
154
|
export declare function proseLeafPlugins(schema: Schema, opts: {
|
|
155
155
|
inline: boolean;
|
|
156
|
-
plaintext: boolean;
|
|
157
156
|
/** The slash menu's report channel; absent mounts no menu and leaves
|
|
158
157
|
* Enter/Escape/the arrows to the links below (`keymap.ts`). */
|
|
159
158
|
slash?: (state: SlashState | undefined) => void;
|
package/dist/core/codec/field.js
CHANGED
|
@@ -27,7 +27,7 @@ import { anchorsFromContent } from './marks.js';
|
|
|
27
27
|
import { createReconciler } from './reconcile.js';
|
|
28
28
|
import { inputRulesPlugin } from './inputrules.js';
|
|
29
29
|
import { bodyKeymap } from './keymap.js';
|
|
30
|
-
import { blockSchema, inlineSchema } from './schema.js';
|
|
30
|
+
import { blockSchema, inlineSchema, plaintextSchema } from './schema.js';
|
|
31
31
|
import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
|
|
32
32
|
import { focusSlashItem, runSlashItem, slashPlugin } from './slash.js';
|
|
33
33
|
const anchorKey = new PluginKey('quill-anchors');
|
|
@@ -108,7 +108,10 @@ export function createField(opts) {
|
|
|
108
108
|
const { doc, addr, container } = opts;
|
|
109
109
|
const inline = !!opts.inline || !!opts.plaintext;
|
|
110
110
|
const plaintext = !!opts.plaintext;
|
|
111
|
-
|
|
111
|
+
// The declared type picks the schema, and the schema is the whole of what
|
|
112
|
+
// `plaintext` suppresses: no mark types to toggle, to paste in, or to mint a
|
|
113
|
+
// shorthand with (`schema.ts`).
|
|
114
|
+
const schema = plaintext ? plaintextSchema : inline ? inlineSchema : blockSchema;
|
|
112
115
|
// Bound once and held: the reader is a `{quill, doc}` pair that reads live, so
|
|
113
116
|
// every read below sees the commit before it.
|
|
114
117
|
const reader = opts.quill.reader(doc);
|
|
@@ -176,7 +179,7 @@ export function createField(opts) {
|
|
|
176
179
|
}
|
|
177
180
|
});
|
|
178
181
|
function buildState(rt) {
|
|
179
|
-
const pmDoc = decode(rt, schema
|
|
182
|
+
const pmDoc = decode(rt, schema);
|
|
180
183
|
const anchors = plaintext ? [] : anchorsFromContent(rt);
|
|
181
184
|
// Seed anchor plugin positions in PM coords via a fresh index over pmDoc.
|
|
182
185
|
const seedIndex = buildLineIndex(pmDoc);
|
|
@@ -188,7 +191,6 @@ export function createField(opts) {
|
|
|
188
191
|
doc: pmDoc,
|
|
189
192
|
plugins: proseLeafPlugins(schema, {
|
|
190
193
|
inline,
|
|
191
|
-
plaintext,
|
|
192
194
|
slash,
|
|
193
195
|
noInputRules: opts.noInputRules,
|
|
194
196
|
// Always installed, so a leaf that mounts without a ghost can still be
|
|
@@ -368,19 +370,18 @@ export function createField(opts) {
|
|
|
368
370
|
* last the two that answer to a caret or a selection beside a block: the gap cursor
|
|
369
371
|
* and {@link pastAtomPlugin}.
|
|
370
372
|
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* ones it would add: skip all.
|
|
373
|
+
* Every mark-shaped plugin reads the schema rather than a flag: over
|
|
374
|
+
* `plaintextSchema` the shorthand rules build nothing (each is guarded on its mark
|
|
375
|
+
* type) and the toggles bind no key, so a `**bold**` keeps the delimiters its author
|
|
376
|
+
* typed without a second rule saying so.
|
|
376
377
|
*/
|
|
377
378
|
export function proseLeafPlugins(schema, opts) {
|
|
378
379
|
const list = [history(), ...(opts.afterHistory ?? [])];
|
|
379
380
|
if (opts.slash)
|
|
380
381
|
list.push(slashPlugin(opts.slash));
|
|
381
|
-
if (!opts.noInputRules
|
|
382
|
+
if (!opts.noInputRules)
|
|
382
383
|
list.push(inputRulesPlugin(schema));
|
|
383
|
-
list.push(keymap(editorKeymap(schema, opts.inline,
|
|
384
|
+
list.push(keymap(editorKeymap(schema, opts.inline, !!opts.slash)));
|
|
384
385
|
list.push(keymap(baseKeymap));
|
|
385
386
|
// All three answer to something only a block leaf holds: an inline leaf is one
|
|
386
387
|
// textblock with no island, no gap to put a cursor in, and nowhere for a pasted
|
|
@@ -460,7 +461,8 @@ function placeholderPlugin(read) {
|
|
|
460
461
|
}
|
|
461
462
|
/**
|
|
462
463
|
* The field's keymap: history, mark toggles, and the body's structural keys; Enter
|
|
463
|
-
* suppressed inline.
|
|
464
|
+
* suppressed inline. The toggles are bound per mark type the schema declares, so a
|
|
465
|
+
* plaintext leaf leaves Mod-b/i/u to the browser.
|
|
464
466
|
*
|
|
465
467
|
* Tab forks on the leaf's role, not on the caret's position. An inline/plaintext
|
|
466
468
|
* leaf is a form field: Tab stays unbound, so field navigation is open for a shell
|
|
@@ -468,20 +470,18 @@ function placeholderPlugin(read) {
|
|
|
468
470
|
* is structural, a chain each nested surface prepends to (`keymap.ts`). One key
|
|
469
471
|
* never means two things within one surface.
|
|
470
472
|
*/
|
|
471
|
-
function editorKeymap(schema, inline,
|
|
473
|
+
function editorKeymap(schema, inline, slash) {
|
|
472
474
|
const map = {
|
|
473
475
|
'Mod-z': undo,
|
|
474
476
|
'Mod-y': redo,
|
|
475
477
|
'Shift-Mod-z': redo
|
|
476
478
|
};
|
|
477
|
-
if (
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
map['Mod-u'] = toggleMark(schema.marks.underline);
|
|
484
|
-
}
|
|
479
|
+
if (schema.marks.strong)
|
|
480
|
+
map['Mod-b'] = toggleMark(schema.marks.strong);
|
|
481
|
+
if (schema.marks.em)
|
|
482
|
+
map['Mod-i'] = toggleMark(schema.marks.em);
|
|
483
|
+
if (schema.marks.underline)
|
|
484
|
+
map['Mod-u'] = toggleMark(schema.marks.underline);
|
|
485
485
|
if (inline) {
|
|
486
486
|
// One textblock only: swallow Enter so no second block is attempted.
|
|
487
487
|
map['Enter'] = () => true;
|
|
@@ -9,7 +9,7 @@ export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
|
9
9
|
export type { TableChromeStrings } from './table-view.js';
|
|
10
10
|
export { DEFAULT_SLASH_STRINGS } from './slash.js';
|
|
11
11
|
export type { SlashState, SlashStrings } from './slash.js';
|
|
12
|
-
export { blockSchema, inlineSchema } from './schema.js';
|
|
12
|
+
export { blockSchema, inlineSchema, plaintextSchema, hasMarks } from './schema.js';
|
|
13
13
|
export { decode, renderContent, usvLength } from './decode.js';
|
|
14
14
|
export { pmToContent, contentEdit, lower } from './encode.js';
|
|
15
15
|
export type { ContentEdit } from './encode.js';
|
package/dist/core/codec/index.js
CHANGED
|
@@ -20,8 +20,9 @@ export { DEFAULT_TABLE_STRINGS } from './table-view.js';
|
|
|
20
20
|
// The slash menu: the codec owns the model and the picks, the chrome the words and
|
|
21
21
|
// the pixels (`visual/SlashMenu.svelte` is the only off-barrel caller).
|
|
22
22
|
export { DEFAULT_SLASH_STRINGS } from './slash.js';
|
|
23
|
-
// Schemas (the decode/encode target; the VisualEditor mounts them).
|
|
24
|
-
|
|
23
|
+
// Schemas (the decode/encode target; the VisualEditor mounts them). `hasMarks` is
|
|
24
|
+
// what a mark surface asks before offering itself over a leaf.
|
|
25
|
+
export { blockSchema, inlineSchema, plaintextSchema, hasMarks } from './schema.js';
|
|
25
26
|
// Decode / encode / positions (tests + VisualEditor).
|
|
26
27
|
//
|
|
27
28
|
// The barrel carries what has an off-barrel caller, and nothing else. A symbol
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
// block rule with no guard of its own reuses `wrappingInputRule`, and the rest mirror
|
|
6
6
|
// an upstream body around one. These produce transactions the codec already lowers:
|
|
7
7
|
// they add no new lowering surface.
|
|
8
|
+
//
|
|
9
|
+
// A rule declines only where what it would mint is unrepresentable, or is a shape
|
|
10
|
+
// another gesture owns — never on how a quill renders it. What a quill typesets is not
|
|
11
|
+
// knowable here (a quill this build has not seen is the open-sets case, CODEC §"Open
|
|
12
|
+
// sets"), and a construct that arrives through `importMarkdown` must stay authorable or
|
|
13
|
+
// a document opens carrying a shape the editor refuses to make. So one guard survives:
|
|
14
|
+
// `- ` / `1. ` at the head of an existing item, where Tab owns the nesting.
|
|
8
15
|
import { InputRule, inputRules, wrappingInputRule } from 'prosemirror-inputrules';
|
|
9
16
|
import { canJoin, findWrapping } from 'prosemirror-transform';
|
|
10
17
|
import { Selection } from 'prosemirror-state';
|
|
@@ -43,11 +50,10 @@ function markInputRule(regexp, markType, delimLen) {
|
|
|
43
50
|
* `wrappingInputRule` (whose body this mirrors), plus one normalization: a
|
|
44
51
|
* `heading` being wrapped becomes a `paragraph` first.
|
|
45
52
|
*
|
|
46
|
-
* `list_item` is `block+`, so `list_item > heading` is *representable
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* {@link markdownInputRules}.
|
|
53
|
+
* `list_item` is `block+`, so `list_item > heading` is *representable*, and `# `
|
|
54
|
+
* inside the item is the gesture that mints it ({@link markdownInputRules}). A wrap
|
|
55
|
+
* keeping the heading would be a second door onto that shape, so `- ` / `1. ` mint a
|
|
56
|
+
* paragraph item wherever they fire.
|
|
51
57
|
*
|
|
52
58
|
* `item` is the second guard, and it is about the gesture rather than the shape: the
|
|
53
59
|
* rule declines at the start of an item's own first block ({@link openingAnItem}).
|
|
@@ -114,16 +120,13 @@ export function markdownInputRules(schema) {
|
|
|
114
120
|
// Block shorthands (only where the schema has the node; the inline schema
|
|
115
121
|
// omits them, so this stays a no-op there).
|
|
116
122
|
if (schema.nodes.heading) {
|
|
117
|
-
// `textblockTypeInputRule`'s body
|
|
118
|
-
//
|
|
119
|
-
//
|
|
123
|
+
// `textblockTypeInputRule`'s body. It fires inside a list item like anywhere else:
|
|
124
|
+
// `list_item > heading` is a shape the content holds and `importMarkdown` produces
|
|
125
|
+
// from `- # title`, so a rule that declined there would refuse to author what a
|
|
126
|
+
// document can arrive carrying.
|
|
120
127
|
const heading = schema.nodes.heading;
|
|
121
128
|
rules.push(new InputRule(/^(#{1,6})\s$/, (state, match, start, end) => {
|
|
122
129
|
const $start = state.doc.resolve(start);
|
|
123
|
-
for (let d = $start.depth; d > 0; d--) {
|
|
124
|
-
if (item && $start.node(d).type === item)
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
130
|
if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), heading)) {
|
|
128
131
|
return null;
|
|
129
132
|
}
|
|
@@ -165,12 +168,6 @@ export function markdownInputRules(schema) {
|
|
|
165
168
|
if ($start.parentOffset !== 0 || $start.parent.content.size !== end - start) {
|
|
166
169
|
return null;
|
|
167
170
|
}
|
|
168
|
-
// The `# ` rule's guard, for its reason: `list_item` is `block+`, so a
|
|
169
|
-
// divider inside an item is representable and renders to nothing.
|
|
170
|
-
for (let d = $start.depth; d > 0; d--) {
|
|
171
|
-
if (item && $start.node(d).type === item)
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
171
|
if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), rule)) {
|
|
175
172
|
return null;
|
|
176
173
|
}
|
|
@@ -194,6 +191,8 @@ export function markdownInputRules(schema) {
|
|
|
194
191
|
if (schema.nodes.ordered_list && schema.nodes.paragraph) {
|
|
195
192
|
rules.push(listWrappingRule(/^(\d+)\.\s$/, schema.nodes.ordered_list, schema.nodes.paragraph, item, (match) => ({ start: +match[1] }), (match, node) => node.childCount + node.attrs.start === +match[1]));
|
|
196
193
|
}
|
|
194
|
+
// No item guard: a quote wrapping an item's own first block is a container the
|
|
195
|
+
// content holds, where a list wrapping one is an item whose only content is an item.
|
|
197
196
|
if (schema.nodes.blockquote) {
|
|
198
197
|
rules.push(wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote));
|
|
199
198
|
}
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { Schema } from 'prosemirror-model';
|
|
2
2
|
/** The full field schema: every block kind, container, mark, and island. */
|
|
3
3
|
export declare const blockSchema: Schema<string, string>;
|
|
4
|
+
/** The constrained inline schema: one paragraph, no block splitting, no
|
|
5
|
+
* containers, no islands, and the full mark set (a `richtext(inline)` field, and
|
|
6
|
+
* a table cell, which is the same content unit). */
|
|
7
|
+
export declare const inlineSchema: Schema<string, string>;
|
|
4
8
|
/**
|
|
5
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* The inline schema with no mark types at all: a `plaintext` field, whose value is
|
|
10
|
+
* literal text the boundary refuses to coerce back once it carries a mark.
|
|
11
|
+
*
|
|
12
|
+
* Declaring none is what makes that structural rather than a rule each path
|
|
13
|
+
* restates: `toggleMark` has no type to apply, the mark input rules build nothing,
|
|
14
|
+
* a paste parses its marks away, and `decode` drops any a stray content arrives
|
|
15
|
+
* carrying (CODEC §Inline mode).
|
|
10
16
|
*/
|
|
11
|
-
export declare const
|
|
17
|
+
export declare const plaintextSchema: Schema<string, never>;
|
|
18
|
+
/** Whether `schema` can carry formatting at all: false for {@link plaintextSchema}
|
|
19
|
+
* alone. What a mark command asks before it offers itself. */
|
|
20
|
+
export declare function hasMarks(schema: Schema): boolean;
|
|
12
21
|
/** True for the constrained inline schema (no block containers); decode branches on it. */
|
|
13
22
|
export declare function isInlineSchema(schema: Schema): boolean;
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
// Each of the three open sets gets an inert carrier so an unrecognized value
|
|
5
5
|
// survives a round-trip: the `unknown` mark, the paragraph's `unknown` attribute,
|
|
6
6
|
// and the `unknown_container` node. `blockSchema` is the full field; `inlineSchema`
|
|
7
|
-
// is the constrained single-textblock form for `richtext(inline)`
|
|
8
|
-
//
|
|
9
|
-
// position machinery, narrower shape. Anchors are
|
|
7
|
+
// is the constrained single-textblock form for `richtext(inline)` (one paragraph,
|
|
8
|
+
// no block split, no containers, no islands) and `plaintextSchema` is that one
|
|
9
|
+
// without marks: same decode/lower/position machinery, narrower shape. Anchors are
|
|
10
|
+
// not marks here (decorations).
|
|
10
11
|
import { Schema } from 'prosemirror-model';
|
|
11
12
|
import { islandBlockSpec, islandInlineSpec } from './islands.js';
|
|
12
|
-
// ── Marks (
|
|
13
|
+
// ── Marks (the block and inline schemas share them; plaintext declares none) ─
|
|
13
14
|
const marks = {
|
|
14
15
|
// Order matters: it fixes mark-set sort order and parse precedence. `link`
|
|
15
16
|
// last so it wraps outermost; `unknown` after it, non-exclusive so several
|
|
@@ -134,21 +135,33 @@ export const blockSchema = new Schema({
|
|
|
134
135
|
nodes: { ...blockNodes, ...inlineLeafNodes },
|
|
135
136
|
marks
|
|
136
137
|
});
|
|
138
|
+
// ── The constrained inline nodes (both inline schemas) ──────────────────────
|
|
139
|
+
// `doc: "paragraph"` (exactly one child) is what makes an Enter a no-op at the
|
|
140
|
+
// model level: there is no second block for a split to land.
|
|
141
|
+
const inlineNodes = {
|
|
142
|
+
doc: { content: 'paragraph' },
|
|
143
|
+
paragraph: { content: 'inline*', toDOM: () => ['p', 0] },
|
|
144
|
+
text: { group: 'inline' }
|
|
145
|
+
};
|
|
146
|
+
/** The constrained inline schema: one paragraph, no block splitting, no
|
|
147
|
+
* containers, no islands, and the full mark set (a `richtext(inline)` field, and
|
|
148
|
+
* a table cell, which is the same content unit). */
|
|
149
|
+
export const inlineSchema = new Schema({ nodes: inlineNodes, marks });
|
|
137
150
|
/**
|
|
138
|
-
* The
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
151
|
+
* The inline schema with no mark types at all: a `plaintext` field, whose value is
|
|
152
|
+
* literal text the boundary refuses to coerce back once it carries a mark.
|
|
153
|
+
*
|
|
154
|
+
* Declaring none is what makes that structural rather than a rule each path
|
|
155
|
+
* restates: `toggleMark` has no type to apply, the mark input rules build nothing,
|
|
156
|
+
* a paste parses its marks away, and `decode` drops any a stray content arrives
|
|
157
|
+
* carrying (CODEC §Inline mode).
|
|
143
158
|
*/
|
|
144
|
-
export const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
marks
|
|
151
|
-
});
|
|
159
|
+
export const plaintextSchema = new Schema({ nodes: inlineNodes, marks: {} });
|
|
160
|
+
/** Whether `schema` can carry formatting at all: false for {@link plaintextSchema}
|
|
161
|
+
* alone. What a mark command asks before it offers itself. */
|
|
162
|
+
export function hasMarks(schema) {
|
|
163
|
+
return Object.keys(schema.marks).length > 0;
|
|
164
|
+
}
|
|
152
165
|
/** True for the constrained inline schema (no block containers); decode branches on it. */
|
|
153
166
|
export function isInlineSchema(schema) {
|
|
154
167
|
return !schema.nodes.blockquote;
|
package/dist/visual/Card.svelte
CHANGED
|
@@ -185,9 +185,13 @@
|
|
|
185
185
|
}
|
|
186
186
|
</script>
|
|
187
187
|
|
|
188
|
+
<!-- `data-qm-card` names the island for a floating surface that has to stay inside one
|
|
189
|
+
({@link FieldHint}). A surface portals to `[data-qm-root]`, so the card is not its
|
|
190
|
+
ancestor and nothing else marks the box its guidance belongs in. -->
|
|
188
191
|
<section
|
|
189
192
|
bind:this={el}
|
|
190
193
|
class="qm-card"
|
|
194
|
+
data-qm-card
|
|
191
195
|
class:qm-main={card.isMain}
|
|
192
196
|
class:qm-unschemable={card.unschemable}
|
|
193
197
|
>
|
|
@@ -46,6 +46,13 @@
|
|
|
46
46
|
* along with the editor's subtree. `undefined` falls back to bits-ui's default. */
|
|
47
47
|
const portalTarget = $derived(triggerEl?.closest<HTMLElement>('[data-qm-root]') ?? undefined);
|
|
48
48
|
|
|
49
|
+
/** What the surface stays inside: the field's own card and the root it portals into.
|
|
50
|
+
* floating-ui intersects the two, so a flip answers to whichever is tighter. Empty
|
|
51
|
+
* falls back to the primitive's default, the surface's own clipping ancestors. */
|
|
52
|
+
const bounds = $derived(
|
|
53
|
+
[triggerEl?.closest<HTMLElement>('[data-qm-card]'), portalTarget].filter((el) => el != null)
|
|
54
|
+
);
|
|
55
|
+
|
|
49
56
|
/** Placement, re-measured on each opening: the surface rides the label rung beside
|
|
50
57
|
* the glyph, or stands over the field. */
|
|
51
58
|
let beside = $state(false);
|
|
@@ -132,11 +139,18 @@
|
|
|
132
139
|
neighbour's control instead, which reaching for it dismisses. -->
|
|
133
140
|
<!-- `align="center"` for the rung, `"start"` over the field: centred, a surface taller
|
|
134
141
|
than the glyph's line box would hang half its height onto the field's own control. -->
|
|
142
|
+
<!-- Both are preferences, and the card is what refuses them: its first field has no room
|
|
143
|
+
above its label inside one, so standing over that field puts the surface on the ground
|
|
144
|
+
above the card, reading as the neighbour's rather than as this field's. Refused, the
|
|
145
|
+
placement reverses and the surface goes under the field, over the control it describes:
|
|
146
|
+
the overlap the fork above spends a probe to avoid, and the only one left where there is
|
|
147
|
+
nothing above to stand over. -->
|
|
135
148
|
|
|
136
149
|
<Popover.Root bind:open>
|
|
137
150
|
<Popover.Portal to={portalTarget}>
|
|
138
151
|
<Popover.Content
|
|
139
152
|
customAnchor={triggerEl ?? null}
|
|
153
|
+
collisionBoundary={bounds}
|
|
140
154
|
side={beside ? 'right' : 'top'}
|
|
141
155
|
align={beside ? 'center' : 'start'}
|
|
142
156
|
sideOffset={6}
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
import Icon from './icons/Icon.svelte';
|
|
66
66
|
import type { IconName } from './icons/nodes.js';
|
|
67
67
|
import {
|
|
68
|
+
hasMarks,
|
|
68
69
|
rangeAnchor,
|
|
69
70
|
type FieldController,
|
|
70
71
|
type LeafViews,
|
|
@@ -140,7 +141,17 @@
|
|
|
140
141
|
// Escape out of a table cell lands.
|
|
141
142
|
const selection = view?.state.selection;
|
|
142
143
|
const formattable = selection instanceof TextSelection && !selection.empty;
|
|
143
|
-
|
|
144
|
+
// …in a leaf that has formatting at all. A `plaintext` leaf declares no mark
|
|
145
|
+
// types (`plaintextSchema`), so this surface withholds itself over one entirely,
|
|
146
|
+
// the way it never rises over the text input a `string` field draws: the value is
|
|
147
|
+
// literal text, and a mark on it is a value the boundary refuses to coerce back.
|
|
148
|
+
if (
|
|
149
|
+
!view ||
|
|
150
|
+
!view.hasFocus() ||
|
|
151
|
+
!formattable ||
|
|
152
|
+
!hasMarks(view.state.schema) ||
|
|
153
|
+
!hasText(view)
|
|
154
|
+
) {
|
|
144
155
|
open = false;
|
|
145
156
|
linkPromptOpen = false;
|
|
146
157
|
return;
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
const pmDoc = decode(value, inlineSchema);
|
|
48
48
|
const state = EditorState.create({
|
|
49
49
|
doc: pmDoc,
|
|
50
|
-
plugins: proseLeafPlugins(inlineSchema, { inline: true
|
|
50
|
+
plugins: proseLeafPlugins(inlineSchema, { inline: true })
|
|
51
51
|
});
|
|
52
52
|
const mounted = new EditorView(containerEl, {
|
|
53
53
|
state,
|