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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { Plugin } from '../../plugins.js';
|
|
3
3
|
import { type FileRowPayload } from './media.js';
|
|
4
|
+
import type { Uploader } from '../uploads.svelte.js';
|
|
4
5
|
export type { FileRowPayload };
|
|
5
6
|
export type FilePluginOptions = {
|
|
6
7
|
/**
|
|
@@ -9,6 +10,14 @@ export type FilePluginOptions = {
|
|
|
9
10
|
* downloads the file. The first file plugin listed in a view gives its row.
|
|
10
11
|
*/
|
|
11
12
|
row?: Snippet<[FileRowPayload]>;
|
|
13
|
+
/**
|
|
14
|
+
* Upload a file and answer its URL: an empty file block's Upload, and
|
|
15
|
+
* the pasted and dropped files no other kind takes, each placed as a
|
|
16
|
+
* file block at once (one undo step), its row showing the file's name,
|
|
17
|
+
* size and progress, then filled with the URL this answers. Without
|
|
18
|
+
* it, files are only linked.
|
|
19
|
+
*/
|
|
20
|
+
upload?: Uploader;
|
|
12
21
|
};
|
|
13
22
|
/**
|
|
14
23
|
* Notion's file block: an attachment, uploaded through `upload` (or
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
import EmptySlot from './EmptySlot.svelte';
|
|
4
4
|
import Control from '../Control.svelte';
|
|
5
5
|
import { labelsOf, mediaEnglish } from '../../labels.js';
|
|
6
|
+
import { emptyHosts } from './EmptySlot.svelte';
|
|
7
|
+
import UploadPreview from './UploadPreview.svelte';
|
|
8
|
+
import { fileUploads, uploadHooks, type Uploader } from '../uploads.svelte.js';
|
|
6
9
|
import {
|
|
7
10
|
claimed,
|
|
8
11
|
escapeHtml,
|
|
@@ -48,8 +51,8 @@
|
|
|
48
51
|
* attachment; Notion's): a media kind (`mediaPlugin`) drawn by this
|
|
49
52
|
* snippet, a link or an uploaded file stored as one data write (a file's
|
|
50
53
|
* whole data, its source, name and size; a video's or audio track's
|
|
51
|
-
* `src`), and with an `upload
|
|
52
|
-
* (`media.files
|
|
54
|
+
* `src`), and with an `upload` (its uploader), the pasted, dropped and
|
|
55
|
+
* picked files it takes (`media.files`). A video or an audio track
|
|
53
56
|
* exports as its tag in a `figure` and imports from one (bare, or in a
|
|
54
57
|
* `figure`) with a safe `src` or `source`; the file kind brings its own
|
|
55
58
|
* (`record`), and its `row` markup (`own`).
|
|
@@ -69,11 +72,13 @@
|
|
|
69
72
|
return src ? { src } : undefined;
|
|
70
73
|
}
|
|
71
74
|
},
|
|
72
|
-
own: Pick<MediaView, 'row'> = {}
|
|
75
|
+
own: Pick<MediaView, 'row'> = {},
|
|
76
|
+
upload?: Uploader
|
|
73
77
|
): Plugin =>
|
|
74
|
-
mediaPlugin(kind, keywords, () => {
|
|
78
|
+
mediaPlugin(kind, keywords, (edytor) => {
|
|
75
79
|
return {
|
|
76
80
|
...own,
|
|
81
|
+
hooks: uploadHooks(edytor, upload),
|
|
77
82
|
store: (block, value, file) => {
|
|
78
83
|
const data = sourceData(kind, value, file);
|
|
79
84
|
if (!data) return 'invalid';
|
|
@@ -83,13 +88,16 @@
|
|
|
83
88
|
},
|
|
84
89
|
record: {
|
|
85
90
|
snippet: source,
|
|
86
|
-
// Its files
|
|
87
|
-
upload
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// Its files, with an `upload`: an Upload and pasted or dropped files.
|
|
92
|
+
...(upload && {
|
|
93
|
+
upload: {
|
|
94
|
+
send: upload,
|
|
95
|
+
accepts: ACCEPTS[kind],
|
|
96
|
+
fallback: kind === 'file',
|
|
97
|
+
data: (file: File, answer: string) => sourceData(kind, answer, file),
|
|
98
|
+
filled: (data: Record<string, unknown>) => safeMediaSrc(data.src) !== null
|
|
99
|
+
}
|
|
100
|
+
}),
|
|
93
101
|
...record
|
|
94
102
|
}
|
|
95
103
|
};
|
|
@@ -103,6 +111,8 @@
|
|
|
103
111
|
-->
|
|
104
112
|
{#snippet source({ block, content }: BlockSnippetPayload<Record<string, unknown>>)}
|
|
105
113
|
{@const src = safeMediaSrc(block.data.src)}
|
|
114
|
+
{@const pending =
|
|
115
|
+
block.handle && !src ? fileUploads(block.handle.edytor)?.of(block.id) : undefined}
|
|
106
116
|
{#if src && block.type === 'file'}
|
|
107
117
|
{@const name =
|
|
108
118
|
typeof block.data.name === 'string' && block.data.name ? block.data.name : fileNameOf(src)}
|
|
@@ -120,10 +130,34 @@
|
|
|
120
130
|
size === undefined
|
|
121
131
|
? ''
|
|
122
132
|
: labelsOf(block.handle?.edytor, 'media', mediaEnglish).fileSize(size),
|
|
133
|
+
uploading: false,
|
|
134
|
+
progress: null,
|
|
123
135
|
link: mediaLink({ draggable: 'false', download: name })
|
|
124
136
|
}}
|
|
125
137
|
/>
|
|
126
138
|
</div>
|
|
139
|
+
{:else if !src && pending?.status === 'uploading' && !emptyHosts.has(block.handle!.edytor)}
|
|
140
|
+
<!-- An upload with no empty markup to draw it: the file's name and size at once, its progress. -->
|
|
141
|
+
<div use:block.void data-edytor-media-empty>
|
|
142
|
+
{#if block.type === 'file' && pending.preview}
|
|
143
|
+
<Control
|
|
144
|
+
own={rowOf}
|
|
145
|
+
{initial}
|
|
146
|
+
payload={{
|
|
147
|
+
block: block.handle,
|
|
148
|
+
src: pending.preview,
|
|
149
|
+
name: pending.name,
|
|
150
|
+
size: pending.size,
|
|
151
|
+
sizeLabel: labelsOf(block.handle?.edytor, 'media', mediaEnglish).fileSize(pending.size),
|
|
152
|
+
uploading: true,
|
|
153
|
+
progress: pending.progress,
|
|
154
|
+
link: mediaLink({ draggable: 'false', download: pending.name })
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
{:else}
|
|
158
|
+
<UploadPreview upload={pending} />
|
|
159
|
+
{/if}
|
|
160
|
+
</div>
|
|
127
161
|
{:else if src && block.type === 'video'}
|
|
128
162
|
<div use:block.void data-edytor-video>
|
|
129
163
|
<!-- svelte-ignore a11y_media_has_caption -->
|
|
@@ -155,10 +189,12 @@
|
|
|
155
189
|
{/snippet}
|
|
156
190
|
|
|
157
191
|
<!-- A file: its name and size, a link downloading it. -->
|
|
158
|
-
{#snippet initial({ src, name, sizeLabel, link }: FileRowPayload)}
|
|
192
|
+
{#snippet initial({ src, name, sizeLabel, link, uploading, progress }: FileRowPayload)}
|
|
159
193
|
<a href={src} target="_blank" rel={LINK_REL} draggable="false" download={name} {@attach link}>
|
|
160
194
|
<span aria-hidden="true">📎</span>
|
|
161
195
|
<span data-edytor-file-name>{name}</span>
|
|
162
196
|
{#if sizeLabel}<span data-edytor-file-size>{sizeLabel}</span>{/if}
|
|
163
197
|
</a>
|
|
198
|
+
{#if uploading}<progress data-edytor-file-progress value={progress ?? undefined} max="1"
|
|
199
|
+
></progress>{/if}
|
|
164
200
|
{/snippet}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BlockDefinition, Plugin } from '../../plugins.js';
|
|
2
|
+
import { type Uploader } from '../uploads.svelte.js';
|
|
2
3
|
import { type MediaView } from './media.js';
|
|
3
4
|
type SourceKind = 'video' | 'audio' | 'file';
|
|
4
5
|
/**
|
|
@@ -6,13 +7,13 @@ type SourceKind = 'video' | 'audio' | 'file';
|
|
|
6
7
|
* attachment; Notion's): a media kind (`mediaPlugin`) drawn by this
|
|
7
8
|
* snippet, a link or an uploaded file stored as one data write (a file's
|
|
8
9
|
* whole data, its source, name and size; a video's or audio track's
|
|
9
|
-
* `src`), and with an `upload
|
|
10
|
-
* (`media.files
|
|
10
|
+
* `src`), and with an `upload` (its uploader), the pasted, dropped and
|
|
11
|
+
* picked files it takes (`media.files`). A video or an audio track
|
|
11
12
|
* exports as its tag in a `figure` and imports from one (bare, or in a
|
|
12
13
|
* `figure`) with a safe `src` or `source`; the file kind brings its own
|
|
13
14
|
* (`record`), and its `row` markup (`own`).
|
|
14
15
|
*/
|
|
15
|
-
export declare const sourcePlugin: (kind: SourceKind, keywords: string[], record?: Partial<BlockDefinition>, own?: Pick<MediaView, "row"
|
|
16
|
+
export declare const sourcePlugin: (kind: SourceKind, keywords: string[], record?: Partial<BlockDefinition>, own?: Pick<MediaView, "row">, upload?: Uploader) => Plugin;
|
|
16
17
|
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> {
|
|
17
18
|
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
18
19
|
$$bindings?: Bindings;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import './upload.css';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { getContext } from 'svelte';
|
|
7
|
+
import type { Edytor } from '../../edytor.svelte.js';
|
|
8
|
+
import type { FileUpload } from '../uploads.svelte.js';
|
|
9
|
+
import { labelsOf, mediaEnglish } from '../../labels.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A file uploading, the plugin's own markup when the view has no empty
|
|
13
|
+
* markup to draw it: an image at once from its object URL (this view's
|
|
14
|
+
* only, never stored) with the progress over it; another file's name and
|
|
15
|
+
* size, then the progress (indeterminate until `upload` reports one).
|
|
16
|
+
*/
|
|
17
|
+
let {
|
|
18
|
+
upload,
|
|
19
|
+
image = false
|
|
20
|
+
}: { upload: Extract<FileUpload, { status: 'uploading' }>; image?: boolean } = $props();
|
|
21
|
+
const labels = labelsOf(getContext<Edytor>('edytor'), 'media', mediaEnglish);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div data-edytor-upload data-edytor-upload-image={image ? '' : undefined} role="status">
|
|
25
|
+
{#if image && upload.preview}
|
|
26
|
+
<img src={upload.preview} alt={upload.name} draggable="false" />
|
|
27
|
+
{:else}
|
|
28
|
+
<span data-edytor-file-name>{upload.name}</span>
|
|
29
|
+
<span data-edytor-file-size>{labels.fileSize(upload.size)}</span>
|
|
30
|
+
{/if}
|
|
31
|
+
<progress value={upload.progress ?? undefined} max="1" aria-label={labels.uploading}></progress>
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import './upload.css';
|
|
2
|
+
import type { FileUpload } from '../uploads.svelte.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
upload: Extract<FileUpload, {
|
|
5
|
+
status: 'uploading';
|
|
6
|
+
}>;
|
|
7
|
+
image?: boolean;
|
|
8
|
+
};
|
|
9
|
+
declare const UploadPreview: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type UploadPreview = ReturnType<typeof UploadPreview>;
|
|
11
|
+
export default UploadPreview;
|
|
@@ -1,6 +1,17 @@
|
|
|
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 VideoPluginOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* Upload a video file and answer its URL: an empty block's Upload, and
|
|
9
|
+
* the pasted and dropped video 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, videos are only linked.
|
|
12
|
+
*/
|
|
13
|
+
upload?: Uploader;
|
|
14
|
+
};
|
|
4
15
|
|
|
5
16
|
/**
|
|
6
17
|
* Notion's video block: a video file (an `http(s)` link, or an upload
|
|
@@ -8,5 +19,6 @@
|
|
|
8
19
|
* A YouTube or Vimeo link is an embed (`createEmbedPlugin()`). Void: its only
|
|
9
20
|
* text is the caption.
|
|
10
21
|
*/
|
|
11
|
-
export const createVideoPlugin = (): Plugin =>
|
|
22
|
+
export const createVideoPlugin = ({ upload }: VideoPluginOptions = {}): Plugin =>
|
|
23
|
+
sourcePlugin('video', ['movie', 'mp4', 'clip'], undefined, {}, upload);
|
|
12
24
|
</script>
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import type { Plugin } from '../../plugins.js';
|
|
2
|
+
import type { Uploader } from '../uploads.svelte.js';
|
|
3
|
+
export type VideoPluginOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Upload a video file and answer its URL: an empty block's Upload, and
|
|
6
|
+
* the pasted and dropped video 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, videos are only linked.
|
|
9
|
+
*/
|
|
10
|
+
upload?: Uploader;
|
|
11
|
+
};
|
|
2
12
|
/**
|
|
3
13
|
* Notion's video block: a video file (an `http(s)` link, or an upload
|
|
4
14
|
* through `upload`) in the browser's player, with an editable caption.
|
|
5
15
|
* A YouTube or Vimeo link is an embed (`createEmbedPlugin()`). Void: its only
|
|
6
16
|
* text is the caption.
|
|
7
17
|
*/
|
|
8
|
-
export declare const createVideoPlugin: () => Plugin;
|
|
18
|
+
export declare const createVideoPlugin: ({ upload }?: VideoPluginOptions) => Plugin;
|
|
9
19
|
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> {
|
|
10
20
|
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
11
21
|
$$bindings?: Bindings;
|
|
@@ -52,16 +52,25 @@ export declare class MediaEmptyController {
|
|
|
52
52
|
* block). Show a passive placeholder then.
|
|
53
53
|
*/
|
|
54
54
|
get readonly(): boolean;
|
|
55
|
-
/** The
|
|
55
|
+
/** The kind's plugin was given an `upload`: offer Upload. */
|
|
56
56
|
get canUpload(): boolean;
|
|
57
57
|
/** The upload of a pasted or dropped file into this block, if any (its `status`, `progress`, `preview`). */
|
|
58
58
|
get pending(): FileUpload | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* The file uploading into this block, shown at once: its object URL
|
|
61
|
+
* (this view's only, never stored; revoked once the upload ends), else `null`.
|
|
62
|
+
*/
|
|
63
|
+
get preview(): string | null;
|
|
64
|
+
/** The share of the upload sent, `0` to `1`; `null` before a report, or with no upload. */
|
|
65
|
+
get progress(): number | null;
|
|
59
66
|
/** Embed `value` (default the `draft`): the block takes the link, or `failed` says why not. */
|
|
60
67
|
embed: (value?: string) => void;
|
|
61
68
|
/**
|
|
62
|
-
* Upload `file` with
|
|
63
|
-
*
|
|
64
|
-
*
|
|
69
|
+
* Upload `file` with its kind's uploader (its plugin's `upload`), the
|
|
70
|
+
* block showing it meanwhile (`pending`, `preview`, `progress`), and
|
|
71
|
+
* store the URL it answers, only on the block as it was picked from:
|
|
72
|
+
* still in the tree, of its kind and empty (a peer may have set a
|
|
73
|
+
* source, or deleted it, meanwhile).
|
|
65
74
|
*/
|
|
66
75
|
upload: (picked: File) => Promise<void>;
|
|
67
76
|
/** The link field (`{@attach empty.field}`): Enter embeds its `draft`. */
|
|
@@ -54,7 +54,7 @@ export class MediaEmptyController {
|
|
|
54
54
|
get readonly() {
|
|
55
55
|
return !this.block || this.block.edytor.readonly;
|
|
56
56
|
}
|
|
57
|
-
/** The
|
|
57
|
+
/** The kind's plugin was given an `upload`: offer Upload. */
|
|
58
58
|
get canUpload() {
|
|
59
59
|
const { block } = this;
|
|
60
60
|
return Boolean(block && fileUploads(block.edytor) && block.definition.upload);
|
|
@@ -64,6 +64,19 @@ export class MediaEmptyController {
|
|
|
64
64
|
const { block } = this;
|
|
65
65
|
return block ? fileUploads(block.edytor)?.of(block.id) : undefined;
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The file uploading into this block, shown at once: its object URL
|
|
69
|
+
* (this view's only, never stored; revoked once the upload ends), else `null`.
|
|
70
|
+
*/
|
|
71
|
+
get preview() {
|
|
72
|
+
const { pending } = this;
|
|
73
|
+
return pending?.status === 'uploading' ? pending.preview : null;
|
|
74
|
+
}
|
|
75
|
+
/** The share of the upload sent, `0` to `1`; `null` before a report, or with no upload. */
|
|
76
|
+
get progress() {
|
|
77
|
+
const { pending } = this;
|
|
78
|
+
return pending?.status === 'uploading' ? pending.progress : null;
|
|
79
|
+
}
|
|
67
80
|
/** Store `value` on the block: why it did not take it, or `null`. */
|
|
68
81
|
#store = (value, file) => this.block && this.view ? this.view.store(this.block, value, file) : 'invalid';
|
|
69
82
|
/** Embed `value` (default the `draft`): the block takes the link, or `failed` says why not. */
|
|
@@ -73,30 +86,28 @@ export class MediaEmptyController {
|
|
|
73
86
|
fileUploads(this.block.edytor)?.clear(this.block.id);
|
|
74
87
|
};
|
|
75
88
|
/**
|
|
76
|
-
* Upload `file` with
|
|
77
|
-
*
|
|
78
|
-
*
|
|
89
|
+
* Upload `file` with its kind's uploader (its plugin's `upload`), the
|
|
90
|
+
* block showing it meanwhile (`pending`, `preview`, `progress`), and
|
|
91
|
+
* store the URL it answers, only on the block as it was picked from:
|
|
92
|
+
* still in the tree, of its kind and empty (a peer may have set a
|
|
93
|
+
* source, or deleted it, meanwhile).
|
|
79
94
|
*/
|
|
80
95
|
upload = async (picked) => {
|
|
81
96
|
const { block } = this;
|
|
82
|
-
const
|
|
83
|
-
if (!
|
|
97
|
+
const uploads = this.canUpload && fileUploads(block?.edytor);
|
|
98
|
+
if (!uploads || !block || block.edytor.readonly)
|
|
84
99
|
return;
|
|
85
100
|
const { edytor, id, type } = block;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
const stored = await uploads.pick(id, picked, (src) => {
|
|
102
|
+
const live = edytor.idToBlock.get(id);
|
|
103
|
+
if (!live?.isInTree || live.type !== type || edytor.readonly)
|
|
104
|
+
return;
|
|
105
|
+
if (edytor.facade.blockDataOf(id)?.src !== undefined)
|
|
106
|
+
return;
|
|
107
|
+
this.failed = this.#store(src, picked);
|
|
108
|
+
});
|
|
109
|
+
if (!stored)
|
|
91
110
|
this.failed = 'upload';
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const live = edytor.idToBlock.get(id);
|
|
95
|
-
if (!live?.isInTree || live.type !== type || edytor.readonly)
|
|
96
|
-
return;
|
|
97
|
-
if (edytor.facade.blockDataOf(id)?.src !== undefined)
|
|
98
|
-
return;
|
|
99
|
-
this.failed = this.#store(src, picked);
|
|
100
111
|
};
|
|
101
112
|
/** The link field (`{@attach empty.field}`): Enter embeds its `draft`. */
|
|
102
113
|
field = (node) => {
|
|
@@ -49,6 +49,13 @@ export type FileRowPayload = {
|
|
|
49
49
|
size: number | undefined;
|
|
50
50
|
/** That size in the view's words (`media.fileSize`), `''` without one. */
|
|
51
51
|
sizeLabel: string;
|
|
52
|
+
/**
|
|
53
|
+
* The file is uploading (then `src` is its object URL, this view's only,
|
|
54
|
+
* never stored): the row shows it at once, with `progress`.
|
|
55
|
+
*/
|
|
56
|
+
uploading: boolean;
|
|
57
|
+
/** The share sent while it uploads, `0` to `1`; `null` before a report or once stored. */
|
|
58
|
+
progress: number | null;
|
|
52
59
|
/**
|
|
53
60
|
* The row's link (`<a href={row.src} {@attach row.link}>`): it downloads
|
|
54
61
|
* the file under its name, in a new tab without the opener, never dragged
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* Loaded by the preview's module: a file uploading, drawn by the plugin when
|
|
2
|
+
the view has no empty markup (`createMediaEmptyPlugin`). */
|
|
3
|
+
[data-edytor-upload] {
|
|
4
|
+
display: flex;
|
|
5
|
+
position: relative;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: 6px;
|
|
8
|
+
}
|
|
9
|
+
[data-edytor-upload] img {
|
|
10
|
+
display: block;
|
|
11
|
+
max-width: 100%;
|
|
12
|
+
}
|
|
13
|
+
[data-edytor-upload] progress {
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 4px;
|
|
16
|
+
}
|
|
17
|
+
[data-edytor-upload-image] progress {
|
|
18
|
+
position: absolute;
|
|
19
|
+
right: 8px;
|
|
20
|
+
bottom: 8px;
|
|
21
|
+
left: 8px;
|
|
22
|
+
width: auto;
|
|
23
|
+
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
<svelte:options runes />
|
|
2
2
|
|
|
3
3
|
<script module lang="ts">
|
|
4
|
+
import './mention.css';
|
|
4
5
|
import type { Snippet } from 'svelte';
|
|
5
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
InlineBlockDefinition,
|
|
8
|
+
InlineBlockSnippetPayload,
|
|
9
|
+
InlineBlockView,
|
|
10
|
+
Plugin,
|
|
11
|
+
TriggerContext
|
|
12
|
+
} from '../../plugins.js';
|
|
6
13
|
import type { TriggerMenuController } from '../triggers/TriggerController.svelte.js';
|
|
7
14
|
import { withRulesAndTriggers } from '../triggers/triggers.js';
|
|
8
15
|
|
|
@@ -18,7 +25,7 @@
|
|
|
18
25
|
avatar?: string;
|
|
19
26
|
};
|
|
20
27
|
|
|
21
|
-
/** What a
|
|
28
|
+
/** What a mention atom stores (whatever its kind). */
|
|
22
29
|
export type MentionData = { id: string; label: string };
|
|
23
30
|
|
|
24
31
|
export type MentionPluginOptions = {
|
|
@@ -27,8 +34,15 @@
|
|
|
27
34
|
query: string,
|
|
28
35
|
ctx: TriggerContext
|
|
29
36
|
) => readonly MentionItem[] | Promise<readonly MentionItem[]>;
|
|
30
|
-
/** The character that opens the menu (default `@`). */
|
|
37
|
+
/** The character that opens the menu, and that the atom shows before its label (default `@`). */
|
|
31
38
|
char?: string;
|
|
39
|
+
/**
|
|
40
|
+
* The inline kind the atoms are (default `mention`), and the name of the
|
|
41
|
+
* trigger menu (`controller.trigger.name`). List the plugin once per kind
|
|
42
|
+
* to mention several things (`@` people, `$` tickers as `ticker`); two
|
|
43
|
+
* instances of one kind: the first listed defines it.
|
|
44
|
+
*/
|
|
45
|
+
kind?: string;
|
|
32
46
|
/**
|
|
33
47
|
* The menu's markup and its words; it renders while `controller.isOpen`,
|
|
34
48
|
* at the caret (without it, nothing shows). Keep its ARIA with `{@attach
|
|
@@ -38,40 +52,71 @@
|
|
|
38
52
|
menu?: Snippet<[TriggerMenuController<MentionItem>]>;
|
|
39
53
|
};
|
|
40
54
|
|
|
55
|
+
/** A mention kind's record: its `char` read by the atom's markup. */
|
|
56
|
+
type MentionKind = InlineBlockDefinition & { char: string };
|
|
57
|
+
|
|
58
|
+
/** The char of the atom's kind (the first listed instance's); `@` with no view (a suggestion). */
|
|
59
|
+
const charOf = (atom: InlineBlockView) =>
|
|
60
|
+
(atom.handle?.edytor.inlineBlocks.get(atom.type) as Partial<MentionKind> | undefined)?.char ??
|
|
61
|
+
'@';
|
|
62
|
+
|
|
63
|
+
const escape = (value: string) =>
|
|
64
|
+
value.replace(
|
|
65
|
+
/[&<>"]/g,
|
|
66
|
+
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]!
|
|
67
|
+
);
|
|
68
|
+
|
|
41
69
|
/**
|
|
42
70
|
* People mentions, as in Notion: `@` opens a menu of the people your app's
|
|
43
71
|
* `items(query)` answers; picking one replaces `@query` with a `mention`
|
|
44
72
|
* atom (`{ id, label }`) and puts the caret after it, in one undo step.
|
|
73
|
+
* `char` and `kind` make another one (`$` tickers as `ticker` atoms).
|
|
45
74
|
*/
|
|
46
|
-
export const createMentionPlugin =
|
|
47
|
-
|
|
48
|
-
|
|
75
|
+
export const createMentionPlugin = (options: MentionPluginOptions): Plugin => {
|
|
76
|
+
const char = options.char ?? '@';
|
|
77
|
+
const kind = options.kind ?? 'mention';
|
|
78
|
+
const labelOf = (data: unknown) => {
|
|
79
|
+
const label = (data as Partial<MentionData> | undefined)?.label;
|
|
80
|
+
return typeof label === 'string' ? label : '';
|
|
81
|
+
};
|
|
82
|
+
const atom: MentionKind = {
|
|
83
|
+
snippet: mention,
|
|
84
|
+
char,
|
|
85
|
+
plain: (data) => char + labelOf(data),
|
|
86
|
+
html: (data) => {
|
|
87
|
+
const id = (data as Partial<MentionData> | undefined)?.id;
|
|
88
|
+
return (
|
|
89
|
+
`<span data-edytor-mention="${escape(kind)}"` +
|
|
90
|
+
(typeof id === 'string' ? ` data-mention-id="${escape(id)}"` : '') +
|
|
91
|
+
`>${escape(char + labelOf(data))}</span>`
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
parse: (element) => {
|
|
95
|
+
// A bare `data-edytor-mention` (an older export) is a `mention`.
|
|
96
|
+
const own = element.dataset.edytorMention;
|
|
97
|
+
if (own === undefined || (own || 'mention') !== kind) return undefined;
|
|
98
|
+
const text = element.textContent ?? '';
|
|
99
|
+
const label = text.startsWith(char) ? text.slice(char.length) : text;
|
|
100
|
+
return { id: element.dataset.mentionId ?? '', label };
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return (edytor) =>
|
|
49
104
|
withRulesAndTriggers(
|
|
50
105
|
edytor,
|
|
51
|
-
{
|
|
52
|
-
inlineBlocks: {
|
|
53
|
-
mention: {
|
|
54
|
-
snippet: mention,
|
|
55
|
-
plain: (data) => {
|
|
56
|
-
const label = (data as Partial<MentionData> | undefined)?.label;
|
|
57
|
-
return typeof label === 'string' && label ? `@${label}` : '@';
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
},
|
|
106
|
+
{ inlineBlocks: { [kind]: atom } },
|
|
62
107
|
{
|
|
63
108
|
triggers: [
|
|
64
109
|
{
|
|
65
|
-
char
|
|
110
|
+
char,
|
|
111
|
+
name: kind,
|
|
66
112
|
items: (query, ctx) => options.items(query, ctx),
|
|
67
|
-
label: (
|
|
68
|
-
key: (
|
|
113
|
+
label: (item: MentionItem) => item.label,
|
|
114
|
+
key: (item: MentionItem) => item.id,
|
|
69
115
|
menu: options.menu,
|
|
70
|
-
onPick: (
|
|
71
|
-
const data: MentionData = { id:
|
|
116
|
+
onPick: (item: MentionItem, { edytor, block, from, caret }) => {
|
|
117
|
+
const data: MentionData = { id: item.id, label: item.label };
|
|
72
118
|
const written =
|
|
73
|
-
edytor.insertInline(block.id, from, { type:
|
|
74
|
-
'applied';
|
|
119
|
+
edytor.insertInline(block.id, from, { type: kind, data }).status === 'applied';
|
|
75
120
|
if (written) caret(from + 1);
|
|
76
121
|
return written;
|
|
77
122
|
}
|
|
@@ -79,20 +124,14 @@
|
|
|
79
124
|
]
|
|
80
125
|
}
|
|
81
126
|
);
|
|
127
|
+
};
|
|
82
128
|
</script>
|
|
83
129
|
|
|
84
130
|
{#snippet mention({ block }: InlineBlockSnippetPayload<Partial<MentionData>>)}
|
|
85
131
|
<span
|
|
86
132
|
class="edytor-mention"
|
|
87
133
|
data-selected={block.selected ? 'true' : undefined}
|
|
88
|
-
data-edytor-mention
|
|
89
|
-
data-mention-id={block.data.id}
|
|
134
|
+
data-edytor-mention={block.type}
|
|
135
|
+
data-mention-id={block.data.id}>{charOf(block)}{block.data.label ?? ''}</span
|
|
90
136
|
>
|
|
91
137
|
{/snippet}
|
|
92
|
-
|
|
93
|
-
<style>
|
|
94
|
-
/* One unit, on one line; its look is a theme's. */
|
|
95
|
-
.edytor-mention {
|
|
96
|
-
white-space: nowrap;
|
|
97
|
-
}
|
|
98
|
-
</style>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import './mention.css';
|
|
1
2
|
import type { Snippet } from 'svelte';
|
|
2
3
|
import type { Plugin, TriggerContext } from '../../plugins.js';
|
|
3
4
|
import type { TriggerMenuController } from '../triggers/TriggerController.svelte.js';
|
|
@@ -12,7 +13,7 @@ export type MentionItem = {
|
|
|
12
13
|
/** An avatar image URL (http, https or data). */
|
|
13
14
|
avatar?: string;
|
|
14
15
|
};
|
|
15
|
-
/** What a
|
|
16
|
+
/** What a mention atom stores (whatever its kind). */
|
|
16
17
|
export type MentionData = {
|
|
17
18
|
id: string;
|
|
18
19
|
label: string;
|
|
@@ -20,8 +21,15 @@ export type MentionData = {
|
|
|
20
21
|
export type MentionPluginOptions = {
|
|
21
22
|
/** The people matching `query` (the text typed after `@`), at once or as a promise. */
|
|
22
23
|
items: (query: string, ctx: TriggerContext) => readonly MentionItem[] | Promise<readonly MentionItem[]>;
|
|
23
|
-
/** The character that opens the menu (default `@`). */
|
|
24
|
+
/** The character that opens the menu, and that the atom shows before its label (default `@`). */
|
|
24
25
|
char?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The inline kind the atoms are (default `mention`), and the name of the
|
|
28
|
+
* trigger menu (`controller.trigger.name`). List the plugin once per kind
|
|
29
|
+
* to mention several things (`@` people, `$` tickers as `ticker`); two
|
|
30
|
+
* instances of one kind: the first listed defines it.
|
|
31
|
+
*/
|
|
32
|
+
kind?: string;
|
|
25
33
|
/**
|
|
26
34
|
* The menu's markup and its words; it renders while `controller.isOpen`,
|
|
27
35
|
* at the caret (without it, nothing shows). Keep its ARIA with `{@attach
|
|
@@ -34,6 +42,7 @@ export type MentionPluginOptions = {
|
|
|
34
42
|
* People mentions, as in Notion: `@` opens a menu of the people your app's
|
|
35
43
|
* `items(query)` answers; picking one replaces `@query` with a `mention`
|
|
36
44
|
* atom (`{ id, label }`) and puts the caret after it, in one undo step.
|
|
45
|
+
* `char` and `kind` make another one (`$` tickers as `ticker` atoms).
|
|
37
46
|
*/
|
|
38
47
|
export declare const createMentionPlugin: (options: MentionPluginOptions) => Plugin;
|
|
39
48
|
declare const MentionPlugin: import("svelte").Component<Record<string, never>, {}, "">;
|