edytor 2.0.0 → 3.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 (222) hide show
  1. package/README.md +19 -4
  2. package/dist/block/block.svelte.js +2 -4
  3. package/dist/clipboard/htmlFlow.d.ts +20 -7
  4. package/dist/clipboard/htmlFlow.js +23 -15
  5. package/dist/clipboard/serializeClipboardFragment.d.ts +37 -4
  6. package/dist/clipboard/serializeClipboardFragment.js +6 -4
  7. package/dist/clipboard/textBlocks.d.ts +8 -0
  8. package/dist/clipboard/textBlocks.js +6 -2
  9. package/dist/cloudflare/room/admission.js +3 -3
  10. package/dist/cloudflare/room/context.d.ts +1 -1
  11. package/dist/cloudflare/routeDocumentSocket.d.ts +17 -0
  12. package/dist/cloudflare/routeDocumentSocket.js +53 -0
  13. package/dist/collaboration/comments/client.d.ts +1 -1
  14. package/dist/collaboration/comments/client.js +1 -1
  15. package/dist/collaboration/remoteSelection.js +3 -3
  16. package/dist/components/Edytor.svelte +2 -10
  17. package/dist/components/Edytor.svelte.d.ts +1 -3
  18. package/dist/crdt/admission.d.ts +5 -10
  19. package/dist/crdt/admission.js +5 -11
  20. package/dist/crdt/doc/funnel.d.ts +7 -6
  21. package/dist/crdt/doc/funnel.js +10 -9
  22. package/dist/crdt/doc/gate.d.ts +0 -17
  23. package/dist/crdt/doc/gate.js +2 -41
  24. package/dist/crdt/doc/history.js +2 -2
  25. package/dist/crdt/document.d.ts +5 -14
  26. package/dist/crdt/document.js +5 -19
  27. package/dist/crdt/edytor-doc.d.ts +4 -4
  28. package/dist/crdt/edytor-doc.js +3 -7
  29. package/dist/crdt/index.d.ts +3 -3
  30. package/dist/crdt/index.js +2 -2
  31. package/dist/crdt/protocol.d.ts +2 -2
  32. package/dist/crdt/protocol.js +2 -2
  33. package/dist/crdt/providers/index.d.ts +1 -1
  34. package/dist/crdt/providers/room.d.ts +10 -9
  35. package/dist/crdt/providers/room.js +13 -10
  36. package/dist/crdt/providers/websocket.d.ts +1 -1
  37. package/dist/crdt/providers/websocket.js +28 -29
  38. package/dist/crdt/semantics.js +3 -1
  39. package/dist/dnd/autoScroll.d.ts +15 -7
  40. package/dist/dnd/autoScroll.js +10 -15
  41. package/dist/dnd/dropIndicator.js +2 -5
  42. package/dist/dnd/external.d.ts +6 -0
  43. package/dist/dnd/external.js +17 -0
  44. package/dist/dnd/geometry.d.ts +2 -0
  45. package/dist/dnd/geometry.js +3 -4
  46. package/dist/dnd/placement.d.ts +1 -1
  47. package/dist/dnd/pragmatic.d.ts +3 -1
  48. package/dist/dnd/pragmatic.js +12 -2
  49. package/dist/dnd/vendor/pragmatic/UPSTREAM.md +51 -18
  50. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/drop-target-for-external.d.ts +4 -0
  51. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/drop-target-for-external.js +6 -0
  52. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/element-adapter.d.ts +0 -2
  53. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/element-adapter.js +4 -22
  54. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter-types.d.ts +26 -0
  55. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter.d.ts +30 -0
  56. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter.js +203 -0
  57. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/get-available-items.js +13 -0
  58. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/get-available-types.js +9 -0
  59. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/is-an-available-type.js +19 -0
  60. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/monitor-for-external.d.ts +4 -0
  61. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/monitor-for-external.js +6 -0
  62. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/honey-pot-fix/make-honey-pot-fix.js +4 -16
  63. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/internal-types.d.ts +1 -8
  64. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/make-adapter/make-drop-target.js +5 -6
  65. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/make-adapter/make-monitor.js +2 -3
  66. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/element/custom-native-drag-preview/set-custom-native-drag-preview.d.ts +1 -1
  67. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/element/custom-native-drag-preview/set-custom-native-drag-preview.js +6 -24
  68. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/contains-files.d.ts +2 -0
  69. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/contains-files.js +5 -0
  70. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/get-files.d.ts +3 -0
  71. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/get-files.js +12 -0
  72. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/native-types.d.ts +12 -0
  73. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/changing-window/is-entering-window-in-safari.js +10 -0
  74. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/changing-window/is-entering-window.js +55 -0
  75. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/entry-point/external.d.ts +4 -0
  76. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/entry-point/external.js +8 -0
  77. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/internal-types.d.ts +0 -4
  78. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/over-element/get-scroll-by.js +16 -72
  79. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/over-element/try-scroll.js +1 -9
  80. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/configuration.js +4 -29
  81. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/get-scroll-change.js +2 -3
  82. package/dist/edytor.svelte.d.ts +2 -5
  83. package/dist/edytor.svelte.js +17 -31
  84. package/dist/events/onBeforeInput.js +1 -1
  85. package/dist/events/onDrop.d.ts +0 -6
  86. package/dist/events/onDrop.js +10 -23
  87. package/dist/events/onInput.d.ts +0 -12
  88. package/dist/events/onInput.js +1 -67
  89. package/dist/export/forms.d.ts +627 -0
  90. package/dist/export/forms.js +749 -0
  91. package/dist/export/index.d.ts +170 -0
  92. package/dist/export/index.js +161 -0
  93. package/dist/export/markdown.d.ts +41 -0
  94. package/dist/export/markdown.js +585 -0
  95. package/dist/export/markdownWriter.d.ts +9 -0
  96. package/dist/export/markdownWriter.js +133 -0
  97. package/dist/export/mdx.d.ts +15 -0
  98. package/dist/export/mdx.js +33 -0
  99. package/dist/index.d.ts +3 -3
  100. package/dist/index.js +1 -1
  101. package/dist/kinds.js +1 -1
  102. package/dist/plugins/anchored.svelte.d.ts +21 -25
  103. package/dist/plugins/anchored.svelte.js +23 -39
  104. package/dist/plugins/blockHandles/BlockHandleController.svelte.js +24 -32
  105. package/dist/plugins/blockHandles/dragPreview.d.ts +26 -7
  106. package/dist/plugins/blockHandles/dragPreview.js +33 -0
  107. package/dist/plugins/blockMenu/BlockMenuController.svelte.d.ts +5 -0
  108. package/dist/plugins/blockMenu/BlockMenuController.svelte.js +7 -2
  109. package/dist/plugins/blockMenu/blockMenuPlugin.js +5 -50
  110. package/dist/plugins/chrome.d.ts +27 -5
  111. package/dist/plugins/chrome.js +24 -9
  112. package/dist/plugins/code/CodePlugin.svelte +12 -9
  113. package/dist/plugins/code/header.svelte.js +7 -11
  114. package/dist/plugins/code/languageMenu.svelte.d.ts +5 -2
  115. package/dist/plugins/code/languageMenu.svelte.js +13 -20
  116. package/dist/plugins/columns/ColumnsPlugin.svelte +7 -20
  117. package/dist/plugins/columns/resize.svelte.d.ts +2 -3
  118. package/dist/plugins/columns/resize.svelte.js +2 -5
  119. package/dist/plugins/comments/CommentsController.svelte.d.ts +6 -0
  120. package/dist/plugins/comments/CommentsController.svelte.js +20 -0
  121. package/dist/plugins/equation/EquationPlugin.svelte +12 -22
  122. package/dist/plugins/equation/equation.svelte.d.ts +7 -15
  123. package/dist/plugins/equation/equation.svelte.js +13 -50
  124. package/dist/plugins/equation/tex.d.ts +17 -0
  125. package/dist/plugins/equation/tex.js +42 -0
  126. package/dist/plugins/fileDrop.d.ts +38 -23
  127. package/dist/plugins/fileDrop.js +82 -76
  128. package/dist/plugins/find/FindController.svelte.d.ts +5 -0
  129. package/dist/plugins/find/FindController.svelte.js +14 -14
  130. package/dist/plugins/find/findPlugin.js +1 -15
  131. package/dist/plugins/image/ImageChrome.svelte +8 -2
  132. package/dist/plugins/image/ImagePlugin.svelte +15 -52
  133. package/dist/plugins/image/controls.svelte.js +16 -2
  134. package/dist/plugins/image/toolbar.svelte.d.ts +5 -4
  135. package/dist/plugins/image/toolbar.svelte.js +21 -31
  136. package/dist/plugins/index.d.ts +1 -1
  137. package/dist/plugins/list.svelte.d.ts +9 -0
  138. package/dist/plugins/list.svelte.js +18 -0
  139. package/dist/plugins/marquee/marqueePlugin.js +0 -4
  140. package/dist/plugins/media/AudioPlugin.svelte +1 -1
  141. package/dist/plugins/media/BookmarkPlugin.svelte +6 -97
  142. package/dist/plugins/media/EmbedPlugin.svelte +5 -90
  143. package/dist/plugins/media/EmptySlot.svelte +36 -20
  144. package/dist/plugins/media/EmptySlot.svelte.d.ts +6 -15
  145. package/dist/plugins/media/FilePlugin.svelte +1 -19
  146. package/dist/plugins/media/MediaEmpty.svelte +2 -55
  147. package/dist/plugins/media/MediaEmpty.svelte.d.ts +18 -14
  148. package/dist/plugins/media/SourceBlock.svelte +142 -52
  149. package/dist/plugins/media/SourceBlock.svelte.d.ts +10 -4
  150. package/dist/plugins/media/VideoPlugin.svelte +1 -1
  151. package/dist/plugins/media/empty.svelte.js +2 -7
  152. package/dist/plugins/media/media.d.ts +2 -63
  153. package/dist/plugins/media/media.js +1 -194
  154. package/dist/plugins/media/sources.d.ts +60 -0
  155. package/dist/plugins/media/sources.js +165 -0
  156. package/dist/plugins/media/urlPaste.svelte.d.ts +2 -1
  157. package/dist/plugins/media/urlPaste.svelte.js +12 -21
  158. package/dist/plugins/mention/MentionPlugin.svelte +8 -35
  159. package/dist/plugins/page/PagePlugin.svelte +2 -9
  160. package/dist/plugins/page/page.js +1 -1
  161. package/dist/plugins/pageLink/PageLinkPlugin.svelte +6 -13
  162. package/dist/plugins/richtext/RichTextPlugin.svelte +85 -197
  163. package/dist/plugins/richtext/body.js +1 -1
  164. package/dist/plugins/richtext/calloutIcons.svelte.d.ts +2 -2
  165. package/dist/plugins/richtext/calloutIcons.svelte.js +5 -7
  166. package/dist/plugins/richtext/richTextOperations.d.ts +1 -19
  167. package/dist/plugins/richtext/richTextOperations.js +3 -49
  168. package/dist/plugins/slashMenu/SlashMenuController.svelte.d.ts +2 -0
  169. package/dist/plugins/slashMenu/SlashMenuController.svelte.js +7 -0
  170. package/dist/plugins/slashMenu/slashMenuPlugin.js +5 -14
  171. package/dist/plugins/table/TableChrome.svelte +2 -3
  172. package/dist/plugins/table/TablePlugin.svelte +20 -54
  173. package/dist/plugins/table/chrome.svelte.d.ts +5 -2
  174. package/dist/plugins/table/chrome.svelte.js +30 -54
  175. package/dist/plugins/table/table.js +3 -3
  176. package/dist/plugins/toc/TocPlugin.svelte +2 -4
  177. package/dist/plugins/toolbar/ToolbarController.svelte.d.ts +7 -0
  178. package/dist/plugins/toolbar/ToolbarController.svelte.js +20 -13
  179. package/dist/plugins/toolbar/toolbarPlugin.js +19 -53
  180. package/dist/plugins/triggers/TriggerController.svelte.d.ts +7 -2
  181. package/dist/plugins/triggers/TriggerController.svelte.js +11 -4
  182. package/dist/plugins/triggers/place.js +1 -2
  183. package/dist/plugins/triggers/triggers.d.ts +6 -1
  184. package/dist/plugins/triggers/triggers.js +12 -13
  185. package/dist/plugins/uploads.svelte.d.ts +5 -2
  186. package/dist/plugins/uploads.svelte.js +25 -15
  187. package/dist/plugins.d.ts +26 -0
  188. package/dist/plugins.js +0 -24
  189. package/dist/selection/replaceSelection.js +2 -2
  190. package/dist/selection/selection.svelte.d.ts +4 -10
  191. package/dist/selection/selection.svelte.js +7 -11
  192. package/dist/session/bindings.js +1 -1
  193. package/dist/session/commands.d.ts +7 -11
  194. package/dist/session/commands.js +13 -19
  195. package/dist/session/inputRules.js +1 -1
  196. package/dist/session/navigation.js +1 -1
  197. package/dist/session/suggestions.svelte.d.ts +0 -11
  198. package/dist/session/suggestions.svelte.js +15 -3
  199. package/dist/session/uploads.d.ts +8 -8
  200. package/dist/surface/lines.js +1 -3
  201. package/dist/surface/observer.svelte.d.ts +17 -1
  202. package/dist/surface/observer.svelte.js +72 -17
  203. package/dist/surface/overlay.d.ts +0 -14
  204. package/dist/surface/overlay.js +0 -25
  205. package/dist/surface/projector.svelte.js +1 -1
  206. package/dist/text/text.svelte.js +3 -13
  207. package/dist/utils/sanitize.d.ts +38 -0
  208. package/dist/utils/sanitize.js +82 -0
  209. package/dist/utils.d.ts +2 -0
  210. package/dist/utils.js +5 -0
  211. package/package.json +18 -1
  212. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/max-z-index.js +0 -3
  213. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/supports-popover.js +0 -16
  214. package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/is-axis-allowed.js +0 -3
  215. package/dist/plugins/code/LanguageMenu.svelte +0 -25
  216. package/dist/plugins/code/LanguageMenu.svelte.d.ts +0 -9
  217. package/dist/plugins/equation/EquationEditor.svelte +0 -27
  218. package/dist/plugins/equation/EquationEditor.svelte.d.ts +0 -9
  219. package/dist/plugins/image/ImageToolbarHost.svelte +0 -37
  220. package/dist/plugins/image/ImageToolbarHost.svelte.d.ts +0 -9
  221. package/dist/plugins/richtext/CalloutIconPicker.svelte +0 -26
  222. package/dist/plugins/richtext/CalloutIconPicker.svelte.d.ts +0 -9
@@ -1,16 +1,15 @@
1
1
  <script module lang="ts">
2
+ import { imageForms } from '../../export/forms.js';
2
3
  import './image.css';
3
- import { escapeHtml } from '../../clipboard/serializeClipboardFragment.js';
4
+
4
5
  import type { Plugin, BlockSnippetPayload } from '../../plugins.js';
5
- import type { Edytor } from '../../edytor.svelte.js';
6
6
  import EmptySlot from '../media/EmptySlot.svelte';
7
7
  import ImageChrome from './ImageChrome.svelte';
8
- import { imageKinds } from '../../crdt/semantics.js';
8
+
9
9
  import {
10
10
  imageAlignOf,
11
11
  imageAltOf,
12
12
  imageWidthOf,
13
- imgData,
14
13
  isImageFile,
15
14
  MAX_INLINE_IMAGE_BYTES,
16
15
  MIN_IMAGE_WIDTH,
@@ -20,7 +19,7 @@
20
19
  type ImageAlign
21
20
  } from './image.js';
22
21
  import { ImageControls, imageControls } from './controls.svelte.js';
23
- import { MEDIA_ICONS, type EmptyView } from '../media/empty.svelte.js';
22
+ import { MEDIA_ICONS } from '../media/empty.svelte.js';
24
23
  import { imageEnglish, keywordsOf, labelsOf } from '../../labels.js';
25
24
  import { listen } from '../../utils/listen.js';
26
25
  import { fileUploads, uploadHooks, type Uploader } from '../uploads.svelte.js';
@@ -41,11 +40,6 @@
41
40
 
42
41
  const px = (width: number | undefined) => (width === undefined ? undefined : `${width}px`);
43
42
 
44
- /** One view's image state: its source rule, the chrome. */
45
- type View = EmptyView & { controls: ImageControls };
46
- /** By view; the first image plugin of a view owns it, as its kind's definition (first wins). */
47
- const views = new WeakMap<Edytor, View>();
48
-
49
43
  export type ImagePluginOptions = {
50
44
  /**
51
45
  * Upload an image file and answer its URL: an empty image's Upload,
@@ -67,23 +61,12 @@
67
61
  const plugin: Plugin = (edytor) => {
68
62
  const labels = labelsOf(edytor, 'image', imageEnglish);
69
63
  const uploads = uploadHooks(edytor, upload);
70
- const own: View = {
71
- controls: new ImageControls(edytor),
72
- // An accepted source; an inline image over the cap is never stored.
73
- store: (block, value) => {
74
- const src = safeImageSrc(value);
75
- if (!src) return 'invalid';
76
- if (oversizedInlineImage(src)) return 'inline';
77
- block.edytor.patchData(block.id, [{ path: ['src'], value: src }]);
78
- return null;
79
- }
80
- };
81
- // The records read with no view (`plugin(undefined)`) keep no state.
82
- if (edytor && !views.has(edytor)) {
83
- views.set(edytor, own);
84
- imageControls.set(edytor, own.controls);
85
- }
86
- const owns = () => views.get(edytor) === own;
64
+ // The view's image state (its source rule, the chrome): the first image
65
+ // plugin of a view owns it, as its kind's definition (first wins). The
66
+ // records read with no view (`plugin(undefined)`) keep no state.
67
+ const controls = new ImageControls(edytor);
68
+ if (edytor && !imageControls.has(edytor)) imageControls.set(edytor, controls);
69
+ const owns = () => imageControls.get(edytor) === controls;
87
70
  return {
88
71
  onPaste: uploads.onPaste,
89
72
  // The chrome: in the overlay, for the image under the pointer.
@@ -91,7 +74,6 @@
91
74
  const release = uploads.onEdytorAttached?.(view);
92
75
  if (!owns()) return release;
93
76
  const { node } = view;
94
- const { controls } = own;
95
77
  const over = (event: PointerEvent) => controls.hover(event.target);
96
78
  const leave = (event: PointerEvent) =>
97
79
  !controls.drag && controls.leave(event.relatedTarget);
@@ -117,7 +99,7 @@
117
99
  },
118
100
  blocks: {
119
101
  image: {
120
- ...imageKinds.image,
102
+ ...imageForms().image,
121
103
  snippet: image,
122
104
  element: 'figure',
123
105
  // Image files, with an `upload`: an Upload and pasted or dropped files.
@@ -139,26 +121,7 @@
139
121
  keywords: keywordsOf(edytor, 'block.image', ['picture', 'photo', 'img']),
140
122
  group: 'Media'
141
123
  }
142
- ],
143
- html: (block, caption) => {
144
- const src = safeImageSrc(block.data?.src);
145
- const width = imageWidthOf(block.data);
146
- const align = imageAlignOf(block.data);
147
- const img = src
148
- ? `<img src="${escapeHtml(src)}" alt="${escapeHtml(imageAltOf(block.data))}"${width ? ` width="${Math.round(width)}"` : ''}>`
149
- : '';
150
- const at = align === 'center' ? '' : ` data-align="${align}"`;
151
- return `<figure${at}>${img}<figcaption>${caption}</figcaption></figure>`;
152
- },
153
- // A `figure` holding an `img`, or a bare `img`, with an accepted
154
- // source; a pasted inline image over the cap is not imported.
155
- parse: (el) => {
156
- if (el.localName === 'img') return imgData(el) ?? undefined;
157
- const data = el.localName === 'figure' && imgData(el.querySelector('img'));
158
- if (!data) return undefined;
159
- const align = el.getAttribute('data-align');
160
- return align === 'left' || align === 'right' ? { ...data, align } : data;
161
- }
124
+ ]
162
125
  }
163
126
  }
164
127
  };
@@ -172,7 +135,7 @@
172
135
  <!-- prettier-ignore -->
173
136
  {#snippet image({ block, content }: BlockSnippetPayload<ImageData>)}
174
137
  <!-- A suggestion's preview (no handle) reads only its data. -->
175
- {@const view = block.handle ? views.get(block.handle.edytor) : undefined}
138
+ {@const controls = block.handle ? imageControls.get(block.handle.edytor) : undefined}
176
139
  {@const src = safeImageSrc(block.data.src)}
177
140
  {@const pending = block.handle && !src ? fileUploads(block.handle.edytor)?.of(block.id) : undefined}
178
141
  {#if src}
@@ -182,7 +145,7 @@
182
145
  alt={imageAltOf(block.data)}
183
146
  draggable="false"
184
147
  style:width={px(
185
- view ? view.controls.width(block.id, block.data) : imageWidthOf(block.data)
148
+ controls ? controls.width(block.id, block.data) : imageWidthOf(block.data)
186
149
  )}
187
150
  />
188
151
  </div>
@@ -196,7 +159,7 @@
196
159
  <EmptySlot
197
160
  block={block.handle}
198
161
  kind="image"
199
- viewOf={(edytor) => (edytor ? views.get(edytor) : undefined)}
162
+ viewOf={(edytor) => (edytor ? imageControls.get(edytor) : undefined)}
200
163
  />
201
164
  </div>
202
165
  {/if}<!--
@@ -1,6 +1,6 @@
1
1
  import { BandDrag } from '../bands.svelte.js';
2
2
  import { layerBox, pin, sameValues } from '../../surface/overlay.js';
3
- import { imageAlignOf, imageWidthOf, MIN_IMAGE_WIDTH } from './image.js';
3
+ import { imageAlignOf, imageWidthOf, MIN_IMAGE_WIDTH, oversizedInlineImage, safeImageSrc } from './image.js';
4
4
  /** The image element of a shown image block (none while it uploads); an image holds no block. */
5
5
  const imageOf = (node) => node?.querySelector('[data-edytor-image] > img') ?? null;
6
6
  /**
@@ -29,13 +29,27 @@ export class ImageControls {
29
29
  /** The handles' gesture (a resize band's: press, view-only preview, one write). @internal */
30
30
  band;
31
31
  /** The view's image toolbar, when its plugin is listed. @internal */
32
- toolbar;
32
+ toolbar = $state();
33
33
  constructor(
34
34
  /** @internal */
35
35
  edytor) {
36
36
  this.edytor = edytor;
37
37
  this.band = new BandDrag(edytor);
38
38
  }
39
+ /**
40
+ * Store a link or an upload's answer on `block` (an empty image's): an
41
+ * accepted source; an inline image over the cap is never stored.
42
+ * @internal
43
+ */
44
+ store = (block, value) => {
45
+ const src = safeImageSrc(value);
46
+ if (!src)
47
+ return 'invalid';
48
+ if (oversizedInlineImage(src))
49
+ return 'inline';
50
+ block.edytor.patchData(block.id, [{ path: ['src'], value: src }]);
51
+ return null;
52
+ };
39
53
  /** The handle being dragged. */
40
54
  get drag() {
41
55
  return this.band.drag;
@@ -11,7 +11,6 @@ import type { Attachment } from 'svelte/attachments';
11
11
  import type { Snippet } from 'svelte';
12
12
  import type { Plugin } from '../../plugins.js';
13
13
  import { type ImageAlign } from './image.js';
14
- import { type ImageControls } from './controls.svelte.js';
15
14
  /**
16
15
  * What a `toolbar` snippet receives: the image the chrome is on (`box`),
17
16
  * its alignment (`align`) and alt text (`alt`, `setAlt`), the alt field's
@@ -20,9 +19,11 @@ import { type ImageControls } from './controls.svelte.js';
20
19
  */
21
20
  export declare class ImageToolbar {
22
21
  #private;
23
- constructor(
24
- /** @internal */
25
- controls: ImageControls);
22
+ /**
23
+ * Where the toolbar stands (`ChromeAnchor`): the image the chrome is
24
+ * on, whose top right your `toolbar` places itself in.
25
+ */
26
+ readonly anchor: import("../chrome.js").ChromeAnchor;
26
27
  /** The measured image the toolbar is on (`null`: none). */
27
28
  get box(): import("./controls.svelte.js").ImageBox | null;
28
29
  /** The view is readonly (reactive). */
@@ -1,10 +1,9 @@
1
- import { onPress, takeKeys } from '../../events/onFocus.js';
2
- import { pin } from '../../surface/overlay.js';
1
+ import { takeKeys } from '../../events/onFocus.js';
3
2
  import { imageAlignOf, imageAltOf } from './image.js';
4
3
  import { imageControls } from './controls.svelte.js';
5
- import { identify, keepFocus } from '../chrome.js';
4
+ import { identify, keepFocus, onPressOutside, virtualAnchor } from '../chrome.js';
6
5
  import { all, listen } from '../../utils/listen.js';
7
- import ImageToolbarHost from './ImageToolbarHost.svelte';
6
+ import { keysOf } from '../list.svelte.js';
8
7
  /** Where a press keeps the alt field open: the toolbar (its Alt button toggles it) and the field's own. */
9
8
  const ALT_CHROME = '[data-edytor-image-alt-panel], [data-edytor-image-toolbar], [data-edytor-image-alt]';
10
9
  /**
@@ -15,11 +14,22 @@ const ALT_CHROME = '[data-edytor-image-alt-panel], [data-edytor-image-toolbar],
15
14
  */
16
15
  export class ImageToolbar {
17
16
  controls;
17
+ snippet;
18
+ /** @internal */
18
19
  constructor(
19
20
  /** @internal */
20
- controls) {
21
+ controls,
22
+ /** The app's markup. @internal */
23
+ snippet) {
21
24
  this.controls = controls;
25
+ this.snippet = snippet;
22
26
  }
27
+ /**
28
+ * Where the toolbar stands (`ChromeAnchor`): the image the chrome is
29
+ * on, whose top right your `toolbar` places itself in.
30
+ */
31
+ anchor = virtualAnchor(() => this.#image()?.getBoundingClientRect(), () => this.#image());
32
+ #image = () => this.#block()?.node?.querySelector('[data-edytor-image] img');
23
33
  /** The measured image the toolbar is on (`null`: none). */
24
34
  get box() {
25
35
  return this.controls.box;
@@ -59,20 +69,6 @@ export class ImageToolbar {
59
69
  this.editing = this.editing ? null : (this.box?.id ?? null);
60
70
  this.#edytor.overlay.invalidate();
61
71
  };
62
- /**
63
- * A press anywhere (`onPress`, capture): one outside the alt panel and the
64
- * toolbar (whose Alt button toggles it) closes the field, so the chrome
65
- * follows the pointer again.
66
- * @internal
67
- */
68
- pressed = (event) => {
69
- const target = event.target;
70
- if (this.editing === null)
71
- return;
72
- if (target?.closest?.(ALT_CHROME))
73
- return;
74
- this.closeAlt();
75
- };
76
72
  /** Focus left the alt field: for somewhere outside its panel, the field closes. @internal */
77
73
  blurred = (event) => {
78
74
  const to = event.relatedTarget;
@@ -106,13 +102,8 @@ export class ImageToolbar {
106
102
  identify(node, this.#edytor.popups.idOf('image-alt'));
107
103
  node.setAttribute('data-edytor-image-alt', '');
108
104
  node.focus({ preventScroll: true });
109
- const keydown = (event) => {
110
- if (event.isComposing || (event.key !== 'Enter' && event.key !== 'Escape'))
111
- return;
112
- event.preventDefault();
113
- this.closeAlt(true);
114
- };
115
- return all(listen(node, 'keydown', keydown), listen(node, 'focusout', this.blurred));
105
+ const close = () => this.closeAlt(true);
106
+ return all(listen(node, 'keydown', keysOf({ Enter: close, Escape: close })), listen(node, 'focusout', this.blurred));
116
107
  };
117
108
  /** Close the alt field; with `keys`, the editor takes the keys back. */
118
109
  closeAlt = (keys = false) => {
@@ -135,11 +126,10 @@ export const createImageToolbarPlugin = ({ toolbar }) => (edytor) => ({
135
126
  const controls = imageControls.get(edytor);
136
127
  if (!controls || controls.toolbar)
137
128
  return;
138
- const bar = (controls.toolbar = new ImageToolbar(controls));
129
+ const bar = (controls.toolbar = new ImageToolbar(controls, toolbar));
139
130
  return all(
140
- // A press outside the alt panel closes it (WebKit's lone `mousedown` too).
141
- onPress(edytor, node.ownerDocument, bar.pressed, true), edytor.overlay.mount(ImageToolbarHost, { toolbar: bar, snippet: toolbar }, 'edytor-image-toolbar-host',
142
- // With the image chrome: above the block handles and the column bands.
143
- 7, (host) => () => pin(host)), () => (controls.toolbar = undefined));
131
+ // A press outside the alt panel and the toolbar (whose Alt button toggles it)
132
+ // closes it (WebKit's lone `mousedown` too).
133
+ onPressOutside(edytor, node.ownerDocument, ALT_CHROME, () => bar.closeAlt()), () => (controls.toolbar = undefined));
144
134
  }
145
135
  });
@@ -1,7 +1,7 @@
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
3
  export { createArrowMovePlugin } from './arrowMove/arrowMove.js';
4
- export type { MenuItemPayload } from './chrome.js';
4
+ export type { ChromeAnchor, MenuItemPayload } from './chrome.js';
5
5
  export { BLOCK_ACTIVATE_EVENT, BLOCK_ADD_EVENT, type BlockActivation, type BlockAddition } from './blockHandles/BlockHandleController.svelte.js';
6
6
  export { createBlockHandlesPlugin, type BlockHandlesOptions, type BlockHandleSnippetPayload } from './blockHandles/blockHandlesPlugin.js';
7
7
  export { createCodePlugin, CODE_LANGUAGES, loadCodeLanguage, type CodeLanguage, type CodePluginOptions } from './code/CodePlugin.svelte';
@@ -36,6 +36,13 @@ type Steps = {
36
36
  ends?: boolean;
37
37
  usable?: (index: number) => boolean;
38
38
  };
39
+ /**
40
+ * A field's or a control's keys (`keydown`): `keys[event.key]` runs and the
41
+ * key's default is prevented (with `stop`, it goes no further), unless the
42
+ * handler answers `false`; a key an IME composes, or one already prevented,
43
+ * is left alone.
44
+ */
45
+ export declare const keysOf: (keys: Partial<Record<string, (event: KeyboardEvent) => unknown>>, stop?: boolean) => ((event: KeyboardEvent) => void);
39
46
  /**
40
47
  * A key on a menu that holds the keyboard itself, not a field (WAI-ARIA's
41
48
  * menu keys): the arrows, Home and End move its row (`stepRow`), Enter and
@@ -112,6 +119,8 @@ export declare abstract class ChromeList {
112
119
  role: 'listbox' | 'menu', open?: (node: HTMLElement) => Omit<Popup, 'id'> | false | null);
113
120
  /** How many rows the keyboard walks. */
114
121
  protected abstract get count(): number;
122
+ /** The view is readonly (reactive): the list closes. */
123
+ get readonly(): boolean;
115
124
  /** The list's element id (page-unique), for `aria-controls`. */
116
125
  get listId(): string;
117
126
  /** Row `index`'s element id (page-unique), for `aria-activedescendant`. */
@@ -70,6 +70,20 @@ export const stepRow = (key, at, count, { columns = 0, ends = false, usable = ()
70
70
  if (ends && length && (key === 'Home' || key === 'End'))
71
71
  return rows[key === 'Home' ? 0 : length - 1];
72
72
  };
73
+ /**
74
+ * A field's or a control's keys (`keydown`): `keys[event.key]` runs and the
75
+ * key's default is prevented (with `stop`, it goes no further), unless the
76
+ * handler answers `false`; a key an IME composes, or one already prevented,
77
+ * is left alone.
78
+ */
79
+ export const keysOf = (keys, stop = false) => (event) => {
80
+ const run = keys[event.key];
81
+ if (event.defaultPrevented || event.isComposing || !run || run(event) === false)
82
+ return;
83
+ event.preventDefault();
84
+ if (stop)
85
+ event.stopPropagation();
86
+ };
73
87
  /**
74
88
  * A key on a menu that holds the keyboard itself, not a field (WAI-ARIA's
75
89
  * menu keys): the arrows, Home and End move its row (`stepRow`), Enter and
@@ -192,6 +206,10 @@ export class ChromeList {
192
206
  this.role = role;
193
207
  this.popup = listPopup(edytor, owner, role, open);
194
208
  }
209
+ /** The view is readonly (reactive): the list closes. */
210
+ get readonly() {
211
+ return this.edytor.readonly;
212
+ }
195
213
  /** The list's element id (page-unique), for `aria-controls`. */
196
214
  get listId() {
197
215
  return this.edytor.popups.idOf(this.owner);
@@ -3,9 +3,6 @@ import MarqueeBox from './MarqueeBox.svelte';
3
3
  import { MarqueeController } from './MarqueeController.svelte.js';
4
4
  /** The handle gutter: a block handle's own box, around its buttons (never a button). */
5
5
  const HANDLE_HOST = '[data-edytor-block-handle-host]';
6
- const controllers = new WeakMap();
7
- /** The marquee's controller of `edytor`; `undefined` when the plugin is not listed. @internal */
8
- export const marqueeController = (edytor) => controllers.get(edytor);
9
6
  /**
10
7
  * Block selection with a rectangle (Notion's rubber band), opt-in: a press
11
8
  * in the editor's empty area — the margins beside the blocks (the handle
@@ -18,7 +15,6 @@ export const marqueeController = (edytor) => controllers.get(edytor);
18
15
  */
19
16
  export const createMarqueePlugin = (options = {}) => (edytor) => {
20
17
  const controller = new MarqueeController(edytor, options.threshold ?? 4);
21
- controllers.set(edytor, controller);
22
18
  return {
23
19
  onEdytorAttached: ({ node }) => {
24
20
  const layer = edytor.overlay.layer;
@@ -19,5 +19,5 @@
19
19
  * Void: its only text is the caption.
20
20
  */
21
21
  export const createAudioPlugin = ({ upload }: AudioPluginOptions = {}): Plugin =>
22
- sourcePlugin('audio', ['sound', 'music', 'mp3'], undefined, {}, upload);
22
+ sourcePlugin('audio', ['sound', 'music', 'mp3'], {}, upload);
23
23
  </script>
@@ -1,21 +1,11 @@
1
1
  <script module lang="ts">
2
- import type { Plugin, BlockSnippetPayload } from '../../plugins.js';
2
+ import { mediaForms } from '../../export/forms.js';
3
+ import type { Plugin } from '../../plugins.js';
3
4
  import type { Block } from '../../block/block.svelte.js';
4
- import EmptySlot from './EmptySlot.svelte';
5
- import Control from '../Control.svelte';
5
+ import { previewData, source } from './SourceBlock.svelte';
6
6
  import type { Snippet } from 'svelte';
7
7
  import { urlPaste, type UrlPasteController } from './urlPaste.svelte.js';
8
- import {
9
- MEDIA_ICONS,
10
- escapeHtml,
11
- mediaControl,
12
- mediaLink,
13
- mediaPlugin,
14
- mediaView,
15
- safeWebUrl,
16
- LINK_REL,
17
- type BookmarkCardPayload
18
- } from './media.js';
8
+ import { MEDIA_ICONS, mediaPlugin, safeWebUrl, type BookmarkCardPayload } from './media.js';
19
9
 
20
10
  export type { BookmarkCardPayload };
21
11
 
@@ -54,21 +44,6 @@
54
44
  card?: Snippet<[BookmarkCardPayload]>;
55
45
  };
56
46
 
57
- /** A preview's text field, trimmed and capped: it is stored in the document. */
58
- const text = (value: unknown, max: number) =>
59
- typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : undefined;
60
-
61
- /** The fields of `preview` a bookmark stores: text capped, images {@link safeWebUrl} only. */
62
- const previewData = (preview: BookmarkPreview | null | undefined) => {
63
- const fields = {
64
- title: text(preview?.title, 300),
65
- description: text(preview?.description, 1000),
66
- image: safeWebUrl(preview?.image) ?? undefined,
67
- icon: safeWebUrl(preview?.icon) ?? undefined
68
- };
69
- return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
70
- };
71
-
72
47
  /**
73
48
  * The unfurl's writes: background bookkeeping, not a gesture, so no
74
49
  * view's history tracks this origin (`crdt/document.ts`, History). The
@@ -77,9 +52,6 @@
77
52
  */
78
53
  const UNFURL_ORIGIN = Symbol('edytor.bookmark.unfurl');
79
54
 
80
- /** The view's card markup, if the app gave one. */
81
- const cardOf = mediaControl('bookmark', 'card');
82
-
83
55
  /**
84
56
  * Notion's web bookmark: a card linking to a page, with its title,
85
57
  * description and images when your `unfurl` answers them, and an
@@ -135,72 +107,9 @@
135
107
  options.menu
136
108
  ),
137
109
  record: {
138
- snippet: bookmark,
139
- html: (block, caption) => {
140
- const url = safeWebUrl(block.data?.url);
141
- const title = text(block.data?.title, 300) ?? url;
142
- const link = url ? `<a href="${escapeHtml(url)}">${escapeHtml(title!)}</a>` : '';
143
- return `<figure data-edytor-bookmark>${link}<figcaption>${caption}</figcaption></figure>`;
144
- },
145
- plain: (block, caption) =>
146
- [safeWebUrl(block.data?.url), caption].filter(Boolean).join('\n'),
147
- // Its own export: a `figure[data-edytor-bookmark]` holding the link.
148
- parse: (el) => {
149
- if (el.localName !== 'figure' || !el.hasAttribute('data-edytor-bookmark')) return;
150
- const anchor = el.querySelector('a[href]');
151
- const url = safeWebUrl(anchor?.getAttribute('href'));
152
- const title = text(anchor?.textContent, 300);
153
- return url ? { url, ...(title && title !== url ? { title } : {}) } : undefined;
154
- }
110
+ snippet: source,
111
+ ...mediaForms().bookmark
155
112
  }
156
113
  };
157
114
  });
158
115
  </script>
159
-
160
- {#snippet bookmark({ block, content }: BlockSnippetPayload<Record<string, unknown>>)}
161
- {@const url = safeWebUrl(block.data.url)}
162
- {#if url}
163
- <div use:block.void data-edytor-bookmark>
164
- <Control
165
- own={cardOf}
166
- {initial}
167
- payload={{
168
- block: block.handle,
169
- url,
170
- title: text(block.data.title, 300),
171
- description: text(block.data.description, 1000),
172
- image: safeWebUrl(block.data.image) ?? undefined,
173
- icon: safeWebUrl(block.data.icon) ?? undefined,
174
- link: mediaLink({ draggable: 'false' })
175
- }}
176
- />
177
- </div>
178
- {:else}
179
- <div use:block.void data-edytor-media-empty>
180
- <EmptySlot
181
- block={block.handle}
182
- kind="bookmark"
183
- viewOf={(edytor) => mediaView(edytor, 'bookmark')}
184
- />
185
- </div>
186
- {/if}
187
- <!-- The core renders the kind's <figure> around this markup. -->
188
- <!-- svelte-ignore a11y_figcaption_parent -->
189
- <figcaption>{@render content()}</figcaption>
190
- {/snippet}
191
-
192
- <!-- Notion's card: the page's title, description and URL (with its icon) beside its cover. -->
193
- {#snippet initial({ url, title, description, image, icon, link }: BookmarkCardPayload)}
194
- <a href={url} target="_blank" rel={LINK_REL} draggable="false" {@attach link}>
195
- <span data-edytor-bookmark-text>
196
- <span data-edytor-bookmark-title>{title ?? url}</span>
197
- {#if description}<span data-edytor-bookmark-description>{description}</span>{/if}
198
- <span data-edytor-bookmark-url>
199
- {#if icon}<img src={icon} alt="" draggable="false" />{/if}<span>{url}</span>
200
- </span>
201
- </span>
202
- {#if image}
203
- <span data-edytor-bookmark-cover><img src={image} alt="" draggable="false" /></span>
204
- {/if}
205
- </a>
206
- {/snippet}
@@ -1,8 +1,7 @@
1
1
  <script module lang="ts">
2
- import type { Plugin, BlockSnippetPayload } from '../../plugins.js';
3
- import type { Block } from '../../block/block.svelte.js';
4
- import EmptySlot from './EmptySlot.svelte';
5
- import Control from '../Control.svelte';
2
+ import { mediaForms } from '../../export/forms.js';
3
+ import type { Plugin } from '../../plugins.js';
4
+ import { source } from './SourceBlock.svelte';
6
5
  import type { Snippet } from 'svelte';
7
6
  import { urlPaste, type UrlPasteController } from './urlPaste.svelte.js';
8
7
  import {
@@ -10,14 +9,8 @@
10
9
  EMBED_PROVIDERS,
11
10
  EMBED_SANDBOX,
12
11
  MEDIA_ICONS,
13
- claimed,
14
12
  embedSourceOf,
15
- escapeHtml,
16
- mediaControl,
17
- mediaLink,
18
- LINK_REL,
19
13
  mediaPlugin,
20
- mediaView,
21
14
  safeWebUrl,
22
15
  type EmbedFallbackPayload,
23
16
  type EmbedProvider
@@ -54,16 +47,6 @@
54
47
  fallback?: Snippet<[EmbedFallbackPayload]>;
55
48
  };
56
49
 
57
- /** The view's fallback markup, if the app gave one. */
58
- const fallbackOf = mediaControl('embed', 'fallback');
59
-
60
- /**
61
- * The view's allowlist: the first embed plugin listed in it (first wins,
62
- * as for the kind). Never a broader list: no entry plays nothing.
63
- */
64
- const providers = (block: Block | undefined) =>
65
- mediaView(block?.edytor, 'embed')?.providers ?? [];
66
-
67
50
  /**
68
51
  * Notion's embed block: an "Embed a link" panel until it has a link, then
69
52
  * the provider's player in a sandboxed frame, with an editable caption.
@@ -98,78 +81,10 @@
98
81
  options.menu
99
82
  ),
100
83
  record: {
101
- snippet: embed,
102
- html: (block, caption) => {
103
- const player = embedSourceOf(block.data?.url, allowed);
104
- const url = safeWebUrl(block.data?.url);
105
- const media = player
106
- ? `<iframe src="${escapeHtml(player.src)}" title="${escapeHtml(player.provider.name)}" sandbox="${EMBED_SANDBOX}" allowfullscreen></iframe>`
107
- : url
108
- ? `<a href="${escapeHtml(url)}">${escapeHtml(url)}</a>`
109
- : '';
110
- return `<figure>${media}<figcaption>${caption}</figcaption></figure>`;
111
- },
112
- plain: (block, caption) =>
113
- [safeWebUrl(block.data?.url), caption].filter(Boolean).join('\n'),
114
- // An allowlisted `iframe` (bare, or in a `figure`): its `src` is the link.
115
- parse: (el) => {
116
- const src = claimed(el, 'iframe')?.getAttribute('src');
117
- const url = embedSourceOf(src, allowed) && safeWebUrl(src);
118
- return url ? { url } : undefined;
119
- }
84
+ snippet: source,
85
+ ...mediaForms(allowed).embed
120
86
  }
121
87
  })
122
88
  );
123
89
  };
124
90
  </script>
125
-
126
- {#snippet embed({ block, content }: BlockSnippetPayload<{ url?: string }>)}
127
- {@const player = block.handle ? embedSourceOf(block.data.url, providers(block.handle)) : null}
128
- {@const url = safeWebUrl(block.data.url)}
129
- {#if player}
130
- {@const { height, aspectRatio = '16 / 9' } = player.provider}
131
- <div
132
- use:block.void
133
- data-edytor-embed
134
- style:aspect-ratio={height ? undefined : aspectRatio}
135
- style:height={height ? `${height}px` : undefined}
136
- >
137
- <iframe
138
- src={player.src}
139
- title={player.provider.name}
140
- sandbox={EMBED_SANDBOX}
141
- allow={EMBED_ALLOW}
142
- referrerpolicy="strict-origin-when-cross-origin"
143
- loading="lazy"
144
- allowfullscreen
145
- style="display: block; width: 100%; height: 100%; border: 0"
146
- ></iframe>
147
- </div>
148
- {:else if url}
149
- <!-- A link no provider plays (a peer may store any), or a suggestion's preview (no handle, so no
150
- view's allowlist): a plain link, never a frame. -->
151
- <div use:block.void data-edytor-embed-unsupported>
152
- <Control
153
- own={fallbackOf}
154
- {initial}
155
- payload={{ block: block.handle, url, link: mediaLink() }}
156
- />
157
- </div>
158
- {:else}
159
- <div use:block.void data-edytor-media-empty>
160
- <EmptySlot
161
- block={block.handle}
162
- kind="embed"
163
- viewOf={(edytor) => mediaView(edytor, 'embed')}
164
- />
165
- </div>
166
- {/if}
167
- <!-- The core renders the kind's <figure> around this markup. -->
168
- <!-- svelte-ignore a11y_figcaption_parent -->
169
- <figcaption>{@render content()}</figcaption>
170
- {/snippet}
171
-
172
- <!-- The link itself. -->
173
- {#snippet initial({ url, link }: EmbedFallbackPayload)}
174
- <a href={url} target="_blank" rel={LINK_REL} {@attach link}>{url}</a>
175
- {/snippet}