@quillmark/svelte 0.20.0 → 0.21.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/THEMING.md +1 -1
- package/dist/core/codec/atoms.js +25 -10
- package/dist/core/codec/blocks.d.ts +5 -5
- package/dist/core/codec/blocks.js +23 -6
- package/dist/core/codec/code.d.ts +3 -3
- package/dist/core/codec/code.js +51 -21
- package/dist/core/codec/field.d.ts +9 -7
- package/dist/core/codec/field.js +22 -11
- package/dist/core/codec/inputrules.d.ts +4 -0
- package/dist/core/codec/inputrules.js +43 -5
- package/dist/core/codec/link-paste.d.ts +11 -0
- package/dist/core/codec/link-paste.js +32 -0
- package/dist/core/codec/lists.d.ts +2 -2
- package/dist/core/codec/lists.js +66 -26
- package/dist/core/codec/prose.css +71 -43
- package/dist/core/codec/schema.js +55 -11
- package/dist/core/codec/slash.js +0 -0
- package/dist/core/codec/table-view.d.ts +5 -0
- package/dist/core/codec/table-view.js +232 -103
- package/dist/core/rungs.d.ts +6 -0
- package/dist/core/rungs.js +11 -0
- package/dist/core/theme.css +24 -12
- package/dist/visual/ArrayField.svelte +168 -48
- package/dist/visual/BooleanField.svelte +12 -1
- package/dist/visual/Card.svelte +32 -20
- package/dist/visual/Card.svelte.d.ts +1 -0
- package/dist/visual/CardControls.svelte +15 -15
- package/dist/visual/DateField.svelte +93 -64
- package/dist/visual/EnumField.svelte +9 -4
- package/dist/visual/FieldHint.svelte +22 -5
- package/dist/visual/FieldLabel.svelte +3 -3
- package/dist/visual/FormatPopover.svelte +25 -9
- package/dist/visual/MatrixField.svelte +10 -7
- package/dist/visual/ObjectField.svelte +2 -4
- package/dist/visual/TipsCard.svelte +2 -5
- package/dist/visual/VisualEditorInner.svelte +108 -26
- package/dist/visual/controls.css +2 -1
- package/dist/visual/icons/Icon.svelte +3 -2
- package/dist/visual/icons/Icon.svelte.d.ts +3 -2
- package/dist/visual/icons/nodes.d.ts +25 -0
- package/dist/visual/icons/nodes.js +8 -0
- package/dist/visual/motion.d.ts +25 -3
- package/dist/visual/motion.js +58 -9
- package/dist/visual/strings.d.ts +6 -5
- package/dist/visual/strings.js +3 -3
- package/package.json +8 -8
package/THEMING.md
CHANGED
|
@@ -56,7 +56,7 @@ The poles take a colour, not `transparent` or `inherit`: the raised surfaces, th
|
|
|
56
56
|
| `--qm-bg` | `#fff` / `#272729` | Base surface: a card, and the preview's sheet. Fields, the popover and the column behind the cards each step off it. |
|
|
57
57
|
| `--qm-fg` | `#1c1c1c` / `#d7dadc` | Base ink. Body text, labels, and borders step off it. |
|
|
58
58
|
| `--qm-accent` | `#71717a` / `#a1a1aa` | Focus rings, active marks, the preview's active field box. The surface spends no hue of its own, so this is the one it wears. |
|
|
59
|
-
| `--qm-danger` | `#9e1518` / `#f9786c` | Error diagnostics
|
|
59
|
+
| `--qm-danger` | `#9e1518` / `#f9786c` | Error diagnostics and the delete glyph. |
|
|
60
60
|
| `--qm-font` | `ui-sans-serif, system-ui, sans-serif` | The editor surface's font family. Controls and buttons take it too, in place of the UA face. |
|
|
61
61
|
| `--qm-font-mono` | `ui-monospace, monospace` | The monospace face: the JSON array control, the tips card. |
|
|
62
62
|
| `--qm-font-size` | `0.875rem` | Chrome text: every control's size, the anchor the ramp derives up (title) and down (label/meta) from, and the anchor a document body sits one small step over. |
|
package/dist/core/codec/atoms.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// The block-atom link of the body's Backspace/Delete chains: a delete against an
|
|
2
|
-
// island or a divider selects it, and the press after that deletes it.
|
|
2
|
+
// island or a divider selects it, and the press after that deletes it. A table is the
|
|
3
|
+
// exception, having cells a caret can enter: the press steps into it instead, so a
|
|
4
|
+
// delete against one edits nothing (§`againstAtom`).
|
|
3
5
|
//
|
|
4
6
|
// This is the neighbour half of the rule the leaf already holds for a key typed over a
|
|
5
7
|
// selection (CODEC §"The table island"): a selection is the subject of the next
|
|
@@ -12,6 +14,7 @@
|
|
|
12
14
|
// first item lifts the item, and that stays the item's answer whatever sits above the
|
|
13
15
|
// list.
|
|
14
16
|
import { NodeSelection, TextSelection } from 'prosemirror-state';
|
|
17
|
+
import { tableAt } from './table-view.js';
|
|
15
18
|
/** The caret, when it sits at a textblock edge with an empty selection: the only place
|
|
16
19
|
* a delete is about a neighbour rather than about a character. `null` everywhere else,
|
|
17
20
|
* which is where the key keeps its ordinary meaning. */
|
|
@@ -44,20 +47,32 @@ function cutAt($pos, side) {
|
|
|
44
47
|
}
|
|
45
48
|
return null;
|
|
46
49
|
}
|
|
47
|
-
/**
|
|
48
|
-
*
|
|
49
|
-
* a
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
/** The press against a block atom: select it, rather than deleting it, or step into a
|
|
51
|
+
* mounted table, its last cell from below and its first from above. An empty line under
|
|
52
|
+
* a table goes with the step, its Backspace having nothing of its own to edit. Declines
|
|
53
|
+
* when the neighbour is anything else a caret can enter, so a paragraph, a heading, a
|
|
54
|
+
* quote and a list all keep the join the base keymap gives them. */
|
|
55
|
+
function againstAtom(side) {
|
|
56
|
+
return (state, dispatch, view) => {
|
|
52
57
|
const $head = atEdge(state, side);
|
|
53
58
|
const $cut = $head && cutAt($head, side);
|
|
54
|
-
if (!$cut)
|
|
59
|
+
if (!$head || !$cut)
|
|
55
60
|
return false;
|
|
56
61
|
const node = side < 0 ? $cut.nodeBefore : $cut.nodeAfter;
|
|
57
62
|
if (!node || !node.isAtom || !node.isBlock || !NodeSelection.isSelectable(node))
|
|
58
63
|
return false;
|
|
59
64
|
const at = side < 0 ? $cut.pos - node.nodeSize : $cut.pos;
|
|
60
|
-
|
|
65
|
+
const table = view && tableAt(view, at);
|
|
66
|
+
if (!table) {
|
|
67
|
+
dispatch?.(state.tr.setSelection(NodeSelection.create(state.doc, at)).scrollIntoView());
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (!dispatch)
|
|
71
|
+
return true;
|
|
72
|
+
const index = $head.index(-1);
|
|
73
|
+
if (side < 0 && $head.parent.content.size === 0 && $head.node(-1).canReplace(index, index + 1))
|
|
74
|
+
dispatch(state.tr.delete($head.before(), $head.after()));
|
|
75
|
+
table.enter(side);
|
|
61
76
|
return true;
|
|
62
77
|
};
|
|
63
78
|
}
|
|
@@ -72,7 +87,7 @@ export function atomKeymap(schema) {
|
|
|
72
87
|
if (!schema.nodes.island_block)
|
|
73
88
|
return {};
|
|
74
89
|
return {
|
|
75
|
-
Backspace:
|
|
76
|
-
Delete:
|
|
90
|
+
Backspace: againstAtom(-1),
|
|
91
|
+
Delete: againstAtom(1)
|
|
77
92
|
};
|
|
78
93
|
}
|
|
@@ -3,11 +3,11 @@ import type { Command, EditorState, Transaction } from 'prosemirror-state';
|
|
|
3
3
|
/** `# `: the heading `level` deep. `list_item` is `block+`, so one inside an item is a
|
|
4
4
|
* shape the content holds and `importMarkdown` produces from `- # title`. */
|
|
5
5
|
export declare function toHeading(level: number): Command;
|
|
6
|
-
/** A fence. The exit paragraph after it is the one a
|
|
7
|
-
* reason, sharpened: a code block is the one block a
|
|
8
|
-
* either, so a fence at the end of a body leaves the
|
|
9
|
-
* stays in the fence, not in the exit it minted. */
|
|
10
|
-
export declare function toCodeBlock(): Command;
|
|
6
|
+
/** A fence, carrying `attrs` (its `lang`). The exit paragraph after it is the one a
|
|
7
|
+
* block island mints for the same reason, sharpened: a code block is the one block a
|
|
8
|
+
* gap cursor will not sit beside either, so a fence at the end of a body leaves the
|
|
9
|
+
* caret nowhere to go. The caret stays in the fence, not in the exit it minted. */
|
|
10
|
+
export declare function toCodeBlock(attrs?: Attrs): Command;
|
|
11
11
|
/** `---`. A divider is a whole block, so this replaces its textblock rather than
|
|
12
12
|
* retyping one: `horizontal_rule` holds no content to retype into, and an empty block
|
|
13
13
|
* is the whole of what it may replace — anything else in one is a paragraph being
|
|
@@ -80,17 +80,17 @@ function wrapAt(tr, pos, type, attrs, joinBefore) {
|
|
|
80
80
|
export function toHeading(level) {
|
|
81
81
|
return retype('heading', { level });
|
|
82
82
|
}
|
|
83
|
-
/** A fence. The exit paragraph after it is the one a
|
|
84
|
-
* reason, sharpened: a code block is the one block a
|
|
85
|
-
* either, so a fence at the end of a body leaves the
|
|
86
|
-
* stays in the fence, not in the exit it minted. */
|
|
87
|
-
export function toCodeBlock() {
|
|
83
|
+
/** A fence, carrying `attrs` (its `lang`). The exit paragraph after it is the one a
|
|
84
|
+
* block island mints for the same reason, sharpened: a code block is the one block a
|
|
85
|
+
* gap cursor will not sit beside either, so a fence at the end of a body leaves the
|
|
86
|
+
* caret nowhere to go. The caret stays in the fence, not in the exit it minted. */
|
|
87
|
+
export function toCodeBlock(attrs) {
|
|
88
88
|
return (state, dispatch) => {
|
|
89
89
|
const { code_block: code, paragraph } = state.schema.nodes;
|
|
90
90
|
const { $from } = state.selection;
|
|
91
91
|
if (!code || !paragraph || !atHead(state) || !fits($from, code))
|
|
92
92
|
return false;
|
|
93
|
-
const tr = state.tr.setBlockType($from.pos, $from.pos, code);
|
|
93
|
+
const tr = state.tr.setBlockType($from.pos, $from.pos, code, attrs);
|
|
94
94
|
const at = tr.doc.resolve($from.pos).after();
|
|
95
95
|
if (!tr.doc.nodeAt(at))
|
|
96
96
|
tr.insert(at, paragraph.create());
|
|
@@ -322,6 +322,22 @@ const takeEmptyLine = (state, dispatch) => {
|
|
|
322
322
|
dispatch?.(state.tr.delete($from.before(), $from.after()).scrollIntoView());
|
|
323
323
|
return true;
|
|
324
324
|
};
|
|
325
|
+
/**
|
|
326
|
+
* Backspace at a heading's head → a paragraph, where it stands, and the press after is
|
|
327
|
+
* the join. The first press takes off the block's own shape, which is the whole of what
|
|
328
|
+
* it edits: the base keymap's join would merge the heading's text into the block above
|
|
329
|
+
* and lose the heading with it, and at the body's start it does nothing at all.
|
|
330
|
+
*/
|
|
331
|
+
const unheading = (state, dispatch) => {
|
|
332
|
+
const paragraph = state.schema.nodes.paragraph;
|
|
333
|
+
const { $from } = state.selection;
|
|
334
|
+
if (!paragraph || !atHead(state) || $from.parent.type.name !== 'heading')
|
|
335
|
+
return false;
|
|
336
|
+
if (!fits($from, paragraph))
|
|
337
|
+
return false;
|
|
338
|
+
dispatch?.(state.tr.setBlockType($from.pos, $from.pos, paragraph).scrollIntoView());
|
|
339
|
+
return true;
|
|
340
|
+
};
|
|
325
341
|
/** The body-wide link of the leaf's key chains: `{}` for a schema with no paragraph
|
|
326
342
|
* to mint. Outermost of the structural links, being about the body, or about the
|
|
327
343
|
* caret's own line, rather than about the surface either stands in. */
|
|
@@ -330,6 +346,7 @@ export function blockKeymap(schema) {
|
|
|
330
346
|
return {};
|
|
331
347
|
return {
|
|
332
348
|
Enter: chainCommands(paragraphAtBodyStart, paragraphFromEmptyFence, splitAcrossBlocks),
|
|
349
|
+
Backspace: unheading,
|
|
333
350
|
Delete: takeEmptyLine
|
|
334
351
|
};
|
|
335
352
|
}
|
|
@@ -9,8 +9,8 @@ import { type Command } from 'prosemirror-state';
|
|
|
9
9
|
* otherwise splits the item in two, each half holding a code block. `Shift-Enter` is
|
|
10
10
|
* the same command because it is the same edit: the line break the key means
|
|
11
11
|
* everywhere else (`breaks.ts`) is, inside a fence, the newline Enter already takes.
|
|
12
|
-
* Every command here declines away from a code block — inside one for the
|
|
13
|
-
*
|
|
14
|
-
* keys elsewhere.
|
|
12
|
+
* Every command here declines away from a code block — inside one for the keys about
|
|
13
|
+
* its text and its last line, against one for the two about its edge — so the list
|
|
14
|
+
* links keep the keys elsewhere.
|
|
15
15
|
*/
|
|
16
16
|
export declare function codeKeymap(schema: Schema): Record<string, Command>;
|
package/dist/core/codec/code.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextSelection } from 'prosemirror-state';
|
|
1
|
+
import { Selection, TextSelection } from 'prosemirror-state';
|
|
2
2
|
import { newlineInCode } from 'prosemirror-commands';
|
|
3
3
|
/** One indent level. */
|
|
4
4
|
const UNIT = ' ';
|
|
@@ -107,7 +107,7 @@ const outdentInCode = (state, dispatch) => {
|
|
|
107
107
|
* The textblock the caret's own block stands directly against on `side`: its sibling
|
|
108
108
|
* under one parent, which is the only neighbour a press joins the text of. `null` at a
|
|
109
109
|
* container's edge, where the base keymap moves the whole block in or out rather than
|
|
110
|
-
* joining anything, and so
|
|
110
|
+
* joining anything, and so changes nothing here.
|
|
111
111
|
*/
|
|
112
112
|
function blockAgainst(state, side) {
|
|
113
113
|
const { $from, empty } = state.selection;
|
|
@@ -122,22 +122,22 @@ function blockAgainst(state, side) {
|
|
|
122
122
|
return { node, pos: side < 0 ? $edge.pos - node.nodeSize : $edge.pos };
|
|
123
123
|
}
|
|
124
124
|
/**
|
|
125
|
-
* Backspace at a fence's
|
|
126
|
-
*
|
|
127
|
-
* press after that is an ordinary join between two prose blocks.
|
|
125
|
+
* Backspace or Delete at a fence's edge, from either side, edits only the block the caret
|
|
126
|
+
* is in.
|
|
128
127
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* `
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
128
|
+
* Inside the fence, a press at its head or its end retypes it to a paragraph and joins
|
|
129
|
+
* nothing: `toCodeBlock`'s inverse in its place (`blocks.ts`), taking no text with it,
|
|
130
|
+
* since `linebreakReplacement` carries the fence's newlines across as breaks
|
|
131
|
+
* (`schema.ts`). The press after that is an ordinary join between two prose blocks.
|
|
132
|
+
*
|
|
133
|
+
* From the prose block against it, the caret steps into the fence, to the end it faces,
|
|
134
|
+
* and the fence is left as it is: a join would retype one side's whole text as the
|
|
135
|
+
* other's kind, and a retype from outside edits a block the caret is not in. An empty
|
|
136
|
+
* line under a fence goes with the step, its Backspace having nothing of its own to edit.
|
|
135
137
|
*
|
|
136
138
|
* Declines where both sides are code and where neither is: those joins retype nothing.
|
|
137
|
-
* An empty block on either side never reaches this at all: the block link takes the
|
|
138
|
-
* empty line ahead of the chain (`blocks.ts`), and there is no text to retype.
|
|
139
139
|
*/
|
|
140
|
-
function
|
|
140
|
+
function atProseEdge(side) {
|
|
141
141
|
return (state, dispatch) => {
|
|
142
142
|
const paragraph = state.schema.nodes.paragraph;
|
|
143
143
|
const edge = blockAgainst(state, side);
|
|
@@ -147,11 +147,40 @@ function retypeAtProseEdge(side) {
|
|
|
147
147
|
const inside = !!$from.parent.type.spec.code;
|
|
148
148
|
if (inside === !!edge.node.type.spec.code)
|
|
149
149
|
return false;
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
if (inside) {
|
|
151
|
+
dispatch?.(state.tr.setBlockType($from.pos, $from.pos, paragraph).scrollIntoView());
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
const tr = state.tr;
|
|
155
|
+
const index = $from.index(-1);
|
|
156
|
+
if (side < 0 && $from.parent.content.size === 0 && $from.node(-1).canReplace(index, index + 1))
|
|
157
|
+
tr.delete($from.before(), $from.after());
|
|
158
|
+
const into = side < 0 ? edge.pos + edge.node.nodeSize - 1 : edge.pos + 1;
|
|
159
|
+
dispatch?.(tr.setSelection(TextSelection.create(tr.doc, into)).scrollIntoView());
|
|
152
160
|
return true;
|
|
153
161
|
};
|
|
154
162
|
}
|
|
163
|
+
/** ↓ on the last line of a fence nothing follows → a paragraph after it, the caret
|
|
164
|
+
* there: no block stands below to arrow to, and a gap cursor will not sit beside a
|
|
165
|
+
* fence. */
|
|
166
|
+
const belowFinalFence = (state, dispatch) => {
|
|
167
|
+
const paragraph = state.schema.nodes.paragraph;
|
|
168
|
+
const { $from, empty } = state.selection;
|
|
169
|
+
if (!paragraph || !empty || !$from.parent.type.spec.code)
|
|
170
|
+
return false;
|
|
171
|
+
if ($from.parent.textBetween($from.parentOffset, $from.parent.content.size).includes('\n'))
|
|
172
|
+
return false;
|
|
173
|
+
const $after = state.doc.resolve($from.after());
|
|
174
|
+
if (Selection.findFrom($after, 1))
|
|
175
|
+
return false;
|
|
176
|
+
if (!$after.parent.canReplaceWith($after.index(), $after.index(), paragraph))
|
|
177
|
+
return false;
|
|
178
|
+
if (dispatch) {
|
|
179
|
+
const tr = state.tr.insert($after.pos, paragraph.create());
|
|
180
|
+
dispatch(tr.setSelection(TextSelection.create(tr.doc, $after.pos + 1)).scrollIntoView());
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
};
|
|
155
184
|
/**
|
|
156
185
|
* The `code_block` link of the body's key chains: `{}` for the inline/plaintext
|
|
157
186
|
* schemas, which declare no code node.
|
|
@@ -161,9 +190,9 @@ function retypeAtProseEdge(side) {
|
|
|
161
190
|
* otherwise splits the item in two, each half holding a code block. `Shift-Enter` is
|
|
162
191
|
* the same command because it is the same edit: the line break the key means
|
|
163
192
|
* everywhere else (`breaks.ts`) is, inside a fence, the newline Enter already takes.
|
|
164
|
-
* Every command here declines away from a code block — inside one for the
|
|
165
|
-
*
|
|
166
|
-
* keys elsewhere.
|
|
193
|
+
* Every command here declines away from a code block — inside one for the keys about
|
|
194
|
+
* its text and its last line, against one for the two about its edge — so the list
|
|
195
|
+
* links keep the keys elsewhere.
|
|
167
196
|
*/
|
|
168
197
|
export function codeKeymap(schema) {
|
|
169
198
|
if (!schema.nodes.code_block)
|
|
@@ -173,7 +202,8 @@ export function codeKeymap(schema) {
|
|
|
173
202
|
'Shift-Tab': outdentInCode,
|
|
174
203
|
Enter: newlineInCode,
|
|
175
204
|
'Shift-Enter': newlineInCode,
|
|
176
|
-
Backspace:
|
|
177
|
-
Delete:
|
|
205
|
+
Backspace: atProseEdge(-1),
|
|
206
|
+
Delete: atProseEdge(1),
|
|
207
|
+
ArrowDown: belowFinalFence
|
|
178
208
|
};
|
|
179
209
|
}
|
|
@@ -193,15 +193,17 @@ export declare function createField(opts: CreateFieldOpts): FieldController;
|
|
|
193
193
|
* never fork the keymap/plugin ordering. History first, then any leaf-specific
|
|
194
194
|
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
195
195
|
* a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
|
|
196
|
-
* the rest of the stack leaves, then the markdown-shorthand input rules
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* the
|
|
196
|
+
* the rest of the stack leaves, then the markdown-shorthand input rules and the
|
|
197
|
+
* Backspace that takes one back, the field keymap over the base keymap, the URL paste
|
|
198
|
+
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
199
|
+
* leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
|
|
200
|
+
* (`islands.ts`), the table paste (`table-paste.ts`) and the markdown paste
|
|
201
|
+
* (`markdown-paste.ts`).
|
|
200
202
|
*
|
|
201
203
|
* Every mark-shaped plugin reads the schema rather than a flag: over either plaintext
|
|
202
|
-
* schema the shorthand rules build nothing (each is guarded on its mark type)
|
|
203
|
-
* toggles bind no key, so a `**bold**` keeps the delimiters
|
|
204
|
-
* a second rule saying so.
|
|
204
|
+
* schema the shorthand rules build nothing (each is guarded on its mark type), the
|
|
205
|
+
* toggles bind no key and a pasted URL stays text, so a `**bold**` keeps the delimiters
|
|
206
|
+
* its author typed without a second rule saying so.
|
|
205
207
|
*/
|
|
206
208
|
export declare function proseLeafPlugins(schema: Schema, opts: {
|
|
207
209
|
inline: boolean;
|
package/dist/core/codec/field.js
CHANGED
|
@@ -27,10 +27,11 @@ import { lower, pmToContent, scanContent, scanDoc, contentEdit } from './encode.
|
|
|
27
27
|
import { islandPastePlugin } from './islands.js';
|
|
28
28
|
import { anchorsFromContent } from './marks.js';
|
|
29
29
|
import { contentEqual, createReconciler } from './reconcile.js';
|
|
30
|
-
import { inputRulesPlugin } from './inputrules.js';
|
|
30
|
+
import { inputRulesPlugin, undoRuleKeymap } from './inputrules.js';
|
|
31
31
|
import { linebreakPlugin } from './breaks.js';
|
|
32
32
|
import { bodyKeymap } from './keymap.js';
|
|
33
33
|
import { blockSchema, isBlockSchema, leafSchema, plainSchema } from './schema.js';
|
|
34
|
+
import { linkPastePlugin } from './link-paste.js';
|
|
34
35
|
import { markdownPastePlugin } from './markdown-paste.js';
|
|
35
36
|
import { tablePastePlugin } from './table-paste.js';
|
|
36
37
|
import { DEFAULT_TABLE_STRINGS, tableNodeView } from './table-view.js';
|
|
@@ -504,24 +505,28 @@ export function createField(opts) {
|
|
|
504
505
|
* never fork the keymap/plugin ordering. History first, then any leaf-specific
|
|
505
506
|
* plugins (`afterHistory`: the addressed leaf passes its anchor-position plugin;
|
|
506
507
|
* a by-value leaf passes none), then {@link linebreakPlugin}, which normalizes what
|
|
507
|
-
* the rest of the stack leaves, then the markdown-shorthand input rules
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
* the
|
|
508
|
+
* the rest of the stack leaves, then the markdown-shorthand input rules and the
|
|
509
|
+
* Backspace that takes one back, the field keymap over the base keymap, the URL paste
|
|
510
|
+
* that links a selection (`link-paste.ts`), and last the ones that answer to a block
|
|
511
|
+
* leaf's own shapes: the gap cursor, {@link pastAtomPlugin}, the island paste pass
|
|
512
|
+
* (`islands.ts`), the table paste (`table-paste.ts`) and the markdown paste
|
|
513
|
+
* (`markdown-paste.ts`).
|
|
511
514
|
*
|
|
512
515
|
* Every mark-shaped plugin reads the schema rather than a flag: over either plaintext
|
|
513
|
-
* schema the shorthand rules build nothing (each is guarded on its mark type)
|
|
514
|
-
* toggles bind no key, so a `**bold**` keeps the delimiters
|
|
515
|
-
* a second rule saying so.
|
|
516
|
+
* schema the shorthand rules build nothing (each is guarded on its mark type), the
|
|
517
|
+
* toggles bind no key and a pasted URL stays text, so a `**bold**` keeps the delimiters
|
|
518
|
+
* its author typed without a second rule saying so.
|
|
516
519
|
*/
|
|
517
520
|
export function proseLeafPlugins(schema, opts) {
|
|
518
521
|
const list = [history(), ...(opts.afterHistory ?? []), linebreakPlugin(schema)];
|
|
519
522
|
if (opts.slash)
|
|
520
523
|
list.push(slashPlugin(opts.slash, opts.slashHint));
|
|
521
524
|
if (!opts.noInputRules)
|
|
522
|
-
list.push(inputRulesPlugin(schema));
|
|
525
|
+
list.push(inputRulesPlugin(schema), undoRuleKeymap());
|
|
523
526
|
list.push(keymap(editorKeymap(schema, opts.inline, !!opts.slash)));
|
|
524
527
|
list.push(keymap(baseKeymap));
|
|
528
|
+
if (schema.marks.link)
|
|
529
|
+
list.push(linkPastePlugin(schema.marks.link));
|
|
525
530
|
// Each answers to something only the block schema holds: any other leaf is
|
|
526
531
|
// textblocks alone, with no island and no gap to put a cursor in.
|
|
527
532
|
if (isBlockSchema(schema))
|
|
@@ -553,8 +558,10 @@ function plainClipboardPlugin() {
|
|
|
553
558
|
* A printable key over a selected atom writes past it. A selection is the subject
|
|
554
559
|
* of the next command, never a thing armed for
|
|
555
560
|
* replacement: Backspace deletes it, Mod-C copies it, and a character lands beside
|
|
556
|
-
* it. A block island takes
|
|
557
|
-
* the image in the same line: one rule, both node
|
|
561
|
+
* it. A block island takes the empty paragraph after it, or a new one where there is
|
|
562
|
+
* none; an inline one the caret after the image in the same line: one rule, both node
|
|
563
|
+
* types. Only a paragraph is reused, since an empty heading or fence would set the
|
|
564
|
+
* text in a shape the key never asked for.
|
|
558
565
|
*
|
|
559
566
|
* PM routes text input over a non-`TextSelection` through `handleTextInput` before
|
|
560
567
|
* falling back to replacing the selection, so this one prop is the whole of it.
|
|
@@ -570,9 +577,13 @@ function pastAtomPlugin() {
|
|
|
570
577
|
return false;
|
|
571
578
|
const tr = view.state.tr;
|
|
572
579
|
const at = selection.to; // just past the node, in both coordinate senses
|
|
580
|
+
const after = tr.doc.resolve(at).nodeAfter;
|
|
573
581
|
if (selection.node.isInline) {
|
|
574
582
|
tr.setSelection(TextSelection.create(tr.doc, at)).insertText(text);
|
|
575
583
|
}
|
|
584
|
+
else if (after?.type === schema.nodes.paragraph && after.content.size === 0) {
|
|
585
|
+
tr.setSelection(TextSelection.create(tr.doc, at + 1)).insertText(text);
|
|
586
|
+
}
|
|
576
587
|
else {
|
|
577
588
|
const para = schema.nodes.paragraph.create(null, schema.text(text));
|
|
578
589
|
tr.insert(at, para);
|
|
@@ -5,3 +5,7 @@ import type { Plugin } from 'prosemirror-state';
|
|
|
5
5
|
export declare function markdownInputRules(schema: Schema): InputRule[];
|
|
6
6
|
/** The input-rules plugin the field mounts by default (opt-out via createField). */
|
|
7
7
|
export declare function inputRulesPlugin(schema: Schema): Plugin;
|
|
8
|
+
/** Backspace on the press right after a rule fired takes the rule back and leaves the
|
|
9
|
+
* text it consumed, the way a writer who meant the literal `# ` says so; and Enter fires
|
|
10
|
+
* the fence. Mounted ahead of every other keymap: each declines on any other press. */
|
|
11
|
+
export declare function undoRuleKeymap(): Plugin;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Input rules: the markdown-shorthand plugin the codec owns (`**`→strong, `*`→em,
|
|
2
2
|
// `~~`→strike, `` ` ``→code, `# `→heading, `- `/`1. `→lists, `> `→blockquote,
|
|
3
|
-
// `---`→divider,
|
|
3
|
+
// `---`→divider), and the fence, which fires on Enter rather than as typed.
|
|
4
4
|
// Mark rules are hand-rolled (prosemirror-inputrules ships only node rules); every
|
|
5
5
|
// block rule is one regexp over a command in `blocks.ts`, which is the slash menu's
|
|
6
6
|
// implementation too. These produce transactions the codec already lowers: they add no
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// sets"), and a construct that arrives through `importMarkdown` must stay authorable or
|
|
13
13
|
// a document opens carrying a shape the editor refuses to make. So one guard survives:
|
|
14
14
|
// `- ` / `1. ` at the head of an existing item, where Tab owns the nesting.
|
|
15
|
-
import { InputRule, inputRules } from 'prosemirror-inputrules';
|
|
15
|
+
import { InputRule, inputRules, undoInputRule } from 'prosemirror-inputrules';
|
|
16
|
+
import { keymap } from 'prosemirror-keymap';
|
|
16
17
|
import { consuming, insertDivider, toCodeBlock, toHeading, wrapInList, wrapInQuote } from './blocks.js';
|
|
17
18
|
/**
|
|
18
19
|
* A mark input rule: when `regexp` (ending in the closing delimiter) matches
|
|
@@ -47,9 +48,10 @@ function markInputRule(regexp, markType, delimLen) {
|
|
|
47
48
|
}
|
|
48
49
|
/** A block shorthand: the regexp that fires it, and the command it runs in the match's
|
|
49
50
|
* place. Every one is `^`-anchored, so the command's own head-of-a-textblock guard is
|
|
50
|
-
* the same position the regexp already stands at.
|
|
51
|
+
* the same position the regexp already stands at. The caret it places is scrolled to
|
|
52
|
+
* here: `consuming` rebuilds the transaction, so a flag the command set is not on it. */
|
|
51
53
|
function blockRule(regexp, command) {
|
|
52
|
-
return new InputRule(regexp, (state, match, start, end) => consuming(state, start, end, command(match))?.tr ?? null);
|
|
54
|
+
return new InputRule(regexp, (state, match, start, end) => consuming(state, start, end, command(match))?.tr.scrollIntoView() ?? null);
|
|
53
55
|
}
|
|
54
56
|
/** The full markdown-shorthand rule set for a block schema. */
|
|
55
57
|
export function markdownInputRules(schema) {
|
|
@@ -69,7 +71,6 @@ export function markdownInputRules(schema) {
|
|
|
69
71
|
// Block shorthands. Each command answers for the schema it is run against, so a
|
|
70
72
|
// schema without the node declines rather than being guarded here.
|
|
71
73
|
rules.push(blockRule(/^(#{1,6})\s$/, (match) => toHeading(match[1].length)));
|
|
72
|
-
rules.push(blockRule(/^```$/, toCodeBlock));
|
|
73
74
|
rules.push(blockRule(/^---$/, insertDivider));
|
|
74
75
|
// `- ` / `1. ` at the start of a block, here or nested inside an item — but not at
|
|
75
76
|
// the start of an item's own first block, where the command declines.
|
|
@@ -82,3 +83,40 @@ export function markdownInputRules(schema) {
|
|
|
82
83
|
export function inputRulesPlugin(schema) {
|
|
83
84
|
return inputRules({ rules: markdownInputRules(schema) });
|
|
84
85
|
}
|
|
86
|
+
/** A fence's opening line: three backticks, then the language, if one is typed. */
|
|
87
|
+
const FENCE = /^```([^\s`]*)$/;
|
|
88
|
+
/**
|
|
89
|
+
* Enter at the end of a paragraph that is a fence's opening line → a code block carrying
|
|
90
|
+
* the language typed after the backticks. The one shorthand Enter fires rather than its
|
|
91
|
+
* last character: on the third backtick the language is not typed yet. It records itself
|
|
92
|
+
* as the input-rules plugin's last rule, so the Backspace after takes it back like any
|
|
93
|
+
* other shorthand.
|
|
94
|
+
*/
|
|
95
|
+
const fenceOnEnter = (state, dispatch) => {
|
|
96
|
+
const { $from, empty } = state.selection;
|
|
97
|
+
const line = $from.parent;
|
|
98
|
+
if (!empty || line.type !== state.schema.nodes.paragraph)
|
|
99
|
+
return false;
|
|
100
|
+
if ($from.parentOffset !== line.content.size || line.childCount !== 1 || !line.firstChild.isText)
|
|
101
|
+
return false;
|
|
102
|
+
const match = FENCE.exec(line.textContent);
|
|
103
|
+
if (!match)
|
|
104
|
+
return false;
|
|
105
|
+
const done = consuming(state, $from.start(), $from.pos, toCodeBlock({ lang: match[1] || null }));
|
|
106
|
+
if (!done)
|
|
107
|
+
return false;
|
|
108
|
+
if (dispatch) {
|
|
109
|
+
const tr = done.tr.scrollIntoView();
|
|
110
|
+
const rules = state.plugins.find((plugin) => plugin.spec.isInputRules);
|
|
111
|
+
if (rules)
|
|
112
|
+
tr.setMeta(rules, { transform: tr, from: $from.pos, to: $from.pos, text: '' });
|
|
113
|
+
dispatch(tr);
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
};
|
|
117
|
+
/** Backspace on the press right after a rule fired takes the rule back and leaves the
|
|
118
|
+
* text it consumed, the way a writer who meant the literal `# ` says so; and Enter fires
|
|
119
|
+
* the fence. Mounted ahead of every other keymap: each declines on any other press. */
|
|
120
|
+
export function undoRuleKeymap() {
|
|
121
|
+
return keymap({ Backspace: undoInputRule, Enter: fenceOnEnter });
|
|
122
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MarkType } from 'prosemirror-model';
|
|
2
|
+
import { Plugin } from 'prosemirror-state';
|
|
3
|
+
/**
|
|
4
|
+
* Links a selection within one block where the clipboard's text is one URL naming a
|
|
5
|
+
* scheme the mark renders. A selection crossing blocks, or held in one taking no link
|
|
6
|
+
* (a code block), keeps the default paste, and so does a URL nothing would follow.
|
|
7
|
+
*
|
|
8
|
+
* The ops are `setLink`'s pair (`visual/links.ts`), so the mark diff lowers the paste
|
|
9
|
+
* as one link exchanged for another.
|
|
10
|
+
*/
|
|
11
|
+
export declare function linkPastePlugin(link: MarkType): Plugin;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Plugin, TextSelection } from 'prosemirror-state';
|
|
2
|
+
import { rendersHref } from './urls.js';
|
|
3
|
+
/** One URL and nothing else: a scheme, then no whitespace. */
|
|
4
|
+
const URL_ALONE = /^[a-z][a-z0-9+.-]*:\S+$/i;
|
|
5
|
+
/**
|
|
6
|
+
* Links a selection within one block where the clipboard's text is one URL naming a
|
|
7
|
+
* scheme the mark renders. A selection crossing blocks, or held in one taking no link
|
|
8
|
+
* (a code block), keeps the default paste, and so does a URL nothing would follow.
|
|
9
|
+
*
|
|
10
|
+
* The ops are `setLink`'s pair (`visual/links.ts`), so the mark diff lowers the paste
|
|
11
|
+
* as one link exchanged for another.
|
|
12
|
+
*/
|
|
13
|
+
export function linkPastePlugin(link) {
|
|
14
|
+
return new Plugin({
|
|
15
|
+
props: {
|
|
16
|
+
handlePaste(view, event) {
|
|
17
|
+
const { selection } = view.state;
|
|
18
|
+
if (!(selection instanceof TextSelection) || selection.empty)
|
|
19
|
+
return false;
|
|
20
|
+
const { $from, $to } = selection;
|
|
21
|
+
if (!$from.sameParent($to) || !$from.parent.type.allowsMarkType(link))
|
|
22
|
+
return false;
|
|
23
|
+
const href = event.clipboardData?.getData('text/plain').trim() ?? '';
|
|
24
|
+
if (!URL_ALONE.test(href) || !rendersHref(href))
|
|
25
|
+
return false;
|
|
26
|
+
const { from, to } = selection;
|
|
27
|
+
view.dispatch(view.state.tr.removeMark(from, to, link).addMark(from, to, link.create({ href })));
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -16,8 +16,8 @@ import type { Command } from 'prosemirror-state';
|
|
|
16
16
|
* `splitListItem`, then `liftListItem`. The middle link carries two behaviors of
|
|
17
17
|
* its own: it splits a non-empty item, and on an empty item it either splits the
|
|
18
18
|
* wrapping item (a nested empty item, so Enter lifts exactly one level) or bails
|
|
19
|
-
* so the third link lifts a top-level empty item out to a paragraph. `Backspace`
|
|
20
|
-
*
|
|
19
|
+
* so the third link lifts a top-level empty item out to a paragraph. `Backspace` lifts
|
|
20
|
+
* an item at its start, and joins a block onto the list it stands after. `Delete` is
|
|
21
21
|
* that same chain at the seam below rather than a second table of cases beside it, so
|
|
22
22
|
* the two keys cannot come to differ over a seam this link reaches. An empty line is
|
|
23
23
|
* not one: the block link takes it ahead of this chain (`blocks.ts`), the caret's own
|