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.
Files changed (116) hide show
  1. package/README.md +14 -72
  2. package/dist/block/block.svelte.d.ts +6 -4
  3. package/dist/block/block.svelte.js +10 -4
  4. package/dist/block/inlineBlock.svelte.d.ts +6 -2
  5. package/dist/block/inlineBlock.svelte.js +7 -2
  6. package/dist/cloudflare/room/context.d.ts +6 -0
  7. package/dist/collaboration/awarenessSelection.d.ts +22 -1
  8. package/dist/collaboration/awarenessSelection.js +42 -0
  9. package/dist/components/Block.svelte +3 -1
  10. package/dist/components/Edytor.svelte +36 -10
  11. package/dist/components/Edytor.svelte.d.ts +8 -1
  12. package/dist/components/InlineBlock.svelte +1 -1
  13. package/dist/crdt/data.d.ts +10 -0
  14. package/dist/crdt/data.js +20 -2
  15. package/dist/crdt/doc/reads.d.ts +7 -0
  16. package/dist/crdt/doc/reads.js +11 -1
  17. package/dist/crdt/document.d.ts +8 -0
  18. package/dist/crdt/document.js +12 -0
  19. package/dist/crdt/edytor-doc.d.ts +18 -0
  20. package/dist/crdt/edytor-doc.js +2 -1
  21. package/dist/crdt/index.d.ts +6 -0
  22. package/dist/crdt/operations.d.ts +1 -1
  23. package/dist/crdt/operations.js +1 -0
  24. package/dist/crdt/props.d.ts +32 -0
  25. package/dist/crdt/props.js +172 -0
  26. package/dist/{plugins/blockHandles → dnd}/dropIndicator.d.ts +2 -2
  27. package/dist/{plugins/blockHandles → dnd}/geometry.d.ts +2 -2
  28. package/dist/dnd/placement.d.ts +200 -0
  29. package/dist/dnd/placement.js +426 -0
  30. package/dist/edytor.svelte.d.ts +17 -4
  31. package/dist/edytor.svelte.js +16 -7
  32. package/dist/events/beforeInputCommands.d.ts +2 -2
  33. package/dist/events/beforeInputCommands.js +8 -4
  34. package/dist/events/onDrop.d.ts +6 -0
  35. package/dist/events/onDrop.js +38 -10
  36. package/dist/events/onPaste.d.ts +2 -1
  37. package/dist/events/onPaste.js +8 -3
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.js +1 -1
  40. package/dist/labels.d.ts +2 -0
  41. package/dist/labels.js +2 -1
  42. package/dist/plugins/arrowMove/arrowMove.d.ts +1 -1
  43. package/dist/plugins/arrowMove/arrowMove.js +1 -1
  44. package/dist/plugins/blockHandles/BlockHandleController.svelte.d.ts +2 -122
  45. package/dist/plugins/blockHandles/BlockHandleController.svelte.js +29 -387
  46. package/dist/plugins/blockHandles/BlockHandles.svelte +14 -32
  47. package/dist/plugins/blockHandles/blockHandlesPlugin.js +2 -1
  48. package/dist/plugins/code/CodeHeader.svelte +5 -3
  49. package/dist/plugins/code/CodePlugin.svelte +3 -3
  50. package/dist/plugins/code/languages.d.ts +0 -3
  51. package/dist/plugins/code/languages.js +0 -2
  52. package/dist/plugins/columns/ColumnResize.svelte +3 -2
  53. package/dist/plugins/columns/ColumnsPlugin.svelte +1 -1
  54. package/dist/plugins/columns/gaps.d.ts +7 -5
  55. package/dist/plugins/columns/gaps.js +11 -6
  56. package/dist/plugins/columns/resize.svelte.d.ts +3 -3
  57. package/dist/plugins/columns/resize.svelte.js +11 -10
  58. package/dist/plugins/equation/EquationPlugin.svelte +1 -8
  59. package/dist/plugins/equation/EquationPlugin.svelte.d.ts +1 -0
  60. package/dist/plugins/equation/equation.css +7 -0
  61. package/dist/plugins/fileDrop.d.ts +28 -0
  62. package/dist/plugins/fileDrop.js +116 -0
  63. package/dist/plugins/image/ImagePlugin.svelte +42 -33
  64. package/dist/plugins/image/ImagePlugin.svelte.d.ts +13 -1
  65. package/dist/plugins/image/image.css +17 -0
  66. package/dist/plugins/index.d.ts +3 -4
  67. package/dist/plugins/index.js +0 -1
  68. package/dist/plugins/markdownShortcuts.js +5 -5
  69. package/dist/plugins/media/AudioPlugin.svelte +13 -1
  70. package/dist/plugins/media/AudioPlugin.svelte.d.ts +11 -1
  71. package/dist/plugins/media/FilePlugin.svelte +11 -1
  72. package/dist/plugins/media/FilePlugin.svelte.d.ts +9 -0
  73. package/dist/plugins/media/SourceBlock.svelte +48 -12
  74. package/dist/plugins/media/SourceBlock.svelte.d.ts +4 -3
  75. package/dist/plugins/media/UploadPreview.svelte +32 -0
  76. package/dist/plugins/media/UploadPreview.svelte.d.ts +11 -0
  77. package/dist/plugins/media/VideoPlugin.svelte +13 -1
  78. package/dist/plugins/media/VideoPlugin.svelte.d.ts +11 -1
  79. package/dist/plugins/media/empty.svelte.d.ts +13 -4
  80. package/dist/plugins/media/empty.svelte.js +30 -19
  81. package/dist/plugins/media/media.d.ts +7 -0
  82. package/dist/plugins/media/upload.css +23 -0
  83. package/dist/plugins/mention/MentionPlugin.svelte +72 -33
  84. package/dist/plugins/mention/MentionPlugin.svelte.d.ts +11 -2
  85. package/dist/plugins/mention/mention.css +6 -0
  86. package/dist/plugins/pageLink/PageLinkPlugin.svelte +122 -17
  87. package/dist/plugins/pageLink/PageLinkPlugin.svelte.d.ts +13 -1
  88. package/dist/plugins/pageLink/pageLink.css +6 -0
  89. package/dist/plugins/richtext/RichTextPlugin.svelte +14 -7
  90. package/dist/plugins/richtext/RichTextPlugin.svelte.d.ts +1 -0
  91. package/dist/plugins/richtext/richtext.css +6 -0
  92. package/dist/plugins/table/chrome.svelte.js +4 -3
  93. package/dist/plugins/uploads.svelte.d.ts +45 -32
  94. package/dist/plugins/uploads.svelte.js +105 -39
  95. package/dist/plugins.d.ts +40 -14
  96. package/dist/session/attempt.d.ts +16 -6
  97. package/dist/session/attempt.js +1 -0
  98. package/dist/session/bindings.js +34 -31
  99. package/dist/session/commands.js +2 -0
  100. package/dist/session/history.js +1 -0
  101. package/dist/session/inputRules.d.ts +6 -3
  102. package/dist/session/inputRules.js +9 -5
  103. package/dist/session/moves.d.ts +33 -0
  104. package/dist/session/moves.js +71 -1
  105. package/dist/session/navigation.d.ts +7 -0
  106. package/dist/session/navigation.js +19 -10
  107. package/dist/session/props.d.ts +28 -0
  108. package/dist/session/props.js +65 -0
  109. package/dist/session/uploads.d.ts +29 -0
  110. package/dist/session/uploads.js +10 -0
  111. package/dist/themes/notion.css +9 -0
  112. package/package.json +1 -1
  113. /package/dist/{plugins/blockHandles → dnd}/dropIndicator.js +0 -0
  114. /package/dist/{plugins/blockHandles → dnd}/geometry.js +0 -0
  115. /package/dist/{plugins/columns → dnd}/stacking.d.ts +0 -0
  116. /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 the gap's left part, above
7
- * the block handles (their `+` and grip together right of it, `gaps.ts`);
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 is the gap's left part at every height.
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`: its left part, `BAND` px (the gap's width when
23
- * narrower) from the column before it, where the guide shows. The handle of
24
- * a column's block takes the rest (`handleSpan`): its `+` and grip together
25
- * flush with the block (Notion's "+ ⋮⋮"), so at the default 46px gap
26
- * (`--edytor-columns-gap`: 10 + 18 + 18) neither is under the band.
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`: its left part, `BAND` px (the gap's width when
31
- * narrower) from the column before it, where the guide shows. The handle of
32
- * a column's block takes the rest (`handleSpan`): its `+` and grip together
33
- * flush with the block (Notion's "+ ⋮⋮"), so at the default 46px gap
34
- * (`--edytor-columns-gap`: 10 + 18 + 18) neither is under the band.
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 }) => ({ left, width: Math.min(BAND, right - left) });
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`: the gap's left
44
- * `BAND` px, the layout's height), above the block handles, whose `+` and
45
- * grip sit together right of it, flush with the block (`gaps.ts`, one
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 './stacking.js';
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`: the gap's left
17
- * `BAND` px, the layout's height), above the block handles, whose `+` and
18
- * grip sit together right of it, flush with the block (`gaps.ts`, one
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, never the gap's (it would jump onto the `+`).
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: ({ node }) => {
75
- if (!owns()) return;
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: with the uploads plugin, an Upload and pasted or dropped files.
105
- upload: {
106
- accepts: isImageFile,
107
- data: (_file, answer) => {
108
- const src = storableImageSrc(answer);
109
- return src ? { src } : null;
110
- },
111
- filled: (data) => safeImageSrc(data.src) !== null,
112
- preview: true
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 branded('image', plugin);
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
+ }
@@ -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';
@@ -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 - before.length + found.start;
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 }), ...(found.closing
98
- ? [facade.prepare.deleteText(id, at + found.open + found.content, found.closing)]
99
- : []), facade.prepare.deleteText(id, at, found.open));
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 => sourcePlugin('audio', ['sound', 'music', 'mp3']);
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>