edytor 1.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.
- package/README.md +26 -69
- package/dist/block/block.svelte.d.ts +6 -4
- package/dist/block/block.svelte.js +12 -8
- package/dist/block/inlineBlock.svelte.d.ts +6 -2
- package/dist/block/inlineBlock.svelte.js +7 -2
- package/dist/clipboard/htmlFlow.d.ts +20 -7
- package/dist/clipboard/htmlFlow.js +23 -15
- package/dist/clipboard/serializeClipboardFragment.d.ts +37 -4
- package/dist/clipboard/serializeClipboardFragment.js +6 -4
- package/dist/clipboard/textBlocks.d.ts +8 -0
- package/dist/clipboard/textBlocks.js +6 -2
- package/dist/cloudflare/room/admission.js +3 -3
- package/dist/cloudflare/room/context.d.ts +7 -1
- package/dist/cloudflare/routeDocumentSocket.d.ts +17 -0
- package/dist/cloudflare/routeDocumentSocket.js +53 -0
- package/dist/collaboration/awarenessSelection.d.ts +22 -1
- package/dist/collaboration/awarenessSelection.js +42 -0
- package/dist/collaboration/comments/client.d.ts +1 -1
- package/dist/collaboration/comments/client.js +1 -1
- package/dist/collaboration/remoteSelection.js +3 -3
- package/dist/components/Block.svelte +3 -1
- package/dist/components/Edytor.svelte +38 -20
- package/dist/components/Edytor.svelte.d.ts +9 -4
- package/dist/components/InlineBlock.svelte +1 -1
- package/dist/crdt/admission.d.ts +5 -10
- package/dist/crdt/admission.js +5 -11
- package/dist/crdt/data.d.ts +10 -0
- package/dist/crdt/data.js +20 -2
- package/dist/crdt/doc/funnel.d.ts +7 -6
- package/dist/crdt/doc/funnel.js +10 -9
- package/dist/crdt/doc/gate.d.ts +0 -17
- package/dist/crdt/doc/gate.js +2 -41
- package/dist/crdt/doc/history.js +2 -2
- package/dist/crdt/doc/reads.d.ts +7 -0
- package/dist/crdt/doc/reads.js +11 -1
- package/dist/crdt/document.d.ts +13 -14
- package/dist/crdt/document.js +17 -19
- package/dist/crdt/edytor-doc.d.ts +22 -4
- package/dist/crdt/edytor-doc.js +5 -8
- package/dist/crdt/index.d.ts +9 -3
- package/dist/crdt/index.js +2 -2
- 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/crdt/protocol.d.ts +2 -2
- package/dist/crdt/protocol.js +2 -2
- package/dist/crdt/providers/index.d.ts +1 -1
- package/dist/crdt/providers/room.d.ts +10 -9
- package/dist/crdt/providers/room.js +13 -10
- package/dist/crdt/providers/websocket.d.ts +1 -1
- package/dist/crdt/providers/websocket.js +28 -29
- package/dist/crdt/semantics.js +3 -1
- package/dist/dnd/autoScroll.d.ts +15 -7
- package/dist/dnd/autoScroll.js +10 -15
- package/dist/{plugins/blockHandles → dnd}/dropIndicator.d.ts +2 -2
- package/dist/{plugins/blockHandles → dnd}/dropIndicator.js +2 -5
- package/dist/dnd/external.d.ts +6 -0
- package/dist/dnd/external.js +17 -0
- package/dist/{plugins/blockHandles → dnd}/geometry.d.ts +4 -2
- package/dist/{plugins/blockHandles → dnd}/geometry.js +3 -4
- package/dist/dnd/placement.d.ts +200 -0
- package/dist/dnd/placement.js +426 -0
- package/dist/dnd/pragmatic.d.ts +3 -1
- package/dist/dnd/pragmatic.js +12 -2
- package/dist/dnd/vendor/pragmatic/UPSTREAM.md +51 -18
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/drop-target-for-external.d.ts +4 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/drop-target-for-external.js +6 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/element-adapter.d.ts +0 -2
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/element-adapter.js +4 -22
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter-types.d.ts +26 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter.d.ts +30 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/external-adapter.js +203 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/get-available-items.js +13 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/get-available-types.js +9 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/is-an-available-type.js +19 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/monitor-for-external.d.ts +4 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/adapter/monitor-for-external.js +6 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/honey-pot-fix/make-honey-pot-fix.js +4 -16
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/internal-types.d.ts +1 -8
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/make-adapter/make-drop-target.js +5 -6
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/make-adapter/make-monitor.js +2 -3
- 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
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/element/custom-native-drag-preview/set-custom-native-drag-preview.js +6 -24
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/contains-files.d.ts +2 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/contains-files.js +5 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/get-files.d.ts +3 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/get-files.js +12 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/public-utils/external/native-types.d.ts +12 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/changing-window/is-entering-window-in-safari.js +10 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/changing-window/is-entering-window.js +55 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/entry-point/external.d.ts +4 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/entry-point/external.js +8 -0
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/internal-types.d.ts +0 -4
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/over-element/get-scroll-by.js +16 -72
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/over-element/try-scroll.js +1 -9
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/configuration.js +4 -29
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/get-scroll-change.js +2 -3
- package/dist/edytor.svelte.d.ts +19 -9
- package/dist/edytor.svelte.js +28 -33
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/beforeInputCommands.js +8 -4
- package/dist/events/onBeforeInput.js +1 -1
- package/dist/events/onDrop.js +25 -10
- package/dist/events/onInput.d.ts +0 -12
- package/dist/events/onInput.js +1 -67
- package/dist/events/onPaste.d.ts +2 -1
- package/dist/events/onPaste.js +8 -3
- package/dist/export/forms.d.ts +627 -0
- package/dist/export/forms.js +749 -0
- package/dist/export/index.d.ts +170 -0
- package/dist/export/index.js +161 -0
- package/dist/export/markdown.d.ts +41 -0
- package/dist/export/markdown.js +585 -0
- package/dist/export/markdownWriter.d.ts +9 -0
- package/dist/export/markdownWriter.js +133 -0
- package/dist/export/mdx.d.ts +15 -0
- package/dist/export/mdx.js +33 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/kinds.js +1 -1
- package/dist/labels.d.ts +2 -0
- package/dist/labels.js +2 -1
- package/dist/plugins/anchored.svelte.d.ts +21 -25
- package/dist/plugins/anchored.svelte.js +23 -39
- 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 +48 -414
- package/dist/plugins/blockHandles/BlockHandles.svelte +14 -32
- package/dist/plugins/blockHandles/blockHandlesPlugin.js +2 -1
- package/dist/plugins/blockHandles/dragPreview.d.ts +26 -7
- package/dist/plugins/blockHandles/dragPreview.js +33 -0
- package/dist/plugins/blockMenu/BlockMenuController.svelte.d.ts +5 -0
- package/dist/plugins/blockMenu/BlockMenuController.svelte.js +7 -2
- package/dist/plugins/blockMenu/blockMenuPlugin.js +5 -50
- package/dist/plugins/chrome.d.ts +27 -5
- package/dist/plugins/chrome.js +24 -9
- package/dist/plugins/code/CodeHeader.svelte +5 -3
- package/dist/plugins/code/CodePlugin.svelte +15 -12
- package/dist/plugins/code/header.svelte.js +7 -11
- package/dist/plugins/code/languageMenu.svelte.d.ts +5 -2
- package/dist/plugins/code/languageMenu.svelte.js +13 -20
- 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 +8 -21
- 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 +5 -6
- package/dist/plugins/columns/resize.svelte.js +12 -14
- package/dist/plugins/comments/CommentsController.svelte.d.ts +6 -0
- package/dist/plugins/comments/CommentsController.svelte.js +20 -0
- package/dist/plugins/equation/EquationPlugin.svelte +13 -30
- package/dist/plugins/equation/EquationPlugin.svelte.d.ts +1 -0
- package/dist/plugins/equation/equation.css +7 -0
- package/dist/plugins/equation/equation.svelte.d.ts +7 -15
- package/dist/plugins/equation/equation.svelte.js +13 -50
- package/dist/plugins/equation/tex.d.ts +17 -0
- package/dist/plugins/equation/tex.js +42 -0
- package/dist/plugins/fileDrop.d.ts +43 -0
- package/dist/plugins/fileDrop.js +122 -0
- package/dist/plugins/find/FindController.svelte.d.ts +5 -0
- package/dist/plugins/find/FindController.svelte.js +14 -14
- package/dist/plugins/find/findPlugin.js +1 -15
- package/dist/plugins/image/ImageChrome.svelte +8 -2
- package/dist/plugins/image/ImagePlugin.svelte +56 -84
- package/dist/plugins/image/ImagePlugin.svelte.d.ts +13 -1
- package/dist/plugins/image/controls.svelte.js +16 -2
- package/dist/plugins/image/image.css +17 -0
- package/dist/plugins/image/toolbar.svelte.d.ts +5 -4
- package/dist/plugins/image/toolbar.svelte.js +21 -31
- package/dist/plugins/index.d.ts +4 -5
- package/dist/plugins/index.js +0 -1
- package/dist/plugins/list.svelte.d.ts +9 -0
- package/dist/plugins/list.svelte.js +18 -0
- package/dist/plugins/markdownShortcuts.js +5 -5
- package/dist/plugins/marquee/marqueePlugin.js +0 -4
- package/dist/plugins/media/AudioPlugin.svelte +13 -1
- package/dist/plugins/media/AudioPlugin.svelte.d.ts +11 -1
- package/dist/plugins/media/BookmarkPlugin.svelte +6 -97
- package/dist/plugins/media/EmbedPlugin.svelte +5 -90
- package/dist/plugins/media/EmptySlot.svelte +36 -20
- package/dist/plugins/media/EmptySlot.svelte.d.ts +6 -15
- package/dist/plugins/media/FilePlugin.svelte +12 -20
- package/dist/plugins/media/FilePlugin.svelte.d.ts +9 -0
- package/dist/plugins/media/MediaEmpty.svelte +2 -55
- package/dist/plugins/media/MediaEmpty.svelte.d.ts +18 -14
- package/dist/plugins/media/SourceBlock.svelte +178 -52
- package/dist/plugins/media/SourceBlock.svelte.d.ts +13 -6
- 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 +32 -26
- package/dist/plugins/media/media.d.ts +9 -63
- package/dist/plugins/media/media.js +1 -194
- package/dist/plugins/media/sources.d.ts +60 -0
- package/dist/plugins/media/sources.js +165 -0
- package/dist/plugins/media/upload.css +23 -0
- package/dist/plugins/media/urlPaste.svelte.d.ts +2 -1
- package/dist/plugins/media/urlPaste.svelte.js +12 -21
- package/dist/plugins/mention/MentionPlugin.svelte +50 -38
- package/dist/plugins/mention/MentionPlugin.svelte.d.ts +11 -2
- package/dist/plugins/mention/mention.css +6 -0
- package/dist/plugins/page/PagePlugin.svelte +2 -9
- package/dist/plugins/page/page.js +1 -1
- package/dist/plugins/pageLink/PageLinkPlugin.svelte +126 -28
- package/dist/plugins/pageLink/PageLinkPlugin.svelte.d.ts +13 -1
- package/dist/plugins/pageLink/pageLink.css +6 -0
- package/dist/plugins/richtext/RichTextPlugin.svelte +99 -204
- package/dist/plugins/richtext/RichTextPlugin.svelte.d.ts +1 -0
- package/dist/plugins/richtext/body.js +1 -1
- package/dist/plugins/richtext/calloutIcons.svelte.d.ts +2 -2
- package/dist/plugins/richtext/calloutIcons.svelte.js +5 -7
- package/dist/plugins/richtext/richTextOperations.d.ts +1 -19
- package/dist/plugins/richtext/richTextOperations.js +3 -49
- package/dist/plugins/richtext/richtext.css +6 -0
- package/dist/plugins/slashMenu/SlashMenuController.svelte.d.ts +2 -0
- package/dist/plugins/slashMenu/SlashMenuController.svelte.js +7 -0
- package/dist/plugins/slashMenu/slashMenuPlugin.js +5 -14
- package/dist/plugins/table/TableChrome.svelte +2 -3
- package/dist/plugins/table/TablePlugin.svelte +20 -54
- package/dist/plugins/table/chrome.svelte.d.ts +5 -2
- package/dist/plugins/table/chrome.svelte.js +33 -56
- package/dist/plugins/table/table.js +3 -3
- package/dist/plugins/toc/TocPlugin.svelte +2 -4
- package/dist/plugins/toolbar/ToolbarController.svelte.d.ts +7 -0
- package/dist/plugins/toolbar/ToolbarController.svelte.js +20 -13
- package/dist/plugins/toolbar/toolbarPlugin.js +19 -53
- package/dist/plugins/triggers/TriggerController.svelte.d.ts +7 -2
- package/dist/plugins/triggers/TriggerController.svelte.js +11 -4
- package/dist/plugins/triggers/place.js +1 -2
- package/dist/plugins/triggers/triggers.d.ts +6 -1
- package/dist/plugins/triggers/triggers.js +12 -13
- package/dist/plugins/uploads.svelte.d.ts +48 -32
- package/dist/plugins/uploads.svelte.js +119 -43
- package/dist/plugins.d.ts +66 -14
- package/dist/plugins.js +0 -24
- package/dist/selection/replaceSelection.js +2 -2
- package/dist/selection/selection.svelte.d.ts +4 -10
- package/dist/selection/selection.svelte.js +7 -11
- package/dist/session/attempt.d.ts +16 -6
- package/dist/session/attempt.js +1 -0
- package/dist/session/bindings.js +35 -32
- package/dist/session/commands.d.ts +7 -11
- package/dist/session/commands.js +15 -19
- package/dist/session/history.js +1 -0
- package/dist/session/inputRules.d.ts +6 -3
- package/dist/session/inputRules.js +10 -6
- 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 +20 -11
- package/dist/session/props.d.ts +28 -0
- package/dist/session/props.js +65 -0
- package/dist/session/suggestions.svelte.d.ts +0 -11
- package/dist/session/suggestions.svelte.js +15 -3
- package/dist/session/uploads.d.ts +29 -0
- package/dist/session/uploads.js +10 -0
- package/dist/surface/lines.js +1 -3
- package/dist/surface/observer.svelte.d.ts +17 -1
- package/dist/surface/observer.svelte.js +72 -17
- package/dist/surface/overlay.d.ts +0 -14
- package/dist/surface/overlay.js +0 -25
- package/dist/surface/projector.svelte.js +1 -1
- package/dist/text/text.svelte.js +3 -13
- package/dist/themes/notion.css +9 -0
- package/dist/utils/sanitize.d.ts +38 -0
- package/dist/utils/sanitize.js +82 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +5 -0
- package/package.json +18 -1
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/max-z-index.js +0 -3
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop/util/supports-popover.js +0 -16
- package/dist/dnd/vendor/pragmatic/pragmatic-drag-and-drop-auto-scroll/shared/is-axis-allowed.js +0 -3
- package/dist/plugins/code/LanguageMenu.svelte +0 -25
- package/dist/plugins/code/LanguageMenu.svelte.d.ts +0 -9
- package/dist/plugins/equation/EquationEditor.svelte +0 -27
- package/dist/plugins/equation/EquationEditor.svelte.d.ts +0 -9
- package/dist/plugins/image/ImageToolbarHost.svelte +0 -37
- package/dist/plugins/image/ImageToolbarHost.svelte.d.ts +0 -9
- package/dist/plugins/richtext/CalloutIconPicker.svelte +0 -26
- package/dist/plugins/richtext/CalloutIconPicker.svelte.d.ts +0 -9
- /package/dist/{plugins/columns → dnd}/stacking.d.ts +0 -0
- /package/dist/{plugins/columns → dnd}/stacking.js +0 -0
|
@@ -255,21 +255,30 @@ const stopBelow = (edytor, block) => {
|
|
|
255
255
|
* cell, a code line, a heading). Never in a view that writes nothing.
|
|
256
256
|
*/
|
|
257
257
|
const leavesLastStop = (edytor) => {
|
|
258
|
-
const
|
|
259
|
-
const {
|
|
260
|
-
|
|
261
|
-
const block = at && edytor.idToBlock.get(at.block);
|
|
262
|
-
if (value.kind !== 'text' || !projection.isCollapsed || !block || !root)
|
|
263
|
-
return false;
|
|
264
|
-
if (!edytor.dispatcher.permits())
|
|
258
|
+
const block = onLastLine(edytor);
|
|
259
|
+
const { root } = edytor;
|
|
260
|
+
if (!block || !root || !edytor.dispatcher.permits())
|
|
265
261
|
return false;
|
|
266
262
|
if (block.parent?.isRoot && block.type === edytor.defaultChild(root))
|
|
267
263
|
return false;
|
|
264
|
+
return !stopBelow(edytor, block.id);
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* The block a collapsed text caret sits on the last line of (a line box
|
|
268
|
+
* where the view lays one out, else a line break), if it does: where
|
|
269
|
+
* ArrowDown leaves the block (`nav.trailing.exit`, `body.down`).
|
|
270
|
+
*/
|
|
271
|
+
export const onLastLine = (edytor) => {
|
|
272
|
+
const { value, projection } = edytor.selection;
|
|
273
|
+
const at = projection.start;
|
|
274
|
+
const block = at && edytor.idToBlock.get(at.block);
|
|
275
|
+
if (value.kind !== 'text' || !projection.isCollapsed || !block)
|
|
276
|
+
return;
|
|
268
277
|
const text = stringOf(edytor, at.block);
|
|
269
278
|
if (text.includes('\n', at.offset))
|
|
270
|
-
return
|
|
271
|
-
|
|
272
|
-
|
|
279
|
+
return;
|
|
280
|
+
const last = edytor.ports.surface.sameLine(at, { block: at.block, offset: text.length });
|
|
281
|
+
return last === false ? undefined : block;
|
|
273
282
|
};
|
|
274
283
|
/**
|
|
275
284
|
* The trailing paragraph (`nav.trailing`, Notion): the caret goes to an
|
|
@@ -294,7 +303,7 @@ export const toTrailingParagraph = (edytor) => {
|
|
|
294
303
|
const text = edytor.idToBlock.get(ids[0] ?? '')?.firstText;
|
|
295
304
|
if (!text)
|
|
296
305
|
return false;
|
|
297
|
-
edytor.dispatcher.
|
|
306
|
+
edytor.dispatcher.caretIn(text, 0);
|
|
298
307
|
return true;
|
|
299
308
|
};
|
|
300
309
|
/**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A view's property writes (`block.data`, `atom.data`, `edytor.data`): the
|
|
3
|
+
* data proxy (`crdt/props.ts`) over the view's one write path.
|
|
4
|
+
*
|
|
5
|
+
* The writes of one synchronous task are one dispatch: they queue per
|
|
6
|
+
* target and leave on the next microtask, or before any other operation
|
|
7
|
+
* this view dispatches, as the `patchData` operation of each target (more
|
|
8
|
+
* than one target: one user command, so one undo step, in one
|
|
9
|
+
* transaction). Hooks see that operation; a veto drops the burst. Until
|
|
10
|
+
* then the proxy reads its target's data with the queued patches applied
|
|
11
|
+
* (`dataPreview`), so a write reads back at once. A view that refuses
|
|
12
|
+
* writes (readonly, a read-only document) refuses an assignment at once:
|
|
13
|
+
* nothing is queued, the dispatcher records `refused`, nothing throws.
|
|
14
|
+
*/
|
|
15
|
+
import type { DataPatch } from '../crdt/data.js';
|
|
16
|
+
import type { DataTarget } from '../crdt/doc/types.js';
|
|
17
|
+
import type { Edytor } from '../edytor.svelte.js';
|
|
18
|
+
export declare class DataWrites {
|
|
19
|
+
#private;
|
|
20
|
+
private readonly edytor;
|
|
21
|
+
constructor(edytor: Edytor);
|
|
22
|
+
/** The live proxy over `target`'s data; `read` is its reactive, committed source. */
|
|
23
|
+
proxy<T extends object>(target: DataTarget, read: () => Record<string, unknown>): T;
|
|
24
|
+
/** Queue `patches` on `target` for this task's dispatch (refused at once when the view refuses writes). */
|
|
25
|
+
write(target: DataTarget, patches: DataPatch[]): undefined;
|
|
26
|
+
/** Dispatch the queued writes now: before any other operation of this view, and at the end of the task. */
|
|
27
|
+
flush(): undefined;
|
|
28
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { propsProxy } from '../crdt/props.js';
|
|
2
|
+
const keyOf = (target) => JSON.stringify(target);
|
|
3
|
+
export class DataWrites {
|
|
4
|
+
edytor;
|
|
5
|
+
#pending = new Map();
|
|
6
|
+
#scheduled = false;
|
|
7
|
+
constructor(edytor) {
|
|
8
|
+
this.edytor = edytor;
|
|
9
|
+
}
|
|
10
|
+
/** The live proxy over `target`'s data; `read` is its reactive, committed source. */
|
|
11
|
+
proxy(target, read) {
|
|
12
|
+
let preview;
|
|
13
|
+
const pending = () => this.#pending.get(keyOf(target));
|
|
14
|
+
const current = () => {
|
|
15
|
+
const base = read();
|
|
16
|
+
const queued = pending();
|
|
17
|
+
if (!queued)
|
|
18
|
+
return base;
|
|
19
|
+
const { version } = this.edytor.facade;
|
|
20
|
+
const count = queued.patches.length;
|
|
21
|
+
if (preview?.version !== version || preview.count !== count) {
|
|
22
|
+
const seen = this.edytor.facade.dataPreview(target, queued.patches);
|
|
23
|
+
preview = { version, count, data: seen?.data ?? null };
|
|
24
|
+
}
|
|
25
|
+
return preview.data ?? base;
|
|
26
|
+
};
|
|
27
|
+
return propsProxy(current, (patches) => this.write(target, patches), (path) => {
|
|
28
|
+
const queued = pending();
|
|
29
|
+
return queued
|
|
30
|
+
? (this.edytor.facade.dataPreview(target, queued.patches)?.ids(path) ?? [])
|
|
31
|
+
: this.edytor.facade.dataItemIds(target, path);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Queue `patches` on `target` for this task's dispatch (refused at once when the view refuses writes). */
|
|
35
|
+
write(target, patches) {
|
|
36
|
+
const { edytor } = this;
|
|
37
|
+
if (!edytor.dispatcher.permits())
|
|
38
|
+
return void edytor.patchData(target, patches);
|
|
39
|
+
const key = keyOf(target);
|
|
40
|
+
const queued = this.#pending.get(key);
|
|
41
|
+
if (queued)
|
|
42
|
+
queued.patches.push(...patches);
|
|
43
|
+
else
|
|
44
|
+
this.#pending.set(key, { target, patches: [...patches] });
|
|
45
|
+
if (this.#scheduled)
|
|
46
|
+
return;
|
|
47
|
+
this.#scheduled = true;
|
|
48
|
+
queueMicrotask(() => {
|
|
49
|
+
this.#scheduled = false;
|
|
50
|
+
if (!edytor.destroyed)
|
|
51
|
+
this.flush();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Dispatch the queued writes now: before any other operation of this view, and at the end of the task. */
|
|
55
|
+
flush() {
|
|
56
|
+
if (this.#pending.size === 0)
|
|
57
|
+
return;
|
|
58
|
+
const all = [...this.#pending.values()];
|
|
59
|
+
this.#pending.clear();
|
|
60
|
+
const { edytor } = this;
|
|
61
|
+
if (all.length === 1)
|
|
62
|
+
return void edytor.patchData(all[0].target, all[0].patches);
|
|
63
|
+
edytor.dispatcher.run('patchData', () => edytor.transact(() => all.forEach(({ target, patches }) => edytor.patchData(target, patches))));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Suggestions (site `editor/suggestions`): content one view proposes at a
|
|
3
|
-
* position — AI output, a completion — shown only in that view until it is
|
|
4
|
-
* accepted. Session state: nothing reaches the document, its history,
|
|
5
|
-
* presence, `onChange` or another view before `accept()`, which is one
|
|
6
|
-
* command (`acceptSuggestion`: admission, hooks, one transaction, one undo
|
|
7
|
-
* step) placing the content as a paste places it (`flow.place`, `flow.slot`,
|
|
8
|
-
* `flow.inline`). A suggestion is anchored by block ids (a text range by its
|
|
9
|
-
* anchors): it survives selection changes and edits elsewhere, and is dropped
|
|
10
|
-
* when its block dies. The layer owns the list; the surface renders it.
|
|
11
|
-
*/
|
|
12
1
|
import type { BlockId, DocAnchor, OpResult } from '../crdt/index.js';
|
|
13
2
|
import type { Edytor } from '../edytor.svelte.js';
|
|
14
3
|
import { type JSONBlock } from '../utils/json.js';
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suggestions (site `editor/suggestions`): content one view proposes at a
|
|
3
|
+
* position — AI output, a completion — shown only in that view until it is
|
|
4
|
+
* accepted. Session state: nothing reaches the document, its history,
|
|
5
|
+
* presence, `onChange` or another view before `accept()`, which is one
|
|
6
|
+
* command (`acceptSuggestion`: admission, hooks, one transaction, one undo
|
|
7
|
+
* step) placing the content as a paste places it (`flow.place`, `flow.slot`,
|
|
8
|
+
* `flow.inline`). A suggestion is anchored by block ids (a text range by its
|
|
9
|
+
* anchors): it survives selection changes and edits elsewhere, and is dropped
|
|
10
|
+
* when its block dies. The layer owns the list; the surface renders it.
|
|
11
|
+
*/
|
|
12
|
+
import { DEV } from 'esm-env';
|
|
1
13
|
import { REFUSED } from '../crdt/doc/plan.js';
|
|
2
14
|
import { textToBlocks } from '../clipboard/textBlocks.js';
|
|
3
15
|
import { selectedMembers, viewOf } from '../selection/visibility.js';
|
|
@@ -329,17 +341,17 @@ export class Suggestions {
|
|
|
329
341
|
resolved = { replace: { anchor: value.anchor, focus: value.focus } };
|
|
330
342
|
else {
|
|
331
343
|
const block = value.kind === 'atom' ? value.blockId : projection.start?.block;
|
|
332
|
-
resolved = block ? { after: block } : fail('no selection to replace');
|
|
344
|
+
resolved = block ? { after: block } : fail(DEV ? 'no selection to replace' : '');
|
|
333
345
|
}
|
|
334
346
|
}
|
|
335
347
|
else
|
|
336
348
|
resolved = at;
|
|
337
349
|
if (!this.#alive(resolved))
|
|
338
|
-
fail('the position names no shown block');
|
|
350
|
+
fail(DEV ? 'the position names no shown block' : '');
|
|
339
351
|
if ('inside' in resolved) {
|
|
340
352
|
const probe = edytor.facade.prepare.insertBlocks({ parent: resolved.inside, index: 0 }, []);
|
|
341
353
|
if (!('writes' in probe))
|
|
342
|
-
fail(`block "${resolved.inside}" holds no children`);
|
|
354
|
+
fail(DEV ? `block "${resolved.inside}" holds no children` : '');
|
|
343
355
|
}
|
|
344
356
|
return resolved;
|
|
345
357
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The view's uploads as the other layers see them: the one per view the
|
|
3
|
+
* first media plugin given an `upload` claims (`uploadHooks`), and whether it takes a
|
|
4
|
+
* file (a kind's `upload` accepts it). A file dragged over the view shows
|
|
5
|
+
* where it lands only when its uploads take it (`drop`, the plugin layer's
|
|
6
|
+
* `plugins/fileDrop.ts`, Pragmatic's external adapter); a view without uploads ignores dropped files.
|
|
7
|
+
*/
|
|
8
|
+
import type { Edytor } from '../edytor.svelte.js';
|
|
9
|
+
import type { DropAt } from './attempt.js';
|
|
10
|
+
/** Files dragged from outside over the view: where they land. */
|
|
11
|
+
export interface FileDropPort {
|
|
12
|
+
/**
|
|
13
|
+
* The placement the drop of files being handled landed at, once (the
|
|
14
|
+
* plugin layer's drop target resolves it before the host's `drop`
|
|
15
|
+
* listener runs); `null` where none shows.
|
|
16
|
+
*/
|
|
17
|
+
take(): DropAt | null;
|
|
18
|
+
}
|
|
19
|
+
/** What the view's uploads answer about a file. */
|
|
20
|
+
export interface ViewUploads {
|
|
21
|
+
/** The kind that takes `file` (its record's `upload`), so a paste or a drop places it. */
|
|
22
|
+
kindOf(file: File): string | undefined;
|
|
23
|
+
/** Where dropped files land (the plugin layer's indicator). */
|
|
24
|
+
readonly drop: FileDropPort;
|
|
25
|
+
}
|
|
26
|
+
/** The view's uploads, when a plugin claimed them. */
|
|
27
|
+
export declare const viewUploads: (edytor: Edytor | undefined) => ViewUploads | undefined;
|
|
28
|
+
/** Claim the view's uploads for `uploads`: the first claim wins (`false` after it). */
|
|
29
|
+
export declare const claimUploads: (edytor: Edytor, uploads: ViewUploads) => boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const views = new WeakMap();
|
|
2
|
+
/** The view's uploads, when a plugin claimed them. */
|
|
3
|
+
export const viewUploads = (edytor) => edytor && views.get(edytor);
|
|
4
|
+
/** Claim the view's uploads for `uploads`: the first claim wins (`false` after it). */
|
|
5
|
+
export const claimUploads = (edytor, uploads) => {
|
|
6
|
+
if (views.has(edytor))
|
|
7
|
+
return false;
|
|
8
|
+
views.set(edytor, uploads);
|
|
9
|
+
return true;
|
|
10
|
+
};
|
package/dist/surface/lines.js
CHANGED
|
@@ -18,9 +18,7 @@ export const caretBox = (text, offset, after) => {
|
|
|
18
18
|
const [endNode, end] = domPointOf(element, from + 1);
|
|
19
19
|
range.setStart(startNode, start);
|
|
20
20
|
range.setEnd(endNode, end);
|
|
21
|
-
|
|
22
|
-
const list = typeof range.getClientRects === 'function' ? range.getClientRects() : null;
|
|
23
|
-
return [...(list ?? [])].find((r) => r.width > 0 || r.height > 0) ?? null;
|
|
21
|
+
return [...range.getClientRects()].find((r) => r.width > 0 || r.height > 0) ?? null;
|
|
24
22
|
};
|
|
25
23
|
const next = box(offset);
|
|
26
24
|
const previous = box(offset - 1);
|
|
@@ -2,6 +2,18 @@ import type { Edytor } from '../edytor.svelte.js';
|
|
|
2
2
|
import type { Text } from '../text/text.svelte.js';
|
|
3
3
|
import { type Kind, type Owned } from './attributes.js';
|
|
4
4
|
import { type Patched, type Replaced } from './cells.js';
|
|
5
|
+
/**
|
|
6
|
+
* What the browser shows for `text`: its element's text nodes in order (WebKit's
|
|
7
|
+
* converted-space wrappers read as plain spaces), the empty filler and the
|
|
8
|
+
* trailing-newline marker stripped.
|
|
9
|
+
*/
|
|
10
|
+
export declare const readDomText: (text: Text) => string;
|
|
11
|
+
export declare const getCollapsedDomTextSelection: (edytor: Edytor) => {
|
|
12
|
+
text: Text;
|
|
13
|
+
offset: number;
|
|
14
|
+
} | null;
|
|
15
|
+
/** A native line break (a diff inserting only newlines at `at`) runs as the key's occurrence. */
|
|
16
|
+
export declare const lineBreak: (edytor: Edytor, text: Text, value: string, at: number) => Promise<void>;
|
|
5
17
|
export declare class SurfaceObserver {
|
|
6
18
|
#private;
|
|
7
19
|
private edytor;
|
|
@@ -12,7 +24,11 @@ export declare class SurfaceObserver {
|
|
|
12
24
|
constructor(edytor: Edytor);
|
|
13
25
|
/** Observe `root` (the host): records name what to compare. */
|
|
14
26
|
attach: (root: HTMLElement) => () => void;
|
|
15
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Register an element the core rendered (an attachment); returns its
|
|
29
|
+
* release. A text element takes the table's attributes now (its id, its
|
|
30
|
+
* editing host inside a void).
|
|
31
|
+
*/
|
|
16
32
|
register: (node: Element, kind: Kind, block: string, name?: string, owned?: () => Owned) => () => void;
|
|
17
33
|
/** The render anchor (`Edytor.svelte`): a registered child of the strict root. */
|
|
18
34
|
anchor: (node: HTMLElement) => {
|
|
@@ -38,7 +38,9 @@ import { diffText } from '../utils/diffText.js';
|
|
|
38
38
|
import { jsonEquals } from '../utils/json.js';
|
|
39
39
|
import { activeMarks, marksForInsertion } from '../session/editing/text.js';
|
|
40
40
|
import { INTENTS } from '../session/attempt.js';
|
|
41
|
-
import {
|
|
41
|
+
import { runOccurrence } from '../events/onBeforeInput.js';
|
|
42
|
+
import { getDomSelectionSnapshot } from '../selection/domSelection.js';
|
|
43
|
+
import { domOffsetIn } from '../selection/selection.utils.js';
|
|
42
44
|
import { diverges, heal, ownedOf } from './attributes.js';
|
|
43
45
|
import { getTextContentOffsetAtPoint } from '../events/domTextOffset.js';
|
|
44
46
|
import { rendersMark } from './cells.js';
|
|
@@ -67,22 +69,68 @@ const textOf = (cell) => cell.runs
|
|
|
67
69
|
.replace(ZWSP, '');
|
|
68
70
|
/** Svelte's anchors: comments and empty text nodes are never input and never removed. */
|
|
69
71
|
const isAnchor = (node) => node.nodeType === Node.COMMENT_NODE || (node.nodeType === Node.TEXT_NODE && !node.nodeValue);
|
|
70
|
-
/**
|
|
71
|
-
const
|
|
72
|
+
/** The `nth` non-empty text node under `node`. */
|
|
73
|
+
const textLeaf = (node, nth = 0) => {
|
|
72
74
|
const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
73
|
-
let count = 0;
|
|
74
75
|
while (walker.nextNode())
|
|
75
|
-
if (walker.currentNode.data)
|
|
76
|
-
count++;
|
|
77
|
-
return count;
|
|
78
|
-
};
|
|
79
|
-
const firstLeaf = (node) => {
|
|
80
|
-
const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
81
|
-
while (walker.nextNode())
|
|
82
|
-
if (walker.currentNode.data)
|
|
76
|
+
if (walker.currentNode.data && nth-- === 0)
|
|
83
77
|
return walker.currentNode;
|
|
84
78
|
return null;
|
|
85
79
|
};
|
|
80
|
+
const ZERO_WIDTH_SPACE = '\u200B';
|
|
81
|
+
/**
|
|
82
|
+
* What the browser shows for `text`: its element's text nodes in order (WebKit's
|
|
83
|
+
* converted-space wrappers read as plain spaces), the empty filler and the
|
|
84
|
+
* trailing-newline marker stripped.
|
|
85
|
+
*/
|
|
86
|
+
export const readDomText = (text) => {
|
|
87
|
+
let value = '';
|
|
88
|
+
const node = text.node;
|
|
89
|
+
if (node) {
|
|
90
|
+
const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
91
|
+
while (walker.nextNode()) {
|
|
92
|
+
const leaf = walker.currentNode;
|
|
93
|
+
value += leaf.parentElement?.closest('span.Apple-converted-space')
|
|
94
|
+
? leaf.data.replaceAll('\u00A0', ' ')
|
|
95
|
+
: leaf.data;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// An empty text's filler shares its node with what the browser typed, on either side.
|
|
99
|
+
if (text.isEmpty)
|
|
100
|
+
return value.replaceAll(ZERO_WIDTH_SPACE, '');
|
|
101
|
+
if (text.endsWithNewline && value.endsWith(ZERO_WIDTH_SPACE)) {
|
|
102
|
+
value = value.slice(0, -ZERO_WIDTH_SPACE.length);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
};
|
|
106
|
+
export const getCollapsedDomTextSelection = (edytor) => {
|
|
107
|
+
const selection = getDomSelectionSnapshot(edytor.node);
|
|
108
|
+
if (!selection?.anchorNode || !selection.isCollapsed) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (!edytor.node?.contains(selection.anchorNode)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const text = edytor.selection.getTextOfNode(selection.anchorNode);
|
|
115
|
+
if (!text?.node) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
// No model rule here: the DOM caret is read BEFORE the reconcile writes
|
|
119
|
+
// the model, so a native insertion legitimately sits past `text.length`
|
|
120
|
+
// (`selectText` clamps the final write).
|
|
121
|
+
return { text, offset: domOffsetIn(text, selection.anchorNode, selection.anchorOffset) };
|
|
122
|
+
};
|
|
123
|
+
/** A native line break (a diff inserting only newlines at `at`) runs as the key's occurrence. */
|
|
124
|
+
export const lineBreak = async (edytor, text, value, at) => {
|
|
125
|
+
if (!text.isInDocument)
|
|
126
|
+
return;
|
|
127
|
+
const inputType = edytor.attempts.confirm()?.inputType === 'insertLineBreak' || value.includes('\n')
|
|
128
|
+
? 'insertLineBreak'
|
|
129
|
+
: 'insertParagraph';
|
|
130
|
+
text.refreshFromModel();
|
|
131
|
+
edytor.selection.selectText(text, at);
|
|
132
|
+
await runOccurrence(edytor, { inputType, cancelable: false });
|
|
133
|
+
};
|
|
86
134
|
const settle = (promises) => Promise.all(promises).then(() => undefined);
|
|
87
135
|
/** The collapsed DOM caret's text offset inside `nodes` (a replacement's), if it is there. */
|
|
88
136
|
const caretIn = (nodes) => {
|
|
@@ -167,9 +215,16 @@ export class SurfaceObserver {
|
|
|
167
215
|
clearTimeout(this.#window ?? undefined);
|
|
168
216
|
};
|
|
169
217
|
};
|
|
170
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* Register an element the core rendered (an attachment); returns its
|
|
220
|
+
* release. A text element takes the table's attributes now (its id, its
|
|
221
|
+
* editing host inside a void).
|
|
222
|
+
*/
|
|
171
223
|
register = (node, kind, block, name, owned) => {
|
|
172
|
-
|
|
224
|
+
const entry = { kind, block, name, owned };
|
|
225
|
+
this.#registry.set(node, entry);
|
|
226
|
+
if (kind === 'text')
|
|
227
|
+
heal(node, ownedOf(this.edytor, node, entry));
|
|
173
228
|
if (kind === 'block')
|
|
174
229
|
this.#blocks.set(block, node);
|
|
175
230
|
else if (kind !== 'anchor' && kind !== 'preview') {
|
|
@@ -579,7 +634,7 @@ export class SurfaceObserver {
|
|
|
579
634
|
const shown = deltas.length ? deltas : [{ text: '\u200B' }];
|
|
580
635
|
for (const [i, delta] of shown.entries()) {
|
|
581
636
|
const node = nodes[i];
|
|
582
|
-
const leaf = node?.nodeType === Node.TEXT_NODE ? node : node &&
|
|
637
|
+
const leaf = node?.nodeType === Node.TEXT_NODE ? node : node && textLeaf(node);
|
|
583
638
|
if (!leaf)
|
|
584
639
|
return false;
|
|
585
640
|
if (leaf.data !== delta.text)
|
|
@@ -614,7 +669,7 @@ export class SurfaceObserver {
|
|
|
614
669
|
const at = replaced ? replaced.caret : caret?.text === text ? caret.offset : undefined;
|
|
615
670
|
const line = diffText(text.stringContent, dom);
|
|
616
671
|
if (line && !line.remove && /^[\r\n]+$/.test(line.insert)) {
|
|
617
|
-
work.push(
|
|
672
|
+
work.push(lineBreak(edytor, text, dom, line.at));
|
|
618
673
|
continue;
|
|
619
674
|
}
|
|
620
675
|
work.push(adopt(edytor, text, dom, at));
|
|
@@ -698,7 +753,7 @@ export class SurfaceObserver {
|
|
|
698
753
|
if (marks.join() !== chain.join())
|
|
699
754
|
return 'mark-chain';
|
|
700
755
|
// The snippet's markup is its own, but the delta renders one text node.
|
|
701
|
-
if (
|
|
756
|
+
if (!textLeaf(node) || textLeaf(node, 1))
|
|
702
757
|
return 'mark-text';
|
|
703
758
|
}
|
|
704
759
|
}
|
|
@@ -50,20 +50,6 @@ export declare const pin: (host: HTMLElement) => CSSStyleDeclaration & {
|
|
|
50
50
|
left: string;
|
|
51
51
|
top: string;
|
|
52
52
|
};
|
|
53
|
-
/**
|
|
54
|
-
* A popup's place near `rect` (client px), 8px inside the viewport: `gap`
|
|
55
|
-
* below it, above it when there is no room below (`above`: above first,
|
|
56
|
-
* below when there is no room above), its left edge at `rect`'s or
|
|
57
|
-
* centered on it (`center`). Measured now from `box` (the host's first
|
|
58
|
-
* element by default; `fallback` before it has a size), written by the
|
|
59
|
-
* answer (the overlay's read-then-write pass).
|
|
60
|
-
*/
|
|
61
|
-
export declare const placeNear: (host: HTMLElement, rect: DOMRect, fallback: readonly [width: number, height: number], { gap, above, center, box }?: {
|
|
62
|
-
gap?: number;
|
|
63
|
-
above?: boolean;
|
|
64
|
-
center?: boolean;
|
|
65
|
-
box?: Element | null;
|
|
66
|
-
}) => (() => void) | void;
|
|
67
53
|
export declare class Overlay {
|
|
68
54
|
#private;
|
|
69
55
|
/** The layer: absolutely positioned at its static place right after the host. */
|
package/dist/surface/overlay.js
CHANGED
|
@@ -68,31 +68,6 @@ export const sameValues = (a, b) => a === b ||
|
|
|
68
68
|
Object.keys(a).every((key) => a[key] === b[key]));
|
|
69
69
|
/** A mounted host whose chrome places itself layer-relative: pinned at the layer's origin. */
|
|
70
70
|
export const pin = (host) => Object.assign(host.style, { position: 'absolute', left: '0px', top: '0px' });
|
|
71
|
-
/**
|
|
72
|
-
* A popup's place near `rect` (client px), 8px inside the viewport: `gap`
|
|
73
|
-
* below it, above it when there is no room below (`above`: above first,
|
|
74
|
-
* below when there is no room above), its left edge at `rect`'s or
|
|
75
|
-
* centered on it (`center`). Measured now from `box` (the host's first
|
|
76
|
-
* element by default; `fallback` before it has a size), written by the
|
|
77
|
-
* answer (the overlay's read-then-write pass).
|
|
78
|
-
*/
|
|
79
|
-
export const placeNear = (host, rect, fallback, { gap = 8, above = false, center = false, box = host.firstElementChild } = {}) => {
|
|
80
|
-
const view = host.ownerDocument.defaultView;
|
|
81
|
-
if (!view)
|
|
82
|
-
return;
|
|
83
|
-
const size = box?.getBoundingClientRect();
|
|
84
|
-
const width = size?.width || fallback[0];
|
|
85
|
-
const height = size?.height || fallback[1];
|
|
86
|
-
const x = center ? rect.left + rect.width / 2 - width / 2 : rect.left;
|
|
87
|
-
const [below, up] = [rect.bottom + gap, rect.top - gap - height];
|
|
88
|
-
const y = above
|
|
89
|
-
? up >= 8
|
|
90
|
-
? up
|
|
91
|
-
: below
|
|
92
|
-
: Math.max(8, below + height < view.innerHeight ? below : up);
|
|
93
|
-
const left = `${Math.max(8, Math.min(x, view.innerWidth - width - 8))}px`;
|
|
94
|
-
return () => Object.assign(host.style, { left, top: `${y}px` });
|
|
95
|
-
};
|
|
96
71
|
/** The band around the viewport (`near`) whose blocks' resizes are watched, in screens above and below. */
|
|
97
72
|
const NEAR_SCREENS = 1;
|
|
98
73
|
export class Overlay {
|
|
@@ -467,7 +467,7 @@ export class Projector {
|
|
|
467
467
|
};
|
|
468
468
|
#write = (dom, { anchor, focus }, collapsed) => {
|
|
469
469
|
this.#parking = false;
|
|
470
|
-
if (!collapsed
|
|
470
|
+
if (!collapsed) {
|
|
471
471
|
try {
|
|
472
472
|
dom.setBaseAndExtent(anchor[0], anchor[1], focus[0], focus[1]);
|
|
473
473
|
return;
|
package/dist/text/text.svelte.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cleanups } from '../utils.js';
|
|
1
2
|
import {} from '../utils/json.js';
|
|
2
3
|
import { segmentJSON } from '../session/handles.js';
|
|
3
4
|
/**
|
|
@@ -101,20 +102,9 @@ export class Text {
|
|
|
101
102
|
attach = (node) => {
|
|
102
103
|
this.node = node;
|
|
103
104
|
this.edytor.nodeToText.set(node, this);
|
|
105
|
+
// Registered, it takes the attribute table's attributes (`surface/attributes.ts`).
|
|
104
106
|
const release = this.edytor.surface.register(node, 'text', this.parent.id);
|
|
105
|
-
|
|
106
|
-
node.setAttribute('data-edytor-text', `true`);
|
|
107
|
-
const pluginDestroy = this.edytor.plugins.flatMap((plugin) => {
|
|
108
|
-
const action = plugin.onTextAttached?.({ node, text: this });
|
|
109
|
-
return typeof action === 'function' ? [action] : [];
|
|
110
|
-
});
|
|
111
|
-
let insideVoid = false;
|
|
112
|
-
for (let block = this.parent; block && !insideVoid; block = block.parent)
|
|
113
|
-
insideVoid = !!block.definition.void;
|
|
114
|
-
if (insideVoid) {
|
|
115
|
-
node.setAttribute('contenteditable', 'true');
|
|
116
|
-
node.style.outline = 'none';
|
|
117
|
-
}
|
|
107
|
+
const pluginDestroy = cleanups(this.edytor.plugins, (plugin) => plugin.onTextAttached?.({ node, text: this }));
|
|
118
108
|
// A display that waited for a mounted destination runs in the next pass.
|
|
119
109
|
this.edytor.projector?.mounted(this);
|
|
120
110
|
return {
|
package/dist/themes/notion.css
CHANGED
|
@@ -729,6 +729,15 @@
|
|
|
729
729
|
}
|
|
730
730
|
|
|
731
731
|
/* ── Columns ────────────────────────────────────────────────────────── */
|
|
732
|
+
/* A layout and its columns reserve no inline space: a column's blocks start
|
|
733
|
+
at the column's edge (Notion's), its handles stand in the gap before it. */
|
|
734
|
+
.edytor-notion
|
|
735
|
+
:is(
|
|
736
|
+
[data-edytor-block][data-edytor-columns],
|
|
737
|
+
[data-edytor-block][data-edytor-column]:not([data-edytor-table-cell])
|
|
738
|
+
) {
|
|
739
|
+
padding-inline: 0;
|
|
740
|
+
}
|
|
732
741
|
/* A column's first and last blocks sit flush with the layout's edges. */
|
|
733
742
|
.edytor-notion [data-edytor-column] > [data-edytor-children] > [data-edytor-block]:first-child {
|
|
734
743
|
margin-top: 0;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sanitizers every reader of untrusted values shares: a link's href and
|
|
3
|
+
* a CSS colour (marks), read the same by render, the clipboard, HTML and
|
|
4
|
+
* markdown import and export. Pure: no DOM, no Svelte, Worker-safe.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* CSS-color sanitizer for `color`/`highlight` marks. The value lands in
|
|
8
|
+
* `style="color: {value}"` — a `;`/`{`/`}` payload escapes the property
|
|
9
|
+
* and injects arbitrary declarations (`url()` exfil, `position:fixed`
|
|
10
|
+
* overlays). Metacharacters, escape/obfuscation sequences, and
|
|
11
|
+
* resource-loading functions are rejected; real `<color>` values
|
|
12
|
+
* (`#hex`, `rgb()/hsl()/oklch()/color-mix()/var()`, named colors) pass.
|
|
13
|
+
*/
|
|
14
|
+
export declare const sanitizeCssColorValue: (value: unknown) => string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Scheme allowlist for link hrefs. `javascript:`/`data:`/`vbscript:`
|
|
17
|
+
* payloads can arrive through native `insertLink`, pasted HTML, or a
|
|
18
|
+
* malicious collaborator — and Svelte renders `href` verbatim. The href is
|
|
19
|
+
* read as the browser's URL parser reads it: it strips C0 controls and
|
|
20
|
+
* spaces at both ends and tab/newline/CR anywhere (`\u0001javascript:` is
|
|
21
|
+
* `javascript:` to a browser), then resolves it against a base; only an
|
|
22
|
+
* allowed scheme, or a relative href that stays relative, passes.
|
|
23
|
+
*/
|
|
24
|
+
export declare const sanitizeLinkHref: (href: unknown) => string | null;
|
|
25
|
+
/**
|
|
26
|
+
* A web page's URL a media block may store or render: the link sanitizer's
|
|
27
|
+
* reading of it ({@link sanitizeLinkHref}: ASCII control characters and spaces stripped,
|
|
28
|
+
* tab/newline anywhere), then only an absolute `http:`/`https:` URL with a
|
|
29
|
+
* host. `null` for anything else: a script or `data:` URL, a `mailto:`, a
|
|
30
|
+
* relative path, a bare domain.
|
|
31
|
+
*/
|
|
32
|
+
export declare const safeWebUrl: (value: unknown) => string | null;
|
|
33
|
+
/**
|
|
34
|
+
* A media file's source (a video, an audio track, an attachment): a
|
|
35
|
+
* {@link safeWebUrl}, or a `blob:` URL an upload answered. Never `data:`:
|
|
36
|
+
* an inline file would be stored in the document and sent in every update.
|
|
37
|
+
*/
|
|
38
|
+
export declare const safeMediaSrc: (value: unknown) => string | null;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sanitizers every reader of untrusted values shares: a link's href and
|
|
3
|
+
* a CSS colour (marks), read the same by render, the clipboard, HTML and
|
|
4
|
+
* markdown import and export. Pure: no DOM, no Svelte, Worker-safe.
|
|
5
|
+
*/
|
|
6
|
+
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
|
|
7
|
+
/**
|
|
8
|
+
* CSS-color sanitizer for `color`/`highlight` marks. The value lands in
|
|
9
|
+
* `style="color: {value}"` — a `;`/`{`/`}` payload escapes the property
|
|
10
|
+
* and injects arbitrary declarations (`url()` exfil, `position:fixed`
|
|
11
|
+
* overlays). Metacharacters, escape/obfuscation sequences, and
|
|
12
|
+
* resource-loading functions are rejected; real `<color>` values
|
|
13
|
+
* (`#hex`, `rgb()/hsl()/oklch()/color-mix()/var()`, named colors) pass.
|
|
14
|
+
*/
|
|
15
|
+
export const sanitizeCssColorValue = (value) => {
|
|
16
|
+
if (typeof value !== 'string')
|
|
17
|
+
return null;
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
if (!trimmed || trimmed.length > 128)
|
|
20
|
+
return null;
|
|
21
|
+
if (/[;{}<>\\'"`]/.test(trimmed) || /url\s*\(|expression\s*\(|\/\*|\*\//i.test(trimmed)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return trimmed;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Scheme allowlist for link hrefs. `javascript:`/`data:`/`vbscript:`
|
|
28
|
+
* payloads can arrive through native `insertLink`, pasted HTML, or a
|
|
29
|
+
* malicious collaborator — and Svelte renders `href` verbatim. The href is
|
|
30
|
+
* read as the browser's URL parser reads it: it strips C0 controls and
|
|
31
|
+
* spaces at both ends and tab/newline/CR anywhere (`\u0001javascript:` is
|
|
32
|
+
* `javascript:` to a browser), then resolves it against a base; only an
|
|
33
|
+
* allowed scheme, or a relative href that stays relative, passes.
|
|
34
|
+
*/
|
|
35
|
+
export const sanitizeLinkHref = (href) => {
|
|
36
|
+
const trimmed = hrefOf(href);
|
|
37
|
+
// A relative href resolves to the base's `https:`: it names no scheme of its own.
|
|
38
|
+
return trimmed &&
|
|
39
|
+
SAFE_LINK_PROTOCOLS.has(urlOf(trimmed, 'https://relative.invalid/')?.protocol ?? '')
|
|
40
|
+
? trimmed
|
|
41
|
+
: null;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* A href as the browser's URL parser reads it: C0 controls and spaces
|
|
45
|
+
* stripped at both ends, tab/newline/CR anywhere; `''` for a non-string
|
|
46
|
+
* (marks arrive from untrusted sources too: synced peers, paste).
|
|
47
|
+
*/
|
|
48
|
+
const hrefOf = (href) => typeof href === 'string'
|
|
49
|
+
? // eslint-disable-next-line no-control-regex
|
|
50
|
+
href.replace(/[\t\n\r]/g, '').replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, '')
|
|
51
|
+
: '';
|
|
52
|
+
/** `href` parsed (against `base`), or `undefined` when it is no URL. */
|
|
53
|
+
const urlOf = (href, base) => {
|
|
54
|
+
try {
|
|
55
|
+
return new URL(href, base);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* A web page's URL a media block may store or render: the link sanitizer's
|
|
63
|
+
* reading of it ({@link sanitizeLinkHref}: ASCII control characters and spaces stripped,
|
|
64
|
+
* tab/newline anywhere), then only an absolute `http:`/`https:` URL with a
|
|
65
|
+
* host. `null` for anything else: a script or `data:` URL, a `mailto:`, a
|
|
66
|
+
* relative path, a bare domain.
|
|
67
|
+
*/
|
|
68
|
+
export const safeWebUrl = (value) => {
|
|
69
|
+
const href = hrefOf(value);
|
|
70
|
+
const url = href ? urlOf(href) : undefined;
|
|
71
|
+
return url && /^https?:$/.test(url.protocol) && url.host ? href : null;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* A media file's source (a video, an audio track, an attachment): a
|
|
75
|
+
* {@link safeWebUrl}, or a `blob:` URL an upload answered. Never `data:`:
|
|
76
|
+
* an inline file would be stored in the document and sent in every update.
|
|
77
|
+
*/
|
|
78
|
+
export const safeMediaSrc = (value) => {
|
|
79
|
+
const blob = typeof value === 'string' ? /^blob:(.*)$/is.exec(value.trim()) : null;
|
|
80
|
+
const origin = blob && safeWebUrl(blob[1]);
|
|
81
|
+
return origin ? `blob:${origin}` : safeWebUrl(value);
|
|
82
|
+
};
|