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
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The resize bands of the hovered layout and the guide of a drag
|
|
6
|
-
* (`ColumnResize`), in the overlay: each band
|
|
7
|
-
* the block handles (their `+` and grip
|
|
6
|
+
* (`ColumnResize`), in the overlay: each band centered in its gap, above
|
|
7
|
+
* the block handles (their `+` and grip flush with the
|
|
8
|
+
* column, `gaps.ts`);
|
|
8
9
|
* hovered, it shows a gray guide in its middle: where the guide shows, a
|
|
9
10
|
* press resizes. A band is a focusable separator: its arrows resize
|
|
10
11
|
* (`resize.key`), focused it shows its guide in the resize color.
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
ColumnResizeStrips,
|
|
166
166
|
{ resize },
|
|
167
167
|
'edytor-column-resizers',
|
|
168
|
-
// Above the block handles (5): the band
|
|
168
|
+
// Above the block handles (5): the band centered in the gap at every height.
|
|
169
169
|
6,
|
|
170
170
|
resize.measure
|
|
171
171
|
)
|
|
@@ -19,11 +19,13 @@ export type Gap = {
|
|
|
19
19
|
*/
|
|
20
20
|
export declare const gapBefore: (column: Block) => Gap | null;
|
|
21
21
|
/**
|
|
22
|
-
* The resize band of `gap`:
|
|
23
|
-
*
|
|
24
|
-
* a column's block takes the
|
|
25
|
-
*
|
|
26
|
-
* (
|
|
22
|
+
* The resize band of `gap`: `BAND` px (the gap's width when narrower)
|
|
23
|
+
* centered in it, its guide on the gap's middle line (Notion's). The handle
|
|
24
|
+
* of a column's block takes the gap's right part (`handleSpan`): its box
|
|
25
|
+
* from the band to the block, its `+` and grip flush with the block
|
|
26
|
+
* (Notion's "+ ⋮⋮", over the band where the gap is narrower than they are).
|
|
27
|
+
* At the default 46px gap (`--edytor-columns-gap`): the band 18–28, the
|
|
28
|
+
* handle's box 28–46.
|
|
27
29
|
*/
|
|
28
30
|
export declare const bandOf: ({ left, right }: Gap) => {
|
|
29
31
|
left: number;
|
|
@@ -27,13 +27,18 @@ export const gapBefore = (column) => {
|
|
|
27
27
|
return null;
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
|
-
* The resize band of `gap`:
|
|
31
|
-
*
|
|
32
|
-
* a column's block takes the
|
|
33
|
-
*
|
|
34
|
-
* (
|
|
30
|
+
* The resize band of `gap`: `BAND` px (the gap's width when narrower)
|
|
31
|
+
* centered in it, its guide on the gap's middle line (Notion's). The handle
|
|
32
|
+
* of a column's block takes the gap's right part (`handleSpan`): its box
|
|
33
|
+
* from the band to the block, its `+` and grip flush with the block
|
|
34
|
+
* (Notion's "+ ⋮⋮", over the band where the gap is narrower than they are).
|
|
35
|
+
* At the default 46px gap (`--edytor-columns-gap`): the band 18–28, the
|
|
36
|
+
* handle's box 28–46.
|
|
35
37
|
*/
|
|
36
|
-
export const bandOf = ({ left, right }) =>
|
|
38
|
+
export const bandOf = ({ left, right }) => {
|
|
39
|
+
const width = Math.min(BAND, right - left);
|
|
40
|
+
return { left: left + (right - left - width) / 2, width };
|
|
41
|
+
};
|
|
37
42
|
/** The width a column block's handle spans in `gap`: from the band's right edge to the block at `blockLeft`. */
|
|
38
43
|
export const handleSpan = (gap, blockLeft) => {
|
|
39
44
|
const band = bandOf(gap);
|
|
@@ -40,9 +40,9 @@ type Drag = {
|
|
|
40
40
|
/**
|
|
41
41
|
* The column resize (`docs/columns-plan.md`, "Resize"), chrome in the
|
|
42
42
|
* overlay: while the pointer is over a layout (not readonly, not stacked),
|
|
43
|
-
* a band in each gap between two shown columns (`bandOf`:
|
|
44
|
-
*
|
|
45
|
-
* grip sit together
|
|
43
|
+
* a band in each gap between two shown columns (`bandOf`: `BAND` px centered
|
|
44
|
+
* in the gap, the layout's height), above the block handles,
|
|
45
|
+
* whose `+` and grip sit together flush with the block (`gaps.ts`, one
|
|
46
46
|
* measurement):
|
|
47
47
|
* wherever its gray guide shows (`--edytor-columns-resize-color`, Notion's,
|
|
48
48
|
* at any height, block rows included), a press resizes. Dragging one
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { pin } from '../../surface/overlay.js';
|
|
2
2
|
import { BandDrag, bandKey } from '../bands.svelte.js';
|
|
3
3
|
import { bandOf, gapBefore } from './gaps.js';
|
|
4
|
-
import { stacks } from '
|
|
4
|
+
import { stacks } from '../../dnd/stacking.js';
|
|
5
5
|
import { columnsEnglish, labelsOf } from '../../labels.js';
|
|
6
6
|
/** The guide line's width (the hover guide's and the drag's). */
|
|
7
7
|
export const GUIDE = 2;
|
|
@@ -10,12 +10,17 @@ export const weightOf = (data) => {
|
|
|
10
10
|
const width = data?.width;
|
|
11
11
|
return typeof width === 'number' && Number.isFinite(width) && width > 0 ? width : 1;
|
|
12
12
|
};
|
|
13
|
+
/** The band's middle in a gap `width` px wide, from the gap's left. */
|
|
14
|
+
const middleOf = (width) => {
|
|
15
|
+
const band = bandOf({ left: 0, right: width });
|
|
16
|
+
return band.left + band.width / 2;
|
|
17
|
+
};
|
|
13
18
|
/**
|
|
14
19
|
* The column resize (`docs/columns-plan.md`, "Resize"), chrome in the
|
|
15
20
|
* overlay: while the pointer is over a layout (not readonly, not stacked),
|
|
16
|
-
* a band in each gap between two shown columns (`bandOf`:
|
|
17
|
-
*
|
|
18
|
-
* grip sit together
|
|
21
|
+
* a band in each gap between two shown columns (`bandOf`: `BAND` px centered
|
|
22
|
+
* in the gap, the layout's height), above the block handles,
|
|
23
|
+
* whose `+` and grip sit together flush with the block (`gaps.ts`, one
|
|
19
24
|
* measurement):
|
|
20
25
|
* wherever its gray guide shows (`--edytor-columns-resize-color`, Notion's,
|
|
21
26
|
* at any height, block rows included), a press resizes. Dragging one
|
|
@@ -119,12 +124,8 @@ export class ColumnResize {
|
|
|
119
124
|
const strips = this.shown ? this.#gaps(origin) : [];
|
|
120
125
|
const drag = this.band.drag;
|
|
121
126
|
const guide = drag && {
|
|
122
|
-
// Where the hover guide showed: the band's middle,
|
|
123
|
-
x: drag.start +
|
|
124
|
-
this.#widthAt(drag) +
|
|
125
|
-
bandOf({ left: 0, right: drag.gap }).width / 2 -
|
|
126
|
-
GUIDE / 2 -
|
|
127
|
-
origin.left,
|
|
127
|
+
// Where the hover guide showed: the band's middle, the gap's (`bandOf`).
|
|
128
|
+
x: drag.start + this.#widthAt(drag) + middleOf(drag.gap) - GUIDE / 2 - origin.left,
|
|
128
129
|
top: drag.top - origin.top,
|
|
129
130
|
height: drag.height
|
|
130
131
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
|
+
import './equation.css';
|
|
2
3
|
import type {
|
|
3
4
|
BlockSnippetPayload,
|
|
4
5
|
InlineBlockSnippetPayload,
|
|
@@ -273,11 +274,3 @@
|
|
|
273
274
|
role="presentation"><EquationView expression={expressionOf(block.data)} display={false} /></span
|
|
274
275
|
>
|
|
275
276
|
{/snippet}
|
|
276
|
-
|
|
277
|
-
<style>
|
|
278
|
-
/* A wide equation scrolls in its block; its look is a theme's. */
|
|
279
|
-
[data-edytor-equation] {
|
|
280
|
-
overflow-x: auto;
|
|
281
|
-
overflow-y: hidden;
|
|
282
|
-
}
|
|
283
|
-
</style>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import './equation.css';
|
|
1
2
|
import type { Plugin } from '../../plugins.js';
|
|
2
3
|
import { EquationEditor, type EquationData, type EquationErrorPayload, type EquationPlaceholderPayload, type EquationPluginOptions, type EquationTarget, type KatexLike, type KatexLoader } from './equation.svelte.js';
|
|
3
4
|
export type { EquationData, EquationEditor, EquationErrorPayload, EquationPlaceholderPayload, EquationPluginOptions, EquationTarget, KatexLike, KatexLoader };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* Loaded by the plugin's module (a component's own <style> is dropped when only
|
|
2
|
+
its module is used: the plugin factory, never the component). */
|
|
3
|
+
/* A wide equation scrolls in its block; its look is a theme's. */
|
|
4
|
+
[data-edytor-equation] {
|
|
5
|
+
overflow-x: auto;
|
|
6
|
+
overflow-y: hidden;
|
|
7
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Files dragged from outside over a view (Notion): the drop indicator a
|
|
3
|
+
* block drag shows, where the files land, and that placement for the drop
|
|
4
|
+
* (the core's `onDrop.ts` asks the view's uploads, `ViewUploads.drop`, and passes it to the occurrence; the view's uploads place one
|
|
5
|
+
* block per file there, one command). The placement is the block drag's
|
|
6
|
+
* own resolution (`DropResolver`) for a drag that moves no block, whose
|
|
7
|
+
* places are the ones the document takes new blocks of the files' kinds
|
|
8
|
+
* at (`canPlaceNew`). Shown only in an editable view whose uploads take
|
|
9
|
+
* one of the files (`viewUploads`); with or without the block handles.
|
|
10
|
+
* Plugin-owned: the view's `FileUploads` (the first media plugin listed with
|
|
11
|
+
* an `upload`) installs it, so a view without one ships none of it.
|
|
12
|
+
*/
|
|
13
|
+
import type { Edytor } from '../edytor.svelte.js';
|
|
14
|
+
import type { DropAt } from '../session/attempt.js';
|
|
15
|
+
import type { DropPlacement } from '../dnd/geometry.js';
|
|
16
|
+
/**
|
|
17
|
+
* A `dragover` of files from outside: where they land, shown (Notion's
|
|
18
|
+
* indicator), `null` where none (a read-only view, no uploads taking them,
|
|
19
|
+
* no place the document takes them at).
|
|
20
|
+
*/
|
|
21
|
+
export declare const fileDragOver: (edytor: Edytor, event: DragEvent) => DropPlacement | null;
|
|
22
|
+
/**
|
|
23
|
+
* A drop of files from outside: the placement shown (re-resolved at the
|
|
24
|
+
* drop point), in block terms, and nothing shows any more.
|
|
25
|
+
*/
|
|
26
|
+
export declare const fileDropAt: (edytor: Edytor, event: DragEvent) => DropAt | null;
|
|
27
|
+
/** The files' drag left the view (or ended): the indicator goes. */
|
|
28
|
+
export declare const fileDragEnd: (edytor: Edytor) => void | undefined;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { canPlaceNew } from '../session/moves.js';
|
|
2
|
+
import { viewUploads } from '../session/uploads.js';
|
|
3
|
+
import { DropIndicator } from '../dnd/dropIndicator.js';
|
|
4
|
+
import { DropResolver, isDropTarget } from '../dnd/placement.js';
|
|
5
|
+
/** The kinds the files of a drag would be, by their types (a `dragover` sees no file, only its type). */
|
|
6
|
+
const kindsOf = (edytor, dataTransfer) => {
|
|
7
|
+
const uploads = viewUploads(edytor);
|
|
8
|
+
if (!uploads || !dataTransfer)
|
|
9
|
+
return [];
|
|
10
|
+
const kinds = new Set();
|
|
11
|
+
for (const item of Array.from(dataTransfer.items ?? []))
|
|
12
|
+
if (item.kind === 'file') {
|
|
13
|
+
const file = item.getAsFile() ?? new File([], 'file', { type: item.type });
|
|
14
|
+
const kind = uploads.kindOf(file);
|
|
15
|
+
if (kind)
|
|
16
|
+
kinds.add(kind);
|
|
17
|
+
}
|
|
18
|
+
// An engine that lists no item for a file (WebKit before the drop): its files themselves.
|
|
19
|
+
if (!kinds.size)
|
|
20
|
+
for (const file of Array.from(dataTransfer.files ?? [])) {
|
|
21
|
+
const kind = uploads.kindOf(file);
|
|
22
|
+
if (kind)
|
|
23
|
+
kinds.add(kind);
|
|
24
|
+
}
|
|
25
|
+
return [...kinds];
|
|
26
|
+
};
|
|
27
|
+
class FileDrag {
|
|
28
|
+
edytor;
|
|
29
|
+
indicator;
|
|
30
|
+
resolver;
|
|
31
|
+
#begun = false;
|
|
32
|
+
constructor(edytor) {
|
|
33
|
+
this.edytor = edytor;
|
|
34
|
+
this.indicator = new DropIndicator(edytor, {
|
|
35
|
+
group: () => [],
|
|
36
|
+
besideOf: (block) => this.resolver.besideOf(block),
|
|
37
|
+
layoutOf: (outer) => this.resolver.layoutOf(outer)
|
|
38
|
+
});
|
|
39
|
+
this.resolver = new DropResolver(edytor, {
|
|
40
|
+
targetOf: (node) => {
|
|
41
|
+
const block = edytor.idToBlock.get(node.dataset.edytorId ?? '');
|
|
42
|
+
return block?.node === node && isDropTarget(edytor, block) ? block : undefined;
|
|
43
|
+
},
|
|
44
|
+
handle: () => null,
|
|
45
|
+
indicator: this.indicator
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** The drag of files of `kinds`: no block moves; a place takes them all. */
|
|
49
|
+
#drag(kinds) {
|
|
50
|
+
return {
|
|
51
|
+
group: [],
|
|
52
|
+
fits: ({ target, position }) => canPlaceNew(this.edytor, kinds, target, position)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The placement at the pointer, shown (`null`: none, and nothing shows):
|
|
57
|
+
* over a drop target, its row's; elsewhere in the host, its margins'.
|
|
58
|
+
*/
|
|
59
|
+
over(kinds, input) {
|
|
60
|
+
const document = this.edytor.node?.ownerDocument;
|
|
61
|
+
if (!document)
|
|
62
|
+
return null;
|
|
63
|
+
if (!this.#begun) {
|
|
64
|
+
this.#begun = true;
|
|
65
|
+
this.indicator.begin(document);
|
|
66
|
+
}
|
|
67
|
+
const drag = this.#drag(kinds);
|
|
68
|
+
const node = this.resolver.hitAt(input);
|
|
69
|
+
const placement = node
|
|
70
|
+
? this.resolver.over(drag, node, input)
|
|
71
|
+
: this.resolver.margin(drag, input);
|
|
72
|
+
if (!placement) {
|
|
73
|
+
this.indicator.clear();
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
// A margin's placement shows as the block drag's page target does (its reorder holds).
|
|
77
|
+
this.indicator.show(node ?? document.body, placement);
|
|
78
|
+
return placement;
|
|
79
|
+
}
|
|
80
|
+
/** The drag left the view, or dropped: nothing shows. */
|
|
81
|
+
end() {
|
|
82
|
+
this.#begun = false;
|
|
83
|
+
this.indicator.end();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const drags = new WeakMap();
|
|
87
|
+
const dragOf = (edytor) => {
|
|
88
|
+
let drag = drags.get(edytor);
|
|
89
|
+
if (!drag)
|
|
90
|
+
drags.set(edytor, (drag = new FileDrag(edytor)));
|
|
91
|
+
return drag;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* A `dragover` of files from outside: where they land, shown (Notion's
|
|
95
|
+
* indicator), `null` where none (a read-only view, no uploads taking them,
|
|
96
|
+
* no place the document takes them at).
|
|
97
|
+
*/
|
|
98
|
+
export const fileDragOver = (edytor, event) => {
|
|
99
|
+
const kinds = edytor.readonly ? [] : kindsOf(edytor, event.dataTransfer);
|
|
100
|
+
if (!kinds.length) {
|
|
101
|
+
drags.get(edytor)?.end();
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return dragOf(edytor).over(kinds, event);
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* A drop of files from outside: the placement shown (re-resolved at the
|
|
108
|
+
* drop point), in block terms, and nothing shows any more.
|
|
109
|
+
*/
|
|
110
|
+
export const fileDropAt = (edytor, event) => {
|
|
111
|
+
const placement = fileDragOver(edytor, event);
|
|
112
|
+
fileDragEnd(edytor);
|
|
113
|
+
return placement && { block: placement.target.id, position: placement.position };
|
|
114
|
+
};
|
|
115
|
+
/** The files' drag left the view (or ended): the indicator goes. */
|
|
116
|
+
export const fileDragEnd = (edytor) => drags.get(edytor)?.end();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
|
+
import './image.css';
|
|
2
3
|
import { escapeHtml } from '../../clipboard/serializeClipboardFragment.js';
|
|
3
4
|
import type { Plugin, BlockSnippetPayload } from '../../plugins.js';
|
|
4
|
-
import { branded } from '../brand.js';
|
|
5
5
|
import type { Edytor } from '../../edytor.svelte.js';
|
|
6
6
|
import EmptySlot from '../media/EmptySlot.svelte';
|
|
7
7
|
import ImageChrome from './ImageChrome.svelte';
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
import { MEDIA_ICONS, type EmptyView } from '../media/empty.svelte.js';
|
|
24
24
|
import { imageEnglish, keywordsOf, labelsOf } from '../../labels.js';
|
|
25
25
|
import { listen } from '../../utils/listen.js';
|
|
26
|
+
import { fileUploads, uploadHooks, type Uploader } from '../uploads.svelte.js';
|
|
27
|
+
import { emptyHosts } from '../media/EmptySlot.svelte';
|
|
28
|
+
import UploadPreview from '../media/UploadPreview.svelte';
|
|
26
29
|
|
|
27
30
|
export {
|
|
28
31
|
MAX_INLINE_IMAGE_BYTES,
|
|
@@ -43,15 +46,27 @@
|
|
|
43
46
|
/** By view; the first image plugin of a view owns it, as its kind's definition (first wins). */
|
|
44
47
|
const views = new WeakMap<Edytor, View>();
|
|
45
48
|
|
|
49
|
+
export type ImagePluginOptions = {
|
|
50
|
+
/**
|
|
51
|
+
* Upload an image file and answer its URL: an empty image's Upload,
|
|
52
|
+
* and the pasted and dropped image files, each placed as an image at
|
|
53
|
+
* once (one undo step), showing the file while it uploads, then filled
|
|
54
|
+
* with the URL this answers. Without it, images are only linked. The
|
|
55
|
+
* first image plugin listed in a view gives it.
|
|
56
|
+
*/
|
|
57
|
+
upload?: Uploader;
|
|
58
|
+
};
|
|
59
|
+
|
|
46
60
|
/**
|
|
47
61
|
* Notion's image block: an "Add an image" panel until it has a source (a
|
|
48
62
|
* pasted link, or an upload when `upload` is given), then the image with
|
|
49
63
|
* an editable caption, its alt text, width and alignment set from the
|
|
50
64
|
* chrome the plugin shows over it. Void: its only text is the caption.
|
|
51
65
|
*/
|
|
52
|
-
export const createImagePlugin = (): Plugin => {
|
|
66
|
+
export const createImagePlugin = ({ upload }: ImagePluginOptions = {}): Plugin => {
|
|
53
67
|
const plugin: Plugin = (edytor) => {
|
|
54
68
|
const labels = labelsOf(edytor, 'image', imageEnglish);
|
|
69
|
+
const uploads = uploadHooks(edytor, upload);
|
|
55
70
|
const own: View = {
|
|
56
71
|
controls: new ImageControls(edytor),
|
|
57
72
|
// An accepted source; an inline image over the cap is never stored.
|
|
@@ -70,9 +85,12 @@
|
|
|
70
85
|
}
|
|
71
86
|
const owns = () => views.get(edytor) === own;
|
|
72
87
|
return {
|
|
88
|
+
onPaste: uploads.onPaste,
|
|
73
89
|
// The chrome: in the overlay, for the image under the pointer.
|
|
74
|
-
onEdytorAttached: (
|
|
75
|
-
|
|
90
|
+
onEdytorAttached: (view) => {
|
|
91
|
+
const release = uploads.onEdytorAttached?.(view);
|
|
92
|
+
if (!owns()) return release;
|
|
93
|
+
const { node } = view;
|
|
76
94
|
const { controls } = own;
|
|
77
95
|
const over = (event: PointerEvent) => controls.hover(event.target);
|
|
78
96
|
const leave = (event: PointerEvent) =>
|
|
@@ -94,6 +112,7 @@
|
|
|
94
112
|
off2();
|
|
95
113
|
off3();
|
|
96
114
|
unmount();
|
|
115
|
+
if (typeof release === 'function') release();
|
|
97
116
|
};
|
|
98
117
|
},
|
|
99
118
|
blocks: {
|
|
@@ -101,16 +120,18 @@
|
|
|
101
120
|
...imageKinds.image,
|
|
102
121
|
snippet: image,
|
|
103
122
|
element: 'figure',
|
|
104
|
-
// Image files
|
|
105
|
-
upload
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
// Image files, with an `upload`: an Upload and pasted or dropped files.
|
|
124
|
+
...(upload && {
|
|
125
|
+
upload: {
|
|
126
|
+
send: upload,
|
|
127
|
+
accepts: isImageFile,
|
|
128
|
+
data: (_file, answer) => {
|
|
129
|
+
const src = storableImageSrc(answer);
|
|
130
|
+
return src ? { src } : null;
|
|
131
|
+
},
|
|
132
|
+
filled: (data) => safeImageSrc(data.src) !== null
|
|
133
|
+
}
|
|
134
|
+
}),
|
|
114
135
|
presets: [
|
|
115
136
|
{
|
|
116
137
|
label: labels.image,
|
|
@@ -143,7 +164,7 @@
|
|
|
143
164
|
};
|
|
144
165
|
};
|
|
145
166
|
|
|
146
|
-
return
|
|
167
|
+
return plugin;
|
|
147
168
|
};
|
|
148
169
|
</script>
|
|
149
170
|
|
|
@@ -153,6 +174,7 @@
|
|
|
153
174
|
<!-- A suggestion's preview (no handle) reads only its data. -->
|
|
154
175
|
{@const view = block.handle ? views.get(block.handle.edytor) : undefined}
|
|
155
176
|
{@const src = safeImageSrc(block.data.src)}
|
|
177
|
+
{@const pending = block.handle && !src ? fileUploads(block.handle.edytor)?.of(block.id) : undefined}
|
|
156
178
|
{#if src}
|
|
157
179
|
<div use:block.void data-edytor-image data-align={imageAlignOf(block.data)}>
|
|
158
180
|
<img
|
|
@@ -164,6 +186,11 @@
|
|
|
164
186
|
)}
|
|
165
187
|
/>
|
|
166
188
|
</div>
|
|
189
|
+
{:else if pending?.status === 'uploading' && !emptyHosts.has(block.handle!.edytor)}
|
|
190
|
+
<!-- An upload with no empty markup to draw it: the file at once, its progress over it. -->
|
|
191
|
+
<div use:block.void data-edytor-image-empty>
|
|
192
|
+
<UploadPreview upload={pending} image />
|
|
193
|
+
</div>
|
|
167
194
|
{:else}
|
|
168
195
|
<div use:block.void data-edytor-image-empty>
|
|
169
196
|
<EmptySlot
|
|
@@ -176,21 +203,3 @@
|
|
|
176
203
|
The core renders the kind's <figure> around this markup.
|
|
177
204
|
--><!-- svelte-ignore a11y_figcaption_parent --><figcaption>{@render content()}</figcaption>
|
|
178
205
|
{/snippet}
|
|
179
|
-
|
|
180
|
-
<style>
|
|
181
|
-
/* Where an image narrower than its block sits (Notion's alignment). */
|
|
182
|
-
[data-edytor-image] {
|
|
183
|
-
display: flex;
|
|
184
|
-
position: relative;
|
|
185
|
-
justify-content: center;
|
|
186
|
-
}
|
|
187
|
-
[data-edytor-image][data-align='left'] {
|
|
188
|
-
justify-content: flex-start;
|
|
189
|
-
}
|
|
190
|
-
[data-edytor-image][data-align='right'] {
|
|
191
|
-
justify-content: flex-end;
|
|
192
|
-
}
|
|
193
|
-
[data-edytor-image] img {
|
|
194
|
-
max-width: 100%;
|
|
195
|
-
}
|
|
196
|
-
</style>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import './image.css';
|
|
1
2
|
import type { Plugin } from '../../plugins.js';
|
|
2
3
|
import { MAX_INLINE_IMAGE_BYTES, MIN_IMAGE_WIDTH, oversizedInlineImage, safeImageSrc, storableImageSrc, type ImageAlign } from './image.js';
|
|
4
|
+
import { type Uploader } from '../uploads.svelte.js';
|
|
3
5
|
export { MAX_INLINE_IMAGE_BYTES, MIN_IMAGE_WIDTH, oversizedInlineImage, safeImageSrc, storableImageSrc, type ImageAlign };
|
|
4
6
|
/** The data an image block reads (all optional). */
|
|
5
7
|
export type ImageData = {
|
|
@@ -8,13 +10,23 @@ export type ImageData = {
|
|
|
8
10
|
width?: number;
|
|
9
11
|
align?: ImageAlign;
|
|
10
12
|
};
|
|
13
|
+
export type ImagePluginOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* Upload an image file and answer its URL: an empty image's Upload,
|
|
16
|
+
* and the pasted and dropped image files, each placed as an image at
|
|
17
|
+
* once (one undo step), showing the file while it uploads, then filled
|
|
18
|
+
* with the URL this answers. Without it, images are only linked. The
|
|
19
|
+
* first image plugin listed in a view gives it.
|
|
20
|
+
*/
|
|
21
|
+
upload?: Uploader;
|
|
22
|
+
};
|
|
11
23
|
/**
|
|
12
24
|
* Notion's image block: an "Add an image" panel until it has a source (a
|
|
13
25
|
* pasted link, or an upload when `upload` is given), then the image with
|
|
14
26
|
* an editable caption, its alt text, width and alignment set from the
|
|
15
27
|
* chrome the plugin shows over it. Void: its only text is the caption.
|
|
16
28
|
*/
|
|
17
|
-
export declare const createImagePlugin: () => Plugin;
|
|
29
|
+
export declare const createImagePlugin: ({ upload }?: ImagePluginOptions) => Plugin;
|
|
18
30
|
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
19
31
|
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
20
32
|
$$bindings?: Bindings;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* Loaded by the plugin's module (a component's own <style> is dropped when only
|
|
2
|
+
its module is used: the plugin factory, never the component). */
|
|
3
|
+
/* Where an image narrower than its block sits (Notion's alignment). */
|
|
4
|
+
[data-edytor-image] {
|
|
5
|
+
display: flex;
|
|
6
|
+
position: relative;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
}
|
|
9
|
+
[data-edytor-image][data-align='left'] {
|
|
10
|
+
justify-content: flex-start;
|
|
11
|
+
}
|
|
12
|
+
[data-edytor-image][data-align='right'] {
|
|
13
|
+
justify-content: flex-end;
|
|
14
|
+
}
|
|
15
|
+
[data-edytor-image] img {
|
|
16
|
+
max-width: 100%;
|
|
17
|
+
}
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { createRichTextPlugin, richTextOperations, richTextPlaceholder, type RichTextPluginOptions, type TodoCheckboxPayload, type CalloutIconPayload, type EmptyBodyPayload } from './richtext/RichTextPlugin.svelte';
|
|
2
2
|
export { CALLOUT_ICONS, createCalloutIconsPlugin, type CalloutIconPicker, type CalloutIconsOptions } from './richtext/calloutIcons.svelte.js';
|
|
3
|
-
export { createUploadsPlugin, type UploadsOptions } from './uploads.svelte.js';
|
|
4
3
|
export { createArrowMovePlugin } from './arrowMove/arrowMove.js';
|
|
5
4
|
export type { MenuItemPayload } from './chrome.js';
|
|
6
5
|
export { BLOCK_ACTIVATE_EVENT, BLOCK_ADD_EVENT, type BlockActivation, type BlockAddition } from './blockHandles/BlockHandleController.svelte.js';
|
|
@@ -21,12 +20,12 @@ export { createBlockMenuPlugin } from './blockMenu/blockMenuPlugin.js';
|
|
|
21
20
|
export { createSuggestionsPlugin, type SuggestionBarOptions, type SuggestionBarPayload } from './suggestions/suggestionsPlugin.js';
|
|
22
21
|
export { createSuggestionBarPlugin } from './suggestions/SuggestionBars.svelte';
|
|
23
22
|
export type { BlockMenuOptions, BlockMenuController, BlockMenuAction, BlockMenuColor } from './blockMenu/BlockMenuController.svelte.js';
|
|
24
|
-
export { createImagePlugin, safeImageSrc, storableImageSrc, oversizedInlineImage, MAX_INLINE_IMAGE_BYTES, MIN_IMAGE_WIDTH, type ImageData, type ImageAlign } from './image/ImagePlugin.svelte';
|
|
23
|
+
export { createImagePlugin, type ImagePluginOptions, safeImageSrc, storableImageSrc, oversizedInlineImage, MAX_INLINE_IMAGE_BYTES, MIN_IMAGE_WIDTH, type ImageData, type ImageAlign } from './image/ImagePlugin.svelte';
|
|
25
24
|
export { createEmbedPlugin, EMBED_PROVIDERS, EMBED_SANDBOX, EMBED_ALLOW, embedSourceOf, type EmbedProvider, type EmbedPluginOptions, type EmbedFallbackPayload } from './media/EmbedPlugin.svelte';
|
|
26
25
|
export { createBookmarkPlugin, type BookmarkPluginOptions, type BookmarkPreview, type BookmarkCardPayload } from './media/BookmarkPlugin.svelte';
|
|
27
26
|
export { createFilePlugin, type FilePluginOptions, type FileRowPayload } from './media/FilePlugin.svelte';
|
|
28
|
-
export { createVideoPlugin } from './media/VideoPlugin.svelte';
|
|
29
|
-
export { createAudioPlugin } from './media/AudioPlugin.svelte';
|
|
27
|
+
export { createVideoPlugin, type VideoPluginOptions } from './media/VideoPlugin.svelte';
|
|
28
|
+
export { createAudioPlugin, type AudioPluginOptions } from './media/AudioPlugin.svelte';
|
|
30
29
|
export { safeWebUrl, safeMediaSrc, type MediaKind } from './media/media.js';
|
|
31
30
|
export type { MediaEmptyController, MediaEmptyFailure } from './media/empty.svelte.js';
|
|
32
31
|
export { createMediaEmptyPlugin, type MediaEmptyOptions } from './media/MediaEmpty.svelte';
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { createRichTextPlugin, richTextOperations, richTextPlaceholder } from './richtext/RichTextPlugin.svelte';
|
|
2
2
|
export { CALLOUT_ICONS, createCalloutIconsPlugin } from './richtext/calloutIcons.svelte.js';
|
|
3
|
-
export { createUploadsPlugin } from './uploads.svelte.js';
|
|
4
3
|
export { createArrowMovePlugin } from './arrowMove/arrowMove.js';
|
|
5
4
|
export { BLOCK_ACTIVATE_EVENT, BLOCK_ADD_EVENT } from './blockHandles/BlockHandleController.svelte.js';
|
|
6
5
|
export { createBlockHandlesPlugin } from './blockHandles/blockHandlesPlugin.js';
|
|
@@ -91,12 +91,12 @@ export const createMarkdownShortcutsPlugin = () => (edytor) => {
|
|
|
91
91
|
if (!text)
|
|
92
92
|
return false;
|
|
93
93
|
const { id } = block;
|
|
94
|
-
// The segment's start, in block offsets: the caret, less the text before it.
|
|
95
|
-
const at = to -
|
|
94
|
+
// The segment's start, in block offsets: the caret (after the typed text), less the text before it.
|
|
95
|
+
const at = to - match.input.length + found.start;
|
|
96
96
|
const { facade, dispatcher } = edytor;
|
|
97
|
-
const plan = () => facade.compose(facade.prepare.formatRange(id, at + found.open, found.content, { [found.mark]: true }),
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
const plan = () => facade.compose(facade.prepare.formatRange(id, at + found.open, found.content, { [found.mark]: true }),
|
|
98
|
+
// The closing marker, the typed character with it.
|
|
99
|
+
facade.prepare.deleteText(id, at + found.open + found.content, found.closing + typed.length), facade.prepare.deleteText(id, at, found.open));
|
|
100
100
|
const payload = { value: typed, start: before.length, end: before.length };
|
|
101
101
|
const result = dispatcher.plan('inlineMarkdown', payload, plan, block, text);
|
|
102
102
|
if (result.status !== 'applied')
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
2
|
import type { Plugin } from '../../plugins.js';
|
|
3
3
|
import { sourcePlugin } from './SourceBlock.svelte';
|
|
4
|
+
import type { Uploader } from '../uploads.svelte.js';
|
|
5
|
+
|
|
6
|
+
export type AudioPluginOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* Upload a audio file and answer its URL: an empty block's Upload, and
|
|
9
|
+
* the pasted and dropped audio files, each placed as a block at once
|
|
10
|
+
* (one undo step), showing its name and progress, then filled with the
|
|
11
|
+
* URL this answers. Without it, audios are only linked.
|
|
12
|
+
*/
|
|
13
|
+
upload?: Uploader;
|
|
14
|
+
};
|
|
4
15
|
|
|
5
16
|
/**
|
|
6
17
|
* Notion's audio block: an audio file (an `http(s)` link, or an upload
|
|
7
18
|
* through `upload`) in the browser's player, with an editable caption.
|
|
8
19
|
* Void: its only text is the caption.
|
|
9
20
|
*/
|
|
10
|
-
export const createAudioPlugin = (): Plugin =>
|
|
21
|
+
export const createAudioPlugin = ({ upload }: AudioPluginOptions = {}): Plugin =>
|
|
22
|
+
sourcePlugin('audio', ['sound', 'music', 'mp3'], undefined, {}, upload);
|
|
11
23
|
</script>
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import type { Plugin } from '../../plugins.js';
|
|
2
|
+
import type { Uploader } from '../uploads.svelte.js';
|
|
3
|
+
export type AudioPluginOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Upload a audio file and answer its URL: an empty block's Upload, and
|
|
6
|
+
* the pasted and dropped audio files, each placed as a block at once
|
|
7
|
+
* (one undo step), showing its name and progress, then filled with the
|
|
8
|
+
* URL this answers. Without it, audios are only linked.
|
|
9
|
+
*/
|
|
10
|
+
upload?: Uploader;
|
|
11
|
+
};
|
|
2
12
|
/**
|
|
3
13
|
* Notion's audio block: an audio file (an `http(s)` link, or an upload
|
|
4
14
|
* through `upload`) in the browser's player, with an editable caption.
|
|
5
15
|
* Void: its only text is the caption.
|
|
6
16
|
*/
|
|
7
|
-
export declare const createAudioPlugin: () => Plugin;
|
|
17
|
+
export declare const createAudioPlugin: ({ upload }?: AudioPluginOptions) => Plugin;
|
|
8
18
|
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
9
19
|
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
10
20
|
$$bindings?: Bindings;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Plugin } from '../../plugins.js';
|
|
4
4
|
import { escapeHtml, fileNameOf, safeMediaSrc, type FileRowPayload } from './media.js';
|
|
5
5
|
import { sourcePlugin } from './SourceBlock.svelte';
|
|
6
|
+
import type { Uploader } from '../uploads.svelte.js';
|
|
6
7
|
|
|
7
8
|
export type { FileRowPayload };
|
|
8
9
|
|
|
@@ -13,6 +14,14 @@
|
|
|
13
14
|
* downloads the file. The first file plugin listed in a view gives its row.
|
|
14
15
|
*/
|
|
15
16
|
row?: Snippet<[FileRowPayload]>;
|
|
17
|
+
/**
|
|
18
|
+
* Upload a file and answer its URL: an empty file block's Upload, and
|
|
19
|
+
* the pasted and dropped files no other kind takes, each placed as a
|
|
20
|
+
* file block at once (one undo step), its row showing the file's name,
|
|
21
|
+
* size and progress, then filled with the URL this answers. Without
|
|
22
|
+
* it, files are only linked.
|
|
23
|
+
*/
|
|
24
|
+
upload?: Uploader;
|
|
16
25
|
};
|
|
17
26
|
|
|
18
27
|
/**
|
|
@@ -42,6 +51,7 @@
|
|
|
42
51
|
return src ? { src, name: name || fileNameOf(src) } : undefined;
|
|
43
52
|
}
|
|
44
53
|
},
|
|
45
|
-
{ row: options.row }
|
|
54
|
+
{ row: options.row },
|
|
55
|
+
options.upload
|
|
46
56
|
);
|
|
47
57
|
</script>
|