edytor 1.0.0 → 2.0.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/README.md +14 -72
- package/dist/block/block.svelte.d.ts +6 -4
- package/dist/block/block.svelte.js +10 -4
- package/dist/block/inlineBlock.svelte.d.ts +6 -2
- package/dist/block/inlineBlock.svelte.js +7 -2
- package/dist/cloudflare/room/context.d.ts +6 -0
- package/dist/collaboration/awarenessSelection.d.ts +22 -1
- package/dist/collaboration/awarenessSelection.js +42 -0
- package/dist/components/Block.svelte +3 -1
- package/dist/components/Edytor.svelte +36 -10
- package/dist/components/Edytor.svelte.d.ts +8 -1
- package/dist/components/InlineBlock.svelte +1 -1
- package/dist/crdt/data.d.ts +10 -0
- package/dist/crdt/data.js +20 -2
- package/dist/crdt/doc/reads.d.ts +7 -0
- package/dist/crdt/doc/reads.js +11 -1
- package/dist/crdt/document.d.ts +8 -0
- package/dist/crdt/document.js +12 -0
- package/dist/crdt/edytor-doc.d.ts +18 -0
- package/dist/crdt/edytor-doc.js +2 -1
- package/dist/crdt/index.d.ts +6 -0
- package/dist/crdt/operations.d.ts +1 -1
- package/dist/crdt/operations.js +1 -0
- package/dist/crdt/props.d.ts +32 -0
- package/dist/crdt/props.js +172 -0
- package/dist/{plugins/blockHandles → dnd}/dropIndicator.d.ts +2 -2
- package/dist/{plugins/blockHandles → dnd}/geometry.d.ts +2 -2
- package/dist/dnd/placement.d.ts +200 -0
- package/dist/dnd/placement.js +426 -0
- package/dist/edytor.svelte.d.ts +17 -4
- package/dist/edytor.svelte.js +16 -7
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/beforeInputCommands.js +8 -4
- package/dist/events/onDrop.d.ts +6 -0
- package/dist/events/onDrop.js +38 -10
- package/dist/events/onPaste.d.ts +2 -1
- package/dist/events/onPaste.js +8 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/labels.d.ts +2 -0
- package/dist/labels.js +2 -1
- package/dist/plugins/arrowMove/arrowMove.d.ts +1 -1
- package/dist/plugins/arrowMove/arrowMove.js +1 -1
- package/dist/plugins/blockHandles/BlockHandleController.svelte.d.ts +2 -122
- package/dist/plugins/blockHandles/BlockHandleController.svelte.js +29 -387
- package/dist/plugins/blockHandles/BlockHandles.svelte +14 -32
- package/dist/plugins/blockHandles/blockHandlesPlugin.js +2 -1
- package/dist/plugins/code/CodeHeader.svelte +5 -3
- package/dist/plugins/code/CodePlugin.svelte +3 -3
- package/dist/plugins/code/languages.d.ts +0 -3
- package/dist/plugins/code/languages.js +0 -2
- package/dist/plugins/columns/ColumnResize.svelte +3 -2
- package/dist/plugins/columns/ColumnsPlugin.svelte +1 -1
- package/dist/plugins/columns/gaps.d.ts +7 -5
- package/dist/plugins/columns/gaps.js +11 -6
- package/dist/plugins/columns/resize.svelte.d.ts +3 -3
- package/dist/plugins/columns/resize.svelte.js +11 -10
- package/dist/plugins/equation/EquationPlugin.svelte +1 -8
- package/dist/plugins/equation/EquationPlugin.svelte.d.ts +1 -0
- package/dist/plugins/equation/equation.css +7 -0
- package/dist/plugins/fileDrop.d.ts +28 -0
- package/dist/plugins/fileDrop.js +116 -0
- package/dist/plugins/image/ImagePlugin.svelte +42 -33
- package/dist/plugins/image/ImagePlugin.svelte.d.ts +13 -1
- package/dist/plugins/image/image.css +17 -0
- package/dist/plugins/index.d.ts +3 -4
- package/dist/plugins/index.js +0 -1
- package/dist/plugins/markdownShortcuts.js +5 -5
- package/dist/plugins/media/AudioPlugin.svelte +13 -1
- package/dist/plugins/media/AudioPlugin.svelte.d.ts +11 -1
- package/dist/plugins/media/FilePlugin.svelte +11 -1
- package/dist/plugins/media/FilePlugin.svelte.d.ts +9 -0
- package/dist/plugins/media/SourceBlock.svelte +48 -12
- package/dist/plugins/media/SourceBlock.svelte.d.ts +4 -3
- package/dist/plugins/media/UploadPreview.svelte +32 -0
- package/dist/plugins/media/UploadPreview.svelte.d.ts +11 -0
- package/dist/plugins/media/VideoPlugin.svelte +13 -1
- package/dist/plugins/media/VideoPlugin.svelte.d.ts +11 -1
- package/dist/plugins/media/empty.svelte.d.ts +13 -4
- package/dist/plugins/media/empty.svelte.js +30 -19
- package/dist/plugins/media/media.d.ts +7 -0
- package/dist/plugins/media/upload.css +23 -0
- package/dist/plugins/mention/MentionPlugin.svelte +72 -33
- package/dist/plugins/mention/MentionPlugin.svelte.d.ts +11 -2
- package/dist/plugins/mention/mention.css +6 -0
- package/dist/plugins/pageLink/PageLinkPlugin.svelte +122 -17
- package/dist/plugins/pageLink/PageLinkPlugin.svelte.d.ts +13 -1
- package/dist/plugins/pageLink/pageLink.css +6 -0
- package/dist/plugins/richtext/RichTextPlugin.svelte +14 -7
- package/dist/plugins/richtext/RichTextPlugin.svelte.d.ts +1 -0
- package/dist/plugins/richtext/richtext.css +6 -0
- package/dist/plugins/table/chrome.svelte.js +4 -3
- package/dist/plugins/uploads.svelte.d.ts +45 -32
- package/dist/plugins/uploads.svelte.js +105 -39
- package/dist/plugins.d.ts +40 -14
- package/dist/session/attempt.d.ts +16 -6
- package/dist/session/attempt.js +1 -0
- package/dist/session/bindings.js +34 -31
- package/dist/session/commands.js +2 -0
- package/dist/session/history.js +1 -0
- package/dist/session/inputRules.d.ts +6 -3
- package/dist/session/inputRules.js +9 -5
- package/dist/session/moves.d.ts +33 -0
- package/dist/session/moves.js +71 -1
- package/dist/session/navigation.d.ts +7 -0
- package/dist/session/navigation.js +19 -10
- package/dist/session/props.d.ts +28 -0
- package/dist/session/props.js +65 -0
- package/dist/session/uploads.d.ts +29 -0
- package/dist/session/uploads.js +10 -0
- package/dist/themes/notion.css +9 -0
- package/package.json +1 -1
- /package/dist/{plugins/blockHandles → dnd}/dropIndicator.js +0 -0
- /package/dist/{plugins/blockHandles → dnd}/geometry.js +0 -0
- /package/dist/{plugins/columns → dnd}/stacking.d.ts +0 -0
- /package/dist/{plugins/columns → dnd}/stacking.js +0 -0
package/dist/plugins.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { Edytor } from './edytor.svelte.js';
|
|
3
3
|
import type { Block } from './block/block.svelte.js';
|
|
4
|
+
import type { BlockMovePosition } from './session/moves.js';
|
|
4
5
|
import type { JSONBlock, JSONDoc, JSONInlineBlock, JSONText } from './utils/json.js';
|
|
5
6
|
import type { Text } from './text/text.svelte.js';
|
|
6
7
|
import type { TextTransform } from './surface/cells.js';
|
|
@@ -42,7 +43,12 @@ export type { Prevent };
|
|
|
42
43
|
export type BlockView<D = Record<string, any>> = {
|
|
43
44
|
readonly id: string;
|
|
44
45
|
readonly type: string;
|
|
45
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The block's data as plain state (`block.data`): reactive at any depth,
|
|
48
|
+
* and writable, each write a dispatched `patchData`. In a suggestion's
|
|
49
|
+
* preview it is a copy: a write there changes nothing.
|
|
50
|
+
*/
|
|
51
|
+
readonly data: D;
|
|
46
52
|
readonly selected: boolean;
|
|
47
53
|
readonly focused: boolean;
|
|
48
54
|
/**
|
|
@@ -241,7 +247,10 @@ export type TextRuleContext = {
|
|
|
241
247
|
};
|
|
242
248
|
/** What an input rule's `replace` receives besides the match. */
|
|
243
249
|
export type InputRuleContext = TextRuleContext & {
|
|
244
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* The typed text that completed the match: the end of the match, already
|
|
252
|
+
* in the document as its own undo step (`to` is after it).
|
|
253
|
+
*/
|
|
245
254
|
typed: string;
|
|
246
255
|
/**
|
|
247
256
|
* Remove the matched text (`from` to `to`) in the same plan as the first
|
|
@@ -267,13 +276,14 @@ export type InputRule = {
|
|
|
267
276
|
*/
|
|
268
277
|
find: RegExp;
|
|
269
278
|
/**
|
|
270
|
-
* The replacement
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
279
|
+
* The replacement, run once the typed text is in the document (its own
|
|
280
|
+
* undo step, so the first undo gives the typed text back). Return a
|
|
281
|
+
* string to replace the whole match with it (one command and one undo
|
|
282
|
+
* step with the matched text's removal; the caret after it), or write it
|
|
283
|
+
* yourself and return `true`, usually through `ctx.remove(() => …)`.
|
|
284
|
+
* Return `false` or `null`, writing nothing, to decline: the next rule is
|
|
285
|
+
* asked, and the typed text stays as typed, as it does when the
|
|
286
|
+
* replacement is refused (by the document, readonly or a plugin's veto).
|
|
277
287
|
*/
|
|
278
288
|
replace: (match: RegExpExecArray, ctx: InputRuleContext) => string | boolean | null | void;
|
|
279
289
|
};
|
|
@@ -292,6 +302,11 @@ export type TriggerContext = TextRuleContext & {
|
|
|
292
302
|
export type Trigger<T = any> = {
|
|
293
303
|
/** The text that opens the menu (one or two characters: `@`, `[[`). */
|
|
294
304
|
char: string;
|
|
305
|
+
/**
|
|
306
|
+
* The menu's name, read by its markup as `controller.trigger.name` (the
|
|
307
|
+
* mention plugin's `kind`: one menu snippet can serve several triggers).
|
|
308
|
+
*/
|
|
309
|
+
name?: string;
|
|
295
310
|
/** The rows for `query`, at once or as a promise (a search); the newest query wins. */
|
|
296
311
|
items: (query: string, ctx: TriggerContext) => readonly T[] | Promise<readonly T[]>;
|
|
297
312
|
/**
|
|
@@ -397,10 +412,18 @@ export type PluginOperations = {
|
|
|
397
412
|
prevent: Prevent;
|
|
398
413
|
e: ClipboardEvent;
|
|
399
414
|
}) => void;
|
|
400
|
-
/**
|
|
415
|
+
/**
|
|
416
|
+
* Called when a paste event is detected, and for a drop (its data as the
|
|
417
|
+
* clipboard's). `at`: where files dropped from outside land, the drop
|
|
418
|
+
* indicator's placement (beside, before, after or inside `block`).
|
|
419
|
+
*/
|
|
401
420
|
onPaste?: (payload: {
|
|
402
421
|
prevent: Prevent;
|
|
403
422
|
e: ClipboardEvent;
|
|
423
|
+
at?: {
|
|
424
|
+
block: string;
|
|
425
|
+
position: BlockMovePosition;
|
|
426
|
+
};
|
|
404
427
|
}) => void;
|
|
405
428
|
};
|
|
406
429
|
/**
|
|
@@ -493,9 +516,11 @@ export type BlockDefinition = {
|
|
|
493
516
|
*/
|
|
494
517
|
rendersContent?: boolean;
|
|
495
518
|
/**
|
|
496
|
-
* The files the kind takes (the image's `image/*`, the file block's any)
|
|
497
|
-
*
|
|
498
|
-
*
|
|
519
|
+
* The files the kind takes (the image's `image/*`, the file block's any)
|
|
520
|
+
* and who sends them (`send`: the media plugins set it from their
|
|
521
|
+
* `upload` option): an empty block's Upload sends one, and a pasted or
|
|
522
|
+
* dropped file becomes a block of this kind (`media.files`). A kind
|
|
523
|
+
* without it takes no files.
|
|
499
524
|
*/
|
|
500
525
|
upload?: KindUpload;
|
|
501
526
|
/**
|
|
@@ -609,7 +634,8 @@ export type BlockDefinition = {
|
|
|
609
634
|
export type InlineBlockView<D = Record<string, any>> = {
|
|
610
635
|
readonly id: string;
|
|
611
636
|
readonly type: string;
|
|
612
|
-
|
|
637
|
+
/** The atom's data as plain state (`atom.data`), writable; a suggested atom's is a copy. */
|
|
638
|
+
readonly data: D;
|
|
613
639
|
readonly selected: boolean;
|
|
614
640
|
/** The atom's id-only handle; a suggested atom has none. */
|
|
615
641
|
readonly handle: InlineBlock | undefined;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { Edytor } from '../edytor.svelte.js';
|
|
20
20
|
import type { Text } from '../text/text.svelte.js';
|
|
21
|
+
import type { BlockMovePosition } from './moves.js';
|
|
21
22
|
type Kind = 'text' | 'composition' | 'payload' | 'break' | 'delete' | 'history';
|
|
22
23
|
export type IntentRow = {
|
|
23
24
|
kind: Kind;
|
|
@@ -63,10 +64,10 @@ export declare const untargeted: (edytor: Edytor) => boolean;
|
|
|
63
64
|
* void's own field.
|
|
64
65
|
*/
|
|
65
66
|
declare const facts: (edytor: Edytor) => {
|
|
66
|
-
projection: import("./selection").SelectionProjection;
|
|
67
|
+
projection: import("./selection.js").SelectionProjection;
|
|
67
68
|
isCollapsed: boolean;
|
|
68
69
|
/** The admitted mark-edge side of a caret. */
|
|
69
|
-
edge: import("./editing/text").EdgeSide | undefined;
|
|
70
|
+
edge: import("./editing/text.js").EdgeSide | undefined;
|
|
70
71
|
isVoidEditableElement: boolean;
|
|
71
72
|
startText: Text | null;
|
|
72
73
|
endText: Text | null;
|
|
@@ -74,7 +75,7 @@ declare const facts: (edytor: Edytor) => {
|
|
|
74
75
|
yEnd: number;
|
|
75
76
|
texts: Text[];
|
|
76
77
|
/** The anchored target, fixed at admission. */
|
|
77
|
-
target: import("./selection").SelectionValue;
|
|
78
|
+
target: import("./selection.js").SelectionValue;
|
|
78
79
|
};
|
|
79
80
|
/**
|
|
80
81
|
* How DOM drift around a model-owned attempt is repaired: the observer
|
|
@@ -107,9 +108,16 @@ export type Occurrence = {
|
|
|
107
108
|
dataTransfer?: DataTransfer | null;
|
|
108
109
|
/** The range the browser declared (`getTargetRanges()`, a drop point). */
|
|
109
110
|
declared?: StaticRange | null;
|
|
111
|
+
/** Where a file drop lands, the indicator's placement (`plugins/fileDrop.ts`). */
|
|
112
|
+
placement?: DropAt | null;
|
|
110
113
|
cancelable: boolean;
|
|
111
114
|
event?: InputEvent;
|
|
112
115
|
};
|
|
116
|
+
/** A drop's placement in block terms: beside, before, after or inside `block`. */
|
|
117
|
+
export type DropAt = {
|
|
118
|
+
block: string;
|
|
119
|
+
position: BlockMovePosition;
|
|
120
|
+
};
|
|
113
121
|
export type Attempt = ReturnType<typeof facts> & {
|
|
114
122
|
id: number;
|
|
115
123
|
/** The intent, fixed at admission. */
|
|
@@ -121,6 +129,7 @@ export type Attempt = ReturnType<typeof facts> & {
|
|
|
121
129
|
dataTransfer: DataTransfer | null;
|
|
122
130
|
hasDataTransferTextPayload: boolean;
|
|
123
131
|
declared: StaticRange | null;
|
|
132
|
+
placement: DropAt | null;
|
|
124
133
|
/** Admitted from a keydown (the key's intent wins; the browser may never announce it). */
|
|
125
134
|
isStructuralKeyFallback: boolean;
|
|
126
135
|
owner: 'model' | 'browser';
|
|
@@ -135,10 +144,10 @@ export declare const intentSnapshot: (edytor: Edytor, inputType: string) => Atte
|
|
|
135
144
|
export declare const caretAt: (edytor: Edytor, text: Text | null | undefined, offset: number) => void;
|
|
136
145
|
/** Re-project an attempt's anchored target when it runs later than its admission. */
|
|
137
146
|
export declare const reproject: (edytor: Edytor, attempt: Attempt) => {
|
|
138
|
-
projection: import("./selection").SelectionProjection;
|
|
147
|
+
projection: import("./selection.js").SelectionProjection;
|
|
139
148
|
isCollapsed: boolean;
|
|
140
149
|
/** The admitted mark-edge side of a caret. */
|
|
141
|
-
edge: import("./editing/text").EdgeSide | undefined;
|
|
150
|
+
edge: import("./editing/text.js").EdgeSide | undefined;
|
|
142
151
|
isVoidEditableElement: boolean;
|
|
143
152
|
startText: Text | null;
|
|
144
153
|
endText: Text | null;
|
|
@@ -146,7 +155,7 @@ export declare const reproject: (edytor: Edytor, attempt: Attempt) => {
|
|
|
146
155
|
yEnd: number;
|
|
147
156
|
texts: Text[];
|
|
148
157
|
/** The anchored target, fixed at admission. */
|
|
149
|
-
target: import("./selection").SelectionValue;
|
|
158
|
+
target: import("./selection.js").SelectionValue;
|
|
150
159
|
} & {
|
|
151
160
|
id: number;
|
|
152
161
|
/** The intent, fixed at admission. */
|
|
@@ -158,6 +167,7 @@ export declare const reproject: (edytor: Edytor, attempt: Attempt) => {
|
|
|
158
167
|
dataTransfer: DataTransfer | null;
|
|
159
168
|
hasDataTransferTextPayload: boolean;
|
|
160
169
|
declared: StaticRange | null;
|
|
170
|
+
placement: DropAt | null;
|
|
161
171
|
/** Admitted from a keydown (the key's intent wins; the browser may never announce it). */
|
|
162
172
|
isStructuralKeyFallback: boolean;
|
|
163
173
|
owner: "model" | "browser";
|
package/dist/session/attempt.js
CHANGED
|
@@ -119,6 +119,7 @@ export const attemptOf = (edytor, occurrence, keyIntent) => {
|
|
|
119
119
|
dataTransfer: occurrence.dataTransfer ?? null,
|
|
120
120
|
hasDataTransferTextPayload: payload !== null,
|
|
121
121
|
declared: occurrence.declared ?? null,
|
|
122
|
+
placement: occurrence.placement ?? null,
|
|
122
123
|
isStructuralKeyFallback: Boolean(keyIntent),
|
|
123
124
|
owner: 'model',
|
|
124
125
|
expect: null,
|
package/dist/session/bindings.js
CHANGED
|
@@ -119,41 +119,44 @@ const moveRoots = (edytor, blocks, direction) => {
|
|
|
119
119
|
.flatMap((moved) => moved ?? []);
|
|
120
120
|
};
|
|
121
121
|
/**
|
|
122
|
-
* Tab / Shift+Tab
|
|
122
|
+
* Tab / Shift+Tab, with `allowNesting`: nest or unnest the selected block, or the caret's block.
|
|
123
123
|
* Selected blocks, or the blocks a text range spans, move one level per
|
|
124
124
|
* group of siblings, a selected block's selected descendants with it, as
|
|
125
125
|
* one command; the selection stays (its anchors moved with the blocks).
|
|
126
126
|
*/
|
|
127
|
-
const nest = (operation) => ({ edytor, prevent }) =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
127
|
+
const nest = (operation) => ({ edytor, prevent }) =>
|
|
128
|
+
// Nesting off (`allowNesting`, the default): the key is not the editor's, the browser moves the focus on.
|
|
129
|
+
edytor.allowNesting &&
|
|
130
|
+
prevent(() => {
|
|
131
|
+
const { selection } = edytor;
|
|
132
|
+
const { value } = selection;
|
|
133
|
+
const direction = operation === 'nestBlock' ? 'in' : 'out';
|
|
134
|
+
const blocks = getSelectionBlocks(edytor);
|
|
135
|
+
if (blocks.length > 1) {
|
|
136
|
+
if (moveRoots(edytor, blocks, direction).length)
|
|
137
|
+
selection.select(value);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const [selected] = selection.selectedBlocks;
|
|
141
|
+
const at = selection.textAt(selection.projection.start);
|
|
142
|
+
const block = selected ?? at?.text.parent;
|
|
143
|
+
if (!block)
|
|
144
|
+
return;
|
|
145
|
+
// Into the block before it; the view's move reveals a closed toggle it lands in or that adopts.
|
|
146
|
+
const into = block.previousBlock;
|
|
147
|
+
if (operation === 'nestBlock' && !into)
|
|
148
|
+
return edytor.dispatcher.record({ operation, status: 'refused' });
|
|
149
|
+
const result = operation === 'unNestBlock'
|
|
150
|
+
? edytor.unNestBlock(block.id)
|
|
151
|
+
: edytor.nestBlock(block.id, into.id);
|
|
152
|
+
if (result.status !== 'applied')
|
|
153
|
+
return;
|
|
154
|
+
edytor.announcer.moved({ blocks: [block], direction }, [block]);
|
|
155
|
+
if (selected)
|
|
156
|
+
selection.select(blockSelection([block.id]));
|
|
157
|
+
else if (at)
|
|
158
|
+
caretAt(edytor, block.content[at.text.index], at.offset);
|
|
159
|
+
});
|
|
157
160
|
/**
|
|
158
161
|
* Backspace/Delete over an editor-owned selection: the selected atom, or
|
|
159
162
|
* the selected blocks (the caret lands at the end of the block before them).
|
package/dist/session/commands.js
CHANGED
|
@@ -458,6 +458,8 @@ export class Dispatcher {
|
|
|
458
458
|
*/
|
|
459
459
|
operate = (operation, args) => {
|
|
460
460
|
const { edytor } = this;
|
|
461
|
+
// The property writes queued in this task go first (`session/props`).
|
|
462
|
+
edytor.dataWrites.flush();
|
|
461
463
|
const own = edytor.facade.prepare[operation];
|
|
462
464
|
const prepare = (a) => {
|
|
463
465
|
if (operation === 'insertText' && a[3] === undefined)
|
package/dist/session/history.js
CHANGED
|
@@ -114,6 +114,7 @@ export class History {
|
|
|
114
114
|
replay = (command) => !this.edytor.dispatcher.permits() ? 'refused' : this.#run(command) ? 'applied' : 'noop';
|
|
115
115
|
#run(command) {
|
|
116
116
|
const { undoManager: um, transaction: key, selection, facade } = this.edytor;
|
|
117
|
+
this.edytor.dataWrites.flush(); // this task's property writes are a step first
|
|
117
118
|
this.#open = null;
|
|
118
119
|
// Other writers (remote-apply and repair restores) stand aside while the
|
|
119
120
|
// replay commits: this view's recorded value decides.
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
* (`## ` → a heading, `:smile:` → 😄). The one engine behind every
|
|
4
4
|
* plugin's `inputRules`: it reads the typed text and the caret, finds the
|
|
5
5
|
* first rule whose `find` ends at the typed text, and replaces the typed
|
|
6
|
-
* `insertText` with
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* `insertText` with two steps: the typed text as typed (an insertion, which
|
|
7
|
+
* may continue the typing before it), then the rule's replacement, a command
|
|
8
|
+
* of its own whose first operation the matched text's removal leads
|
|
9
|
+
* (`dispatcher.lead`), so a refusal of either writes neither and the typed
|
|
10
|
+
* text stays as typed. The first undo gives the typed text back, the
|
|
11
|
+
* caret after it (as the autolink does).
|
|
9
12
|
*/
|
|
10
13
|
import type { Block } from '../block/block.svelte.js';
|
|
11
14
|
import type { Edytor } from '../edytor.svelte.js';
|
|
@@ -57,7 +57,8 @@ export const inputRulesHook = (edytor, rules) => {
|
|
|
57
57
|
/** The match's start in the segment; the match in the block, `from` to the caret. */
|
|
58
58
|
const start = match.index;
|
|
59
59
|
const from = text.segStart + start;
|
|
60
|
-
|
|
60
|
+
// The typed text is in the document: the match ends after it.
|
|
61
|
+
const to = text.segStart + yStart + value.length;
|
|
61
62
|
let removed = null;
|
|
62
63
|
/** Remove the match in the same plan as `then`'s first operation: `ctx.remove`. */
|
|
63
64
|
const remove = (then) => {
|
|
@@ -104,12 +105,15 @@ export const inputRulesHook = (edytor, rules) => {
|
|
|
104
105
|
prevent(() => {
|
|
105
106
|
running = true;
|
|
106
107
|
try {
|
|
108
|
+
// The typed text first, a step the replacement does not join.
|
|
109
|
+
const last = dispatcher.last;
|
|
110
|
+
typeAsIs();
|
|
111
|
+
if (dispatcher.last === last || dispatcher.last?.status !== 'applied')
|
|
112
|
+
return;
|
|
107
113
|
for (const { rule, match } of matches) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return written || typeAsIs();
|
|
114
|
+
if (apply(rule, match) !== null)
|
|
115
|
+
return;
|
|
111
116
|
}
|
|
112
|
-
typeAsIs();
|
|
113
117
|
}
|
|
114
118
|
finally {
|
|
115
119
|
running = false;
|
package/dist/session/moves.d.ts
CHANGED
|
@@ -25,6 +25,17 @@ export type BlockMoveRequest = {
|
|
|
25
25
|
} | {
|
|
26
26
|
direction: BlockMoveDirection;
|
|
27
27
|
});
|
|
28
|
+
/**
|
|
29
|
+
* The view's nesting rule (`allowNesting`, off by default): whether a user
|
|
30
|
+
* gesture may place `blocks` under `parent`. With nesting off, blocks land
|
|
31
|
+
* only at the root or in a block made to hold children — a container (a
|
|
32
|
+
* list, a column, a toggle's or a callout's body: `isContainer`, the kind's
|
|
33
|
+
* `container`), an island (a code block's lines) — or stay under the parent
|
|
34
|
+
* they already have (a document holding nested blocks still edits them).
|
|
35
|
+
* Every move (`destination`: keys, drops, menus, `edytor.move`) and every
|
|
36
|
+
* new block a drop places (`canPlaceNew`) reads it.
|
|
37
|
+
*/
|
|
38
|
+
export declare const nestsInto: (edytor: Edytor, parent: Block, blocks?: readonly Block[]) => boolean;
|
|
28
39
|
/** Whether the move is allowed: for runs (`parts`), whether one of them may move. */
|
|
29
40
|
export declare const canMove: (edytor: Edytor, request: BlockMoveRequest) => boolean;
|
|
30
41
|
/**
|
|
@@ -38,3 +49,25 @@ export declare const canMove: (edytor: Edytor, request: BlockMoveRequest) => boo
|
|
|
38
49
|
* (`announcer.moved`).
|
|
39
50
|
*/
|
|
40
51
|
export declare const move: (edytor: Edytor, request: BlockMoveRequest) => Block[];
|
|
52
|
+
/** A new block a drop places (`placeNew`): its kind, and its id when the caller names it. */
|
|
53
|
+
export type NewBlock = {
|
|
54
|
+
type: string;
|
|
55
|
+
id?: string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Whether new blocks of `kinds` may land at `target`'s `position` (a file
|
|
59
|
+
* dropped there, `placeNew`): the move's places (`destination`) asked of
|
|
60
|
+
* blocks not yet in the document — the container rule (`fits`) of the
|
|
61
|
+
* parent they land in, never inside a void or an island; beside, a layout
|
|
62
|
+
* kind and a target outside any island (`placeBeside` takes any block a
|
|
63
|
+
* column holds).
|
|
64
|
+
*/
|
|
65
|
+
export declare const canPlaceNew: (edytor: Edytor, kinds: readonly string[], target: Block, position: BlockMovePosition) => boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Insert new blocks at `target`'s `position` (a file drop's placement):
|
|
68
|
+
* before or after it, inside it (its last children), or beside it — inserted
|
|
69
|
+
* after the block it stands beside (`besideAt`) and moved there by the one
|
|
70
|
+
* move path (`layout.place-beside`) — in one transaction. The caller's
|
|
71
|
+
* command makes it one undo step. Answers the ids placed (none: refused).
|
|
72
|
+
*/
|
|
73
|
+
export declare const placeNew: (edytor: Edytor, blocks: readonly NewBlock[], target: Block, position: BlockMovePosition) => string[];
|
package/dist/session/moves.js
CHANGED
|
@@ -24,6 +24,22 @@ const placement = (edytor, request) => {
|
|
|
24
24
|
return step?.[0] ? { blocks, target: step[0], position: step[1] } : null;
|
|
25
25
|
};
|
|
26
26
|
const POSITIONS = ['before', 'after', 'inside', ...BESIDE];
|
|
27
|
+
/**
|
|
28
|
+
* The view's nesting rule (`allowNesting`, off by default): whether a user
|
|
29
|
+
* gesture may place `blocks` under `parent`. With nesting off, blocks land
|
|
30
|
+
* only at the root or in a block made to hold children — a container (a
|
|
31
|
+
* list, a column, a toggle's or a callout's body: `isContainer`, the kind's
|
|
32
|
+
* `container`), an island (a code block's lines) — or stay under the parent
|
|
33
|
+
* they already have (a document holding nested blocks still edits them).
|
|
34
|
+
* Every move (`destination`: keys, drops, menus, `edytor.move`) and every
|
|
35
|
+
* new block a drop places (`canPlaceNew`) reads it.
|
|
36
|
+
*/
|
|
37
|
+
export const nestsInto = (edytor, parent, blocks = []) => edytor.allowNesting ||
|
|
38
|
+
parent.isRoot ||
|
|
39
|
+
parent.isContainer ||
|
|
40
|
+
!!parent.definition.container ||
|
|
41
|
+
edytor.facade.isIsland(parent.id) ||
|
|
42
|
+
(blocks.length > 0 && blocks.every((block) => block.parent === parent));
|
|
27
43
|
/**
|
|
28
44
|
* The document write a move request is, or `null` when refused: a
|
|
29
45
|
* well-formed request, then the document's one structural answer
|
|
@@ -59,7 +75,7 @@ const destination = (edytor, request) => {
|
|
|
59
75
|
? { blocks, write: () => edytor.unNestBlocks(ids) }
|
|
60
76
|
: null;
|
|
61
77
|
const to = parent?.isRoot ? null : parent?.id;
|
|
62
|
-
if (!parent || !edytor.facade.canPlace(ids, to))
|
|
78
|
+
if (!parent || !nestsInto(edytor, parent, blocks) || !edytor.facade.canPlace(ids, to))
|
|
63
79
|
return null;
|
|
64
80
|
const at = position === 'inside' ? parent.children.length : target.index + (position === 'after' ? 1 : 0);
|
|
65
81
|
// The document counts the destination's children without the moved blocks.
|
|
@@ -122,3 +138,57 @@ const place = (edytor, request) => {
|
|
|
122
138
|
const result = move.write();
|
|
123
139
|
return result.status === 'applied' ? move.blocks : [];
|
|
124
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* Whether new blocks of `kinds` may land at `target`'s `position` (a file
|
|
143
|
+
* dropped there, `placeNew`): the move's places (`destination`) asked of
|
|
144
|
+
* blocks not yet in the document — the container rule (`fits`) of the
|
|
145
|
+
* parent they land in, never inside a void or an island; beside, a layout
|
|
146
|
+
* kind and a target outside any island (`placeBeside` takes any block a
|
|
147
|
+
* column holds).
|
|
148
|
+
*/
|
|
149
|
+
export const canPlaceNew = (edytor, kinds, target, position) => {
|
|
150
|
+
const { facade } = edytor;
|
|
151
|
+
if (edytor.readonly || !kinds.length || !target.isInTree || target.isRoot)
|
|
152
|
+
return false;
|
|
153
|
+
if (!POSITIONS.includes(position) || facade.insideIsland(target.id))
|
|
154
|
+
return false;
|
|
155
|
+
if (isBeside(position))
|
|
156
|
+
return ([...edytor.blocks.values()].some((definition) => definition.layout) &&
|
|
157
|
+
!facade.isIsland(target.id));
|
|
158
|
+
const parent = position === 'inside' ? target : target.parent;
|
|
159
|
+
if (!parent || !nestsInto(edytor, parent))
|
|
160
|
+
return false;
|
|
161
|
+
const to = parent.isRoot ? null : parent.id;
|
|
162
|
+
if (to !== null && (facade.isVoid(to) || facade.isIsland(to)))
|
|
163
|
+
return false;
|
|
164
|
+
return kinds.every((kind) => facade.fits(to, kind));
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Insert new blocks at `target`'s `position` (a file drop's placement):
|
|
168
|
+
* before or after it, inside it (its last children), or beside it — inserted
|
|
169
|
+
* after the block it stands beside (`besideAt`) and moved there by the one
|
|
170
|
+
* move path (`layout.place-beside`) — in one transaction. The caller's
|
|
171
|
+
* command makes it one undo step. Answers the ids placed (none: refused).
|
|
172
|
+
*/
|
|
173
|
+
export const placeNew = (edytor, blocks, target, position) => {
|
|
174
|
+
const kinds = blocks.map((block) => block.type);
|
|
175
|
+
if (!canPlaceNew(edytor, kinds, target, position))
|
|
176
|
+
return [];
|
|
177
|
+
const beside = isBeside(position);
|
|
178
|
+
const at = beside ? (edytor.idToBlock.get(edytor.facade.besideAt(target.id)) ?? target) : target;
|
|
179
|
+
const parent = position === 'inside' ? target : at.parent;
|
|
180
|
+
const destination = position === 'inside'
|
|
181
|
+
? { parent: target.id, index: target.children.length }
|
|
182
|
+
: {
|
|
183
|
+
parent: !parent || parent.isRoot ? null : parent.id,
|
|
184
|
+
index: at.index + (position === 'before' ? 0 : 1)
|
|
185
|
+
};
|
|
186
|
+
return edytor.transact(() => {
|
|
187
|
+
const { ids } = edytor.insertBlocks(destination, blocks.map((block) => ({ ...block })));
|
|
188
|
+
if (!beside || !ids.length)
|
|
189
|
+
return [...ids];
|
|
190
|
+
const placed = ids.flatMap((id) => edytor.idToBlock.get(id) ?? []);
|
|
191
|
+
move(edytor, { blocks: placed, target, position });
|
|
192
|
+
return [...ids];
|
|
193
|
+
});
|
|
194
|
+
};
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* lands it on no caret stop (`landed`).
|
|
24
24
|
*/
|
|
25
25
|
import type { Edytor } from '../edytor.svelte.js';
|
|
26
|
+
import type { Block } from '../block/block.svelte.js';
|
|
26
27
|
import type { HotKey } from './keymap.js';
|
|
27
28
|
/**
|
|
28
29
|
* With no target (`untargeted`: the value is `none`, no gesture placed the
|
|
@@ -48,6 +49,12 @@ export declare const move: (edytor: Edytor, unit: Unit, key: Dir | "left" | "rig
|
|
|
48
49
|
* moves keep the goal column. Answers whether a text selection moved.
|
|
49
50
|
*/
|
|
50
51
|
export declare const extendVertically: (edytor: Edytor, dir: Dir) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The block a collapsed text caret sits on the last line of (a line box
|
|
54
|
+
* where the view lays one out, else a line break), if it does: where
|
|
55
|
+
* ArrowDown leaves the block (`nav.trailing.exit`, `body.down`).
|
|
56
|
+
*/
|
|
57
|
+
export declare const onLastLine: (edytor: Edytor) => Block | undefined;
|
|
51
58
|
/**
|
|
52
59
|
* The trailing paragraph (`nav.trailing`, Notion): the caret goes to an
|
|
53
60
|
* empty top-level block of the root's default kind after the document's
|
|
@@ -255,21 +255,30 @@ const stopBelow = (edytor, block) => {
|
|
|
255
255
|
* cell, a code line, a heading). Never in a view that writes nothing.
|
|
256
256
|
*/
|
|
257
257
|
const leavesLastStop = (edytor) => {
|
|
258
|
-
const
|
|
259
|
-
const {
|
|
260
|
-
|
|
261
|
-
const block = at && edytor.idToBlock.get(at.block);
|
|
262
|
-
if (value.kind !== 'text' || !projection.isCollapsed || !block || !root)
|
|
263
|
-
return false;
|
|
264
|
-
if (!edytor.dispatcher.permits())
|
|
258
|
+
const block = onLastLine(edytor);
|
|
259
|
+
const { root } = edytor;
|
|
260
|
+
if (!block || !root || !edytor.dispatcher.permits())
|
|
265
261
|
return false;
|
|
266
262
|
if (block.parent?.isRoot && block.type === edytor.defaultChild(root))
|
|
267
263
|
return false;
|
|
264
|
+
return !stopBelow(edytor, block.id);
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* The block a collapsed text caret sits on the last line of (a line box
|
|
268
|
+
* where the view lays one out, else a line break), if it does: where
|
|
269
|
+
* ArrowDown leaves the block (`nav.trailing.exit`, `body.down`).
|
|
270
|
+
*/
|
|
271
|
+
export const onLastLine = (edytor) => {
|
|
272
|
+
const { value, projection } = edytor.selection;
|
|
273
|
+
const at = projection.start;
|
|
274
|
+
const block = at && edytor.idToBlock.get(at.block);
|
|
275
|
+
if (value.kind !== 'text' || !projection.isCollapsed || !block)
|
|
276
|
+
return;
|
|
268
277
|
const text = stringOf(edytor, at.block);
|
|
269
278
|
if (text.includes('\n', at.offset))
|
|
270
|
-
return
|
|
271
|
-
|
|
272
|
-
|
|
279
|
+
return;
|
|
280
|
+
const last = edytor.ports.surface.sameLine(at, { block: at.block, offset: text.length });
|
|
281
|
+
return last === false ? undefined : block;
|
|
273
282
|
};
|
|
274
283
|
/**
|
|
275
284
|
* The trailing paragraph (`nav.trailing`, Notion): the caret goes to an
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A view's property writes (`block.data`, `atom.data`, `edytor.data`): the
|
|
3
|
+
* data proxy (`crdt/props.ts`) over the view's one write path.
|
|
4
|
+
*
|
|
5
|
+
* The writes of one synchronous task are one dispatch: they queue per
|
|
6
|
+
* target and leave on the next microtask, or before any other operation
|
|
7
|
+
* this view dispatches, as the `patchData` operation of each target (more
|
|
8
|
+
* than one target: one user command, so one undo step, in one
|
|
9
|
+
* transaction). Hooks see that operation; a veto drops the burst. Until
|
|
10
|
+
* then the proxy reads its target's data with the queued patches applied
|
|
11
|
+
* (`dataPreview`), so a write reads back at once. A view that refuses
|
|
12
|
+
* writes (readonly, a read-only document) refuses an assignment at once:
|
|
13
|
+
* nothing is queued, the dispatcher records `refused`, nothing throws.
|
|
14
|
+
*/
|
|
15
|
+
import type { DataPatch } from '../crdt/data.js';
|
|
16
|
+
import type { DataTarget } from '../crdt/doc/types.js';
|
|
17
|
+
import type { Edytor } from '../edytor.svelte.js';
|
|
18
|
+
export declare class DataWrites {
|
|
19
|
+
#private;
|
|
20
|
+
private readonly edytor;
|
|
21
|
+
constructor(edytor: Edytor);
|
|
22
|
+
/** The live proxy over `target`'s data; `read` is its reactive, committed source. */
|
|
23
|
+
proxy<T extends object>(target: DataTarget, read: () => Record<string, unknown>): T;
|
|
24
|
+
/** Queue `patches` on `target` for this task's dispatch (refused at once when the view refuses writes). */
|
|
25
|
+
write(target: DataTarget, patches: DataPatch[]): undefined;
|
|
26
|
+
/** Dispatch the queued writes now: before any other operation of this view, and at the end of the task. */
|
|
27
|
+
flush(): undefined;
|
|
28
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { propsProxy } from '../crdt/props.js';
|
|
2
|
+
const keyOf = (target) => JSON.stringify(target);
|
|
3
|
+
export class DataWrites {
|
|
4
|
+
edytor;
|
|
5
|
+
#pending = new Map();
|
|
6
|
+
#scheduled = false;
|
|
7
|
+
constructor(edytor) {
|
|
8
|
+
this.edytor = edytor;
|
|
9
|
+
}
|
|
10
|
+
/** The live proxy over `target`'s data; `read` is its reactive, committed source. */
|
|
11
|
+
proxy(target, read) {
|
|
12
|
+
let preview;
|
|
13
|
+
const pending = () => this.#pending.get(keyOf(target));
|
|
14
|
+
const current = () => {
|
|
15
|
+
const base = read();
|
|
16
|
+
const queued = pending();
|
|
17
|
+
if (!queued)
|
|
18
|
+
return base;
|
|
19
|
+
const { version } = this.edytor.facade;
|
|
20
|
+
const count = queued.patches.length;
|
|
21
|
+
if (preview?.version !== version || preview.count !== count) {
|
|
22
|
+
const seen = this.edytor.facade.dataPreview(target, queued.patches);
|
|
23
|
+
preview = { version, count, data: seen?.data ?? null };
|
|
24
|
+
}
|
|
25
|
+
return preview.data ?? base;
|
|
26
|
+
};
|
|
27
|
+
return propsProxy(current, (patches) => this.write(target, patches), (path) => {
|
|
28
|
+
const queued = pending();
|
|
29
|
+
return queued
|
|
30
|
+
? (this.edytor.facade.dataPreview(target, queued.patches)?.ids(path) ?? [])
|
|
31
|
+
: this.edytor.facade.dataItemIds(target, path);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Queue `patches` on `target` for this task's dispatch (refused at once when the view refuses writes). */
|
|
35
|
+
write(target, patches) {
|
|
36
|
+
const { edytor } = this;
|
|
37
|
+
if (!edytor.dispatcher.permits())
|
|
38
|
+
return void edytor.patchData(target, patches);
|
|
39
|
+
const key = keyOf(target);
|
|
40
|
+
const queued = this.#pending.get(key);
|
|
41
|
+
if (queued)
|
|
42
|
+
queued.patches.push(...patches);
|
|
43
|
+
else
|
|
44
|
+
this.#pending.set(key, { target, patches: [...patches] });
|
|
45
|
+
if (this.#scheduled)
|
|
46
|
+
return;
|
|
47
|
+
this.#scheduled = true;
|
|
48
|
+
queueMicrotask(() => {
|
|
49
|
+
this.#scheduled = false;
|
|
50
|
+
if (!edytor.destroyed)
|
|
51
|
+
this.flush();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Dispatch the queued writes now: before any other operation of this view, and at the end of the task. */
|
|
55
|
+
flush() {
|
|
56
|
+
if (this.#pending.size === 0)
|
|
57
|
+
return;
|
|
58
|
+
const all = [...this.#pending.values()];
|
|
59
|
+
this.#pending.clear();
|
|
60
|
+
const { edytor } = this;
|
|
61
|
+
if (all.length === 1)
|
|
62
|
+
return void edytor.patchData(all[0].target, all[0].patches);
|
|
63
|
+
edytor.dispatcher.run('patchData', () => edytor.transact(() => all.forEach(({ target, patches }) => edytor.patchData(target, patches))));
|
|
64
|
+
}
|
|
65
|
+
}
|