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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fileUploads } from '../uploads.svelte.js';
|
|
2
2
|
import { listen } from '../../utils/listen.js';
|
|
3
|
+
import { keysOf } from '../list.svelte.js';
|
|
3
4
|
/** Each media kind's icon (the image's too): its menu row, its empty block, its paste offer. */
|
|
4
5
|
export const MEDIA_ICONS = {
|
|
5
6
|
image: '🖼',
|
|
@@ -54,7 +55,7 @@ export class MediaEmptyController {
|
|
|
54
55
|
get readonly() {
|
|
55
56
|
return !this.block || this.block.edytor.readonly;
|
|
56
57
|
}
|
|
57
|
-
/** The
|
|
58
|
+
/** The kind's plugin was given an `upload`: offer Upload. */
|
|
58
59
|
get canUpload() {
|
|
59
60
|
const { block } = this;
|
|
60
61
|
return Boolean(block && fileUploads(block.edytor) && block.definition.upload);
|
|
@@ -64,6 +65,19 @@ export class MediaEmptyController {
|
|
|
64
65
|
const { block } = this;
|
|
65
66
|
return block ? fileUploads(block.edytor)?.of(block.id) : undefined;
|
|
66
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The file uploading into this block, shown at once: its object URL
|
|
70
|
+
* (this view's only, never stored; revoked once the upload ends), else `null`.
|
|
71
|
+
*/
|
|
72
|
+
get preview() {
|
|
73
|
+
const { pending } = this;
|
|
74
|
+
return pending?.status === 'uploading' ? pending.preview : null;
|
|
75
|
+
}
|
|
76
|
+
/** The share of the upload sent, `0` to `1`; `null` before a report, or with no upload. */
|
|
77
|
+
get progress() {
|
|
78
|
+
const { pending } = this;
|
|
79
|
+
return pending?.status === 'uploading' ? pending.progress : null;
|
|
80
|
+
}
|
|
67
81
|
/** Store `value` on the block: why it did not take it, or `null`. */
|
|
68
82
|
#store = (value, file) => this.block && this.view ? this.view.store(this.block, value, file) : 'invalid';
|
|
69
83
|
/** Embed `value` (default the `draft`): the block takes the link, or `failed` says why not. */
|
|
@@ -73,39 +87,31 @@ export class MediaEmptyController {
|
|
|
73
87
|
fileUploads(this.block.edytor)?.clear(this.block.id);
|
|
74
88
|
};
|
|
75
89
|
/**
|
|
76
|
-
* Upload `file` with
|
|
77
|
-
*
|
|
78
|
-
*
|
|
90
|
+
* Upload `file` with its kind's uploader (its plugin's `upload`), the
|
|
91
|
+
* block showing it meanwhile (`pending`, `preview`, `progress`), and
|
|
92
|
+
* store the URL it answers, only on the block as it was picked from:
|
|
93
|
+
* still in the tree, of its kind and empty (a peer may have set a
|
|
94
|
+
* source, or deleted it, meanwhile).
|
|
79
95
|
*/
|
|
80
96
|
upload = async (picked) => {
|
|
81
97
|
const { block } = this;
|
|
82
|
-
const
|
|
83
|
-
if (!
|
|
98
|
+
const uploads = this.canUpload && fileUploads(block?.edytor);
|
|
99
|
+
if (!uploads || !block || block.edytor.readonly)
|
|
84
100
|
return;
|
|
85
101
|
const { edytor, id, type } = block;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
const stored = await uploads.pick(id, picked, (src) => {
|
|
103
|
+
const live = edytor.idToBlock.get(id);
|
|
104
|
+
if (!live?.isInTree || live.type !== type || edytor.readonly)
|
|
105
|
+
return;
|
|
106
|
+
if (edytor.facade.blockDataOf(id)?.src !== undefined)
|
|
107
|
+
return;
|
|
108
|
+
this.failed = this.#store(src, picked);
|
|
109
|
+
});
|
|
110
|
+
if (!stored)
|
|
91
111
|
this.failed = 'upload';
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const live = edytor.idToBlock.get(id);
|
|
95
|
-
if (!live?.isInTree || live.type !== type || edytor.readonly)
|
|
96
|
-
return;
|
|
97
|
-
if (edytor.facade.blockDataOf(id)?.src !== undefined)
|
|
98
|
-
return;
|
|
99
|
-
this.failed = this.#store(src, picked);
|
|
100
112
|
};
|
|
101
113
|
/** The link field (`{@attach empty.field}`): Enter embeds its `draft`. */
|
|
102
114
|
field = (node) => {
|
|
103
|
-
|
|
104
|
-
if (event.key !== 'Enter' || event.isComposing)
|
|
105
|
-
return;
|
|
106
|
-
event.preventDefault();
|
|
107
|
-
this.embed();
|
|
108
|
-
};
|
|
109
|
-
return listen(node, 'keydown', keydown);
|
|
115
|
+
return listen(node, 'keydown', keysOf({ Enter: () => this.embed() }));
|
|
110
116
|
};
|
|
111
117
|
}
|
|
@@ -49,6 +49,13 @@ export type FileRowPayload = {
|
|
|
49
49
|
size: number | undefined;
|
|
50
50
|
/** That size in the view's words (`media.fileSize`), `''` without one. */
|
|
51
51
|
sizeLabel: string;
|
|
52
|
+
/**
|
|
53
|
+
* The file is uploading (then `src` is its object URL, this view's only,
|
|
54
|
+
* never stored): the row shows it at once, with `progress`.
|
|
55
|
+
*/
|
|
56
|
+
uploading: boolean;
|
|
57
|
+
/** The share sent while it uploads, `0` to `1`; `null` before a report or once stored. */
|
|
58
|
+
progress: number | null;
|
|
52
59
|
/**
|
|
53
60
|
* The row's link (`<a href={row.src} {@attach row.link}>`): it downloads
|
|
54
61
|
* the file under its name, in a new tab without the opener, never dragged
|
|
@@ -94,67 +101,6 @@ export declare const mediaPlugin: (kind: MediaKind, keywords: string[], own: (ed
|
|
|
94
101
|
hooks?: PluginOperations & PluginDefinitions;
|
|
95
102
|
record: Partial<BlockDefinition>;
|
|
96
103
|
}) => Plugin;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
* reading of it (`sanitizeLinkHref`: ASCII control characters and spaces stripped,
|
|
100
|
-
* tab/newline anywhere), then only an absolute `http:`/`https:` URL with a
|
|
101
|
-
* host. `null` for anything else: a script or `data:` URL, a `mailto:`, a
|
|
102
|
-
* relative path, a bare domain.
|
|
103
|
-
*/
|
|
104
|
-
export declare const safeWebUrl: (value: unknown) => string | null;
|
|
105
|
-
/**
|
|
106
|
-
* A media file's source (a video, an audio track, an attachment): a
|
|
107
|
-
* {@link safeWebUrl}, or a `blob:` URL an upload answered. Never `data:`:
|
|
108
|
-
* an inline file would be stored in the document and sent in every update.
|
|
109
|
-
*/
|
|
110
|
-
export declare const safeMediaSrc: (value: unknown) => string | null;
|
|
111
|
-
/** One allowlisted embed provider: which links it plays, and its player. */
|
|
112
|
-
export type EmbedProvider = {
|
|
113
|
-
/** The provider's name: the frame's `title`. */
|
|
114
|
-
name: string;
|
|
115
|
-
/**
|
|
116
|
-
* The player's URL for a link of this provider, else `null`. It must be
|
|
117
|
-
* `https:` on another origin than the page's: any other answer is
|
|
118
|
-
* refused (the frame has `allow-scripts` and `allow-same-origin`, so a
|
|
119
|
-
* player on the page's origin, or on one that can script it, could lift
|
|
120
|
-
* its own sandbox). Match the host exactly (never a
|
|
121
|
-
* suffix or a substring) and check every id you copy into the player URL.
|
|
122
|
-
*/
|
|
123
|
-
embed: (url: URL) => string | null;
|
|
124
|
-
/** The frame's CSS `aspect-ratio` (default `16 / 9`); ignored with `height`. */
|
|
125
|
-
aspectRatio?: string;
|
|
126
|
-
/** A fixed frame height in pixels (a music player). */
|
|
127
|
-
height?: number;
|
|
128
|
-
};
|
|
129
|
-
/** The frame's `sandbox`: scripts and the provider's own origin, popups that leave it; no top navigation, modals, downloads or forms. */
|
|
130
|
-
export declare const EMBED_SANDBOX = "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation";
|
|
131
|
-
/** The frame's permissions (`allow`): what a player needs, nothing that reads the viewer. */
|
|
132
|
-
export declare const EMBED_ALLOW = "fullscreen; picture-in-picture; encrypted-media; clipboard-write";
|
|
133
|
-
/**
|
|
134
|
-
* The default allowlist: YouTube (through `youtube-nocookie.com`), Vimeo,
|
|
135
|
-
* Loom, Figma, CodePen and Spotify. A link no provider plays renders no
|
|
136
|
-
* frame. Extend it with `createEmbedPlugin({ providers: [...EMBED_PROVIDERS, mine] })`.
|
|
137
|
-
*/
|
|
138
|
-
export declare const EMBED_PROVIDERS: readonly EmbedProvider[];
|
|
139
|
-
/**
|
|
140
|
-
* The player for `value` (a stored or pasted link): the first provider of
|
|
141
|
-
* `providers` that plays it, and its `https:` player URL. `null` when the
|
|
142
|
-
* link is not a {@link safeWebUrl}, no provider plays it, or the player URL
|
|
143
|
-
* a provider answers is not `https:` or is on the page's own origin. The frame's `src` is always this
|
|
144
|
-
* answer, never the stored value.
|
|
145
|
-
*/
|
|
146
|
-
export declare const embedSourceOf: (value: unknown, providers?: readonly EmbedProvider[]) => {
|
|
147
|
-
src: string;
|
|
148
|
-
provider: EmbedProvider;
|
|
149
|
-
} | null;
|
|
150
|
-
/** A file's display name from its URL's last path segment, decoded. */
|
|
151
|
-
export declare const fileNameOf: (src: string) => string;
|
|
152
|
-
/**
|
|
153
|
-
* The element a media kind's HTML import reads: `element` itself when it
|
|
154
|
-
* is a `tag` (not inside a `figure`, which claims it), or a `figure`'s own
|
|
155
|
-
* `tag` child.
|
|
156
|
-
*/
|
|
157
|
-
export declare const claimed: (element: HTMLElement, tag: string) => HTMLElement | undefined;
|
|
158
|
-
/** An imported `video`'s or `audio`'s source: its `src`, else its first `source`'s. */
|
|
159
|
-
export declare const mediaSourceOf: (element: HTMLElement | undefined) => string | null;
|
|
104
|
+
export * from './sources.js';
|
|
105
|
+
import type { EmbedProvider } from './sources.js';
|
|
160
106
|
export { escapeHtml };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { attach } from '../chrome.js';
|
|
2
2
|
import { mediaKinds } from '../../crdt/semantics.js';
|
|
3
|
-
import { sanitizeLinkHref } from '../richtext/richTextOperations.js';
|
|
4
3
|
import { escapeHtml } from '../../clipboard/serializeClipboardFragment.js';
|
|
5
4
|
import { keywordsOf, labelsOf, mediaEnglish } from '../../labels.js';
|
|
6
5
|
import { MEDIA_ICONS } from './empty.svelte.js';
|
|
@@ -56,197 +55,5 @@ export const mediaPlugin = (kind, keywords, own) => (edytor) => {
|
|
|
56
55
|
}
|
|
57
56
|
};
|
|
58
57
|
};
|
|
59
|
-
|
|
60
|
-
* A web page's URL a media block may store or render: the link sanitizer's
|
|
61
|
-
* reading of it (`sanitizeLinkHref`: ASCII control characters and spaces stripped,
|
|
62
|
-
* tab/newline anywhere), then only an absolute `http:`/`https:` URL with a
|
|
63
|
-
* host. `null` for anything else: a script or `data:` URL, a `mailto:`, a
|
|
64
|
-
* relative path, a bare domain.
|
|
65
|
-
*/
|
|
66
|
-
export const safeWebUrl = (value) => {
|
|
67
|
-
const href = sanitizeLinkHref(value);
|
|
68
|
-
if (href === null)
|
|
69
|
-
return null;
|
|
70
|
-
try {
|
|
71
|
-
const url = new URL(href);
|
|
72
|
-
return (url.protocol === 'http:' || url.protocol === 'https:') && url.host ? href : null;
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
/**
|
|
79
|
-
* A media file's source (a video, an audio track, an attachment): a
|
|
80
|
-
* {@link safeWebUrl}, or a `blob:` URL an upload answered. Never `data:`:
|
|
81
|
-
* an inline file would be stored in the document and sent in every update.
|
|
82
|
-
*/
|
|
83
|
-
export const safeMediaSrc = (value) => {
|
|
84
|
-
const blob = typeof value === 'string' ? /^blob:(.*)$/is.exec(value.trim()) : null;
|
|
85
|
-
const origin = blob && safeWebUrl(blob[1]);
|
|
86
|
-
return origin ? `blob:${origin}` : safeWebUrl(value);
|
|
87
|
-
};
|
|
88
|
-
/** The frame's `sandbox`: scripts and the provider's own origin, popups that leave it; no top navigation, modals, downloads or forms. */
|
|
89
|
-
export const EMBED_SANDBOX = 'allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation';
|
|
90
|
-
/** The frame's permissions (`allow`): what a player needs, nothing that reads the viewer. */
|
|
91
|
-
export const EMBED_ALLOW = 'fullscreen; picture-in-picture; encrypted-media; clipboard-write';
|
|
92
|
-
const ID = /^[\w-]{1,128}$/;
|
|
93
|
-
const HASH = /^[\da-f]{1,32}$/i;
|
|
94
|
-
/** `url`'s path segments (empty ones dropped) when its host is one of `hosts`, else none. */
|
|
95
|
-
const on = (url, ...hosts) => hosts.includes(url.hostname.toLowerCase()) ? url.pathname.split('/').filter(Boolean) : [];
|
|
96
|
-
/** A YouTube start time (`t=42`, `t=42s`, `start=42`) in seconds. */
|
|
97
|
-
const youtubeStart = (url) => {
|
|
98
|
-
const t = url.searchParams.get('t') ?? url.searchParams.get('start');
|
|
99
|
-
return t && /^\d{1,6}s?$/.test(t) ? `?start=${parseInt(t, 10)}` : '';
|
|
100
|
-
};
|
|
101
|
-
const youtube = {
|
|
102
|
-
name: 'YouTube',
|
|
103
|
-
embed: (url) => {
|
|
104
|
-
const [first, second] = on(url, 'youtu.be', 'youtube.com', 'www.youtube.com', 'm.youtube.com', 'music.youtube.com', 'www.youtube-nocookie.com');
|
|
105
|
-
const id = url.hostname.toLowerCase() === 'youtu.be'
|
|
106
|
-
? first
|
|
107
|
-
: first === 'watch'
|
|
108
|
-
? url.searchParams.get('v')
|
|
109
|
-
: first && ['shorts', 'embed', 'live'].includes(first)
|
|
110
|
-
? second
|
|
111
|
-
: undefined;
|
|
112
|
-
return id && /^[\w-]{11}$/.test(id)
|
|
113
|
-
? `https://www.youtube-nocookie.com/embed/${id}${youtubeStart(url)}`
|
|
114
|
-
: null;
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
const vimeo = {
|
|
118
|
-
name: 'Vimeo',
|
|
119
|
-
embed: (url) => {
|
|
120
|
-
const [player, ...video] = on(url, 'player.vimeo.com');
|
|
121
|
-
const [id, hash] = player === 'video' ? video : on(url, 'vimeo.com', 'www.vimeo.com');
|
|
122
|
-
if (!id || !/^\d{1,12}$/.test(id))
|
|
123
|
-
return null;
|
|
124
|
-
const h = hash && HASH.test(hash) ? hash : url.searchParams.get('h');
|
|
125
|
-
return `https://player.vimeo.com/video/${id}${h && HASH.test(h) ? `?h=${h}` : ''}`;
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
const loom = {
|
|
129
|
-
name: 'Loom',
|
|
130
|
-
embed: (url) => {
|
|
131
|
-
const [kind, id] = on(url, 'loom.com', 'www.loom.com');
|
|
132
|
-
return (kind === 'share' || kind === 'embed') && id && /^[\da-f]{32}$/i.test(id)
|
|
133
|
-
? `https://www.loom.com/embed/${id}`
|
|
134
|
-
: null;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
const figma = {
|
|
138
|
-
name: 'Figma',
|
|
139
|
-
embed: (url) => {
|
|
140
|
-
const [kind, key] = on(url, 'figma.com', 'www.figma.com');
|
|
141
|
-
return key &&
|
|
142
|
-
ID.test(key) &&
|
|
143
|
-
['file', 'design', 'proto', 'board', 'slides', 'deck'].includes(kind)
|
|
144
|
-
? `https://www.figma.com/embed?embed_host=edytor&url=${encodeURIComponent(url.href)}`
|
|
145
|
-
: null;
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
const codepen = {
|
|
149
|
-
name: 'CodePen',
|
|
150
|
-
embed: (url) => {
|
|
151
|
-
const [user, kind, id] = on(url, 'codepen.io');
|
|
152
|
-
return user && ID.test(user) && (kind === 'pen' || kind === 'embed') && id && ID.test(id)
|
|
153
|
-
? `https://codepen.io/${user}/embed/${id}?default-tab=result`
|
|
154
|
-
: null;
|
|
155
|
-
},
|
|
156
|
-
height: 400
|
|
157
|
-
};
|
|
158
|
-
const spotify = {
|
|
159
|
-
name: 'Spotify',
|
|
160
|
-
embed: (url) => {
|
|
161
|
-
let parts = on(url, 'open.spotify.com');
|
|
162
|
-
if (parts[0] === 'embed' || /^intl-[a-z-]+$/i.test(parts[0] ?? ''))
|
|
163
|
-
parts = parts.slice(1);
|
|
164
|
-
const [kind, id] = parts;
|
|
165
|
-
return ['track', 'album', 'playlist', 'episode', 'show', 'artist'].includes(kind) &&
|
|
166
|
-
id &&
|
|
167
|
-
/^[A-Za-z0-9]{1,64}$/.test(id)
|
|
168
|
-
? `https://open.spotify.com/embed/${kind}/${id}`
|
|
169
|
-
: null;
|
|
170
|
-
},
|
|
171
|
-
height: 352
|
|
172
|
-
};
|
|
173
|
-
/**
|
|
174
|
-
* The default allowlist: YouTube (through `youtube-nocookie.com`), Vimeo,
|
|
175
|
-
* Loom, Figma, CodePen and Spotify. A link no provider plays renders no
|
|
176
|
-
* frame. Extend it with `createEmbedPlugin({ providers: [...EMBED_PROVIDERS, mine] })`.
|
|
177
|
-
*/
|
|
178
|
-
export const EMBED_PROVIDERS = Object.freeze([
|
|
179
|
-
youtube,
|
|
180
|
-
vimeo,
|
|
181
|
-
loom,
|
|
182
|
-
figma,
|
|
183
|
-
codepen,
|
|
184
|
-
spotify
|
|
185
|
-
]);
|
|
186
|
-
/**
|
|
187
|
-
* `url` is on the page's own origin (in a browser): a frame there with
|
|
188
|
-
* `allow-scripts` and `allow-same-origin` could remove its own sandbox.
|
|
189
|
-
*/
|
|
190
|
-
const sameOrigin = (url) => {
|
|
191
|
-
const origin = globalThis.location?.origin;
|
|
192
|
-
return typeof origin === 'string' && url.origin === origin;
|
|
193
|
-
};
|
|
194
|
-
/**
|
|
195
|
-
* The player for `value` (a stored or pasted link): the first provider of
|
|
196
|
-
* `providers` that plays it, and its `https:` player URL. `null` when the
|
|
197
|
-
* link is not a {@link safeWebUrl}, no provider plays it, or the player URL
|
|
198
|
-
* a provider answers is not `https:` or is on the page's own origin. The frame's `src` is always this
|
|
199
|
-
* answer, never the stored value.
|
|
200
|
-
*/
|
|
201
|
-
export const embedSourceOf = (value, providers = EMBED_PROVIDERS) => {
|
|
202
|
-
const href = safeWebUrl(value);
|
|
203
|
-
if (!href)
|
|
204
|
-
return null;
|
|
205
|
-
const url = new URL(href);
|
|
206
|
-
for (const provider of providers) {
|
|
207
|
-
let src;
|
|
208
|
-
try {
|
|
209
|
-
src = provider.embed(new URL(url.href));
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
if (src === null || src === undefined)
|
|
215
|
-
continue;
|
|
216
|
-
try {
|
|
217
|
-
const player = new URL(src);
|
|
218
|
-
if (player.protocol === 'https:' && safeWebUrl(src) === src && !sameOrigin(player))
|
|
219
|
-
return { src, provider };
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
// An unparsable player URL is refused like any other non-https one.
|
|
223
|
-
}
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
return null;
|
|
227
|
-
};
|
|
228
|
-
/** A file's display name from its URL's last path segment, decoded. */
|
|
229
|
-
export const fileNameOf = (src) => {
|
|
230
|
-
try {
|
|
231
|
-
const last = new URL(src).pathname.split('/').filter(Boolean).at(-1);
|
|
232
|
-
return last ? decodeURIComponent(last) : src;
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
return src;
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
/**
|
|
239
|
-
* The element a media kind's HTML import reads: `element` itself when it
|
|
240
|
-
* is a `tag` (not inside a `figure`, which claims it), or a `figure`'s own
|
|
241
|
-
* `tag` child.
|
|
242
|
-
*/
|
|
243
|
-
export const claimed = (element, tag) => {
|
|
244
|
-
if (element.localName === 'figure')
|
|
245
|
-
return [...element.children].find((child) => child.localName === tag);
|
|
246
|
-
return element.localName === tag && element.parentElement?.localName !== 'figure'
|
|
247
|
-
? element
|
|
248
|
-
: undefined;
|
|
249
|
-
};
|
|
250
|
-
/** An imported `video`'s or `audio`'s source: its `src`, else its first `source`'s. */
|
|
251
|
-
export const mediaSourceOf = (element) => safeMediaSrc(element?.getAttribute('src') ?? element?.querySelector('source[src]')?.getAttribute('src'));
|
|
58
|
+
export * from './sources.js';
|
|
252
59
|
export { escapeHtml };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The media kinds' pure source rules: which URL a media block may store or
|
|
3
|
+
* render, the embed providers and their players, a file's name, the element
|
|
4
|
+
* an import reads. No DOM state, no Svelte: the export entry reads them too.
|
|
5
|
+
*/
|
|
6
|
+
import { safeMediaSrc, safeWebUrl } from '../../utils/sanitize.js';
|
|
7
|
+
export { safeMediaSrc, safeWebUrl };
|
|
8
|
+
/** One allowlisted embed provider: which links it plays, and its player. */
|
|
9
|
+
export type EmbedProvider = {
|
|
10
|
+
/** The provider's name: the frame's `title`. */
|
|
11
|
+
name: string;
|
|
12
|
+
/**
|
|
13
|
+
* The player's URL for a link of this provider, else `null`. It must be
|
|
14
|
+
* `https:` on another origin than the page's: any other answer is
|
|
15
|
+
* refused (the frame has `allow-scripts` and `allow-same-origin`, so a
|
|
16
|
+
* player on the page's origin, or on one that can script it, could lift
|
|
17
|
+
* its own sandbox). Match the host exactly (never a
|
|
18
|
+
* suffix or a substring) and check every id you copy into the player URL.
|
|
19
|
+
*/
|
|
20
|
+
embed: (url: URL) => string | null;
|
|
21
|
+
/** The frame's CSS `aspect-ratio` (default `16 / 9`); ignored with `height`. */
|
|
22
|
+
aspectRatio?: string;
|
|
23
|
+
/** A fixed frame height in pixels (a music player). */
|
|
24
|
+
height?: number;
|
|
25
|
+
};
|
|
26
|
+
/** The frame's `sandbox`: scripts and the provider's own origin, popups that leave it; no top navigation, modals, downloads or forms. */
|
|
27
|
+
export declare const EMBED_SANDBOX = "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation";
|
|
28
|
+
/** The frame's permissions (`allow`): what a player needs, nothing that reads the viewer. */
|
|
29
|
+
export declare const EMBED_ALLOW = "fullscreen; picture-in-picture; encrypted-media; clipboard-write";
|
|
30
|
+
/**
|
|
31
|
+
* The default allowlist: YouTube (through `youtube-nocookie.com`), Vimeo,
|
|
32
|
+
* Loom, Figma, CodePen and Spotify. A link no provider plays renders no
|
|
33
|
+
* frame. Extend it with `createEmbedPlugin({ providers: [...EMBED_PROVIDERS, mine] })`.
|
|
34
|
+
*/
|
|
35
|
+
export declare const EMBED_PROVIDERS: readonly EmbedProvider[];
|
|
36
|
+
/**
|
|
37
|
+
* The player for `value` (a stored or pasted link): the first provider of
|
|
38
|
+
* `providers` that plays it, and its `https:` player URL. `null` when the
|
|
39
|
+
* link is not a {@link safeWebUrl}, no provider plays it, or the player URL
|
|
40
|
+
* a provider answers is not `https:` or is on the page's own origin. The frame's `src` is always this
|
|
41
|
+
* answer, never the stored value.
|
|
42
|
+
*/
|
|
43
|
+
export declare const embedSourceOf: (value: unknown, providers?: readonly EmbedProvider[]) => {
|
|
44
|
+
src: string;
|
|
45
|
+
provider: EmbedProvider;
|
|
46
|
+
} | null;
|
|
47
|
+
/** A file's display name from its URL's last path segment, decoded. */
|
|
48
|
+
export declare const fileNameOf: (src: string) => string;
|
|
49
|
+
/** A file block's name: its stored `name`, else its source's last segment (`''` without one). */
|
|
50
|
+
export declare const fileNameFor: (data: {
|
|
51
|
+
name?: unknown;
|
|
52
|
+
} | undefined, src: string | null) => string;
|
|
53
|
+
/**
|
|
54
|
+
* The element a media kind's HTML import reads: `element` itself when it
|
|
55
|
+
* is a `tag` (not inside a `figure`, which claims it), or a `figure`'s own
|
|
56
|
+
* `tag` child.
|
|
57
|
+
*/
|
|
58
|
+
export declare const claimed: (element: HTMLElement, tag: string) => HTMLElement | undefined;
|
|
59
|
+
/** An imported `video`'s or `audio`'s source: its `src`, else its first `source`'s. */
|
|
60
|
+
export declare const mediaSourceOf: (element: HTMLElement | undefined) => string | null;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The media kinds' pure source rules: which URL a media block may store or
|
|
3
|
+
* render, the embed providers and their players, a file's name, the element
|
|
4
|
+
* an import reads. No DOM state, no Svelte: the export entry reads them too.
|
|
5
|
+
*/
|
|
6
|
+
import { safeMediaSrc, safeWebUrl } from '../../utils/sanitize.js';
|
|
7
|
+
export { safeMediaSrc, safeWebUrl };
|
|
8
|
+
/** The frame's `sandbox`: scripts and the provider's own origin, popups that leave it; no top navigation, modals, downloads or forms. */
|
|
9
|
+
export const EMBED_SANDBOX = 'allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation';
|
|
10
|
+
/** The frame's permissions (`allow`): what a player needs, nothing that reads the viewer. */
|
|
11
|
+
export const EMBED_ALLOW = 'fullscreen; picture-in-picture; encrypted-media; clipboard-write';
|
|
12
|
+
const ID = /^[\w-]{1,128}$/;
|
|
13
|
+
const HASH = /^[\da-f]{1,32}$/i;
|
|
14
|
+
/** `url`'s path segments (empty ones dropped) when its host is exactly one `hosts` matches, else none. */
|
|
15
|
+
const on = (url, hosts) => hosts.test(url.hostname) ? url.pathname.split('/').filter(Boolean) : [];
|
|
16
|
+
const youtube = {
|
|
17
|
+
name: 'YouTube',
|
|
18
|
+
embed: (url) => {
|
|
19
|
+
const [first = '', second] = on(url, /^((www\.|m\.|music\.)?youtube\.com|youtu\.be|www\.youtube-nocookie\.com)$/);
|
|
20
|
+
const id = url.hostname === 'youtu.be'
|
|
21
|
+
? first
|
|
22
|
+
: first === 'watch'
|
|
23
|
+
? url.searchParams.get('v')
|
|
24
|
+
: /^(shorts|embed|live)$/.test(first) && second;
|
|
25
|
+
// A start time (`t=42`, `t=42s`, `start=42`) in seconds.
|
|
26
|
+
const t = url.searchParams.get('t') ?? url.searchParams.get('start');
|
|
27
|
+
return id && /^[\w-]{11}$/.test(id)
|
|
28
|
+
? `https://www.youtube-nocookie.com/embed/${id}${t && /^\d{1,6}s?$/.test(t) ? `?start=${parseInt(t, 10)}` : ''}`
|
|
29
|
+
: null;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const vimeo = {
|
|
33
|
+
name: 'Vimeo',
|
|
34
|
+
embed: (url) => {
|
|
35
|
+
const [player, ...video] = on(url, /^player\.vimeo\.com$/);
|
|
36
|
+
const [id, hash] = player === 'video' ? video : on(url, /^(www\.)?vimeo\.com$/);
|
|
37
|
+
const h = hash && HASH.test(hash) ? hash : url.searchParams.get('h');
|
|
38
|
+
return id && /^\d{1,12}$/.test(id)
|
|
39
|
+
? `https://player.vimeo.com/video/${id}${h && HASH.test(h) ? `?h=${h}` : ''}`
|
|
40
|
+
: null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const loom = {
|
|
44
|
+
name: 'Loom',
|
|
45
|
+
embed: (url) => {
|
|
46
|
+
const [kind = '', id = ''] = on(url, /^(www\.)?loom\.com$/);
|
|
47
|
+
return /^(share|embed)$/.test(kind) && /^[\da-f]{32}$/i.test(id)
|
|
48
|
+
? `https://www.loom.com/embed/${id}`
|
|
49
|
+
: null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const figma = {
|
|
53
|
+
name: 'Figma',
|
|
54
|
+
embed: (url) => {
|
|
55
|
+
const [kind = '', key = ''] = on(url, /^(www\.)?figma\.com$/);
|
|
56
|
+
return ID.test(key) && /^(file|design|proto|board|slides|deck)$/.test(kind)
|
|
57
|
+
? `https://www.figma.com/embed?embed_host=edytor&url=${encodeURIComponent(url.href)}`
|
|
58
|
+
: null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const codepen = {
|
|
62
|
+
name: 'CodePen',
|
|
63
|
+
embed: (url) => {
|
|
64
|
+
const [user = '', kind = '', id = ''] = on(url, /^codepen\.io$/);
|
|
65
|
+
return ID.test(user) && /^(pen|embed)$/.test(kind) && ID.test(id)
|
|
66
|
+
? `https://codepen.io/${user}/embed/${id}?default-tab=result`
|
|
67
|
+
: null;
|
|
68
|
+
},
|
|
69
|
+
height: 400
|
|
70
|
+
};
|
|
71
|
+
const spotify = {
|
|
72
|
+
name: 'Spotify',
|
|
73
|
+
embed: (url) => {
|
|
74
|
+
let parts = on(url, /^open\.spotify\.com$/);
|
|
75
|
+
if (/^(embed|intl-[a-z-]+)$/i.test(parts[0] ?? ''))
|
|
76
|
+
parts = parts.slice(1);
|
|
77
|
+
const [kind = '', id = ''] = parts;
|
|
78
|
+
return /^(track|album|playlist|episode|show|artist)$/.test(kind) &&
|
|
79
|
+
/^[A-Za-z0-9]{1,64}$/.test(id)
|
|
80
|
+
? `https://open.spotify.com/embed/${kind}/${id}`
|
|
81
|
+
: null;
|
|
82
|
+
},
|
|
83
|
+
height: 352
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* The default allowlist: YouTube (through `youtube-nocookie.com`), Vimeo,
|
|
87
|
+
* Loom, Figma, CodePen and Spotify. A link no provider plays renders no
|
|
88
|
+
* frame. Extend it with `createEmbedPlugin({ providers: [...EMBED_PROVIDERS, mine] })`.
|
|
89
|
+
*/
|
|
90
|
+
export const EMBED_PROVIDERS = Object.freeze([
|
|
91
|
+
youtube,
|
|
92
|
+
vimeo,
|
|
93
|
+
loom,
|
|
94
|
+
figma,
|
|
95
|
+
codepen,
|
|
96
|
+
spotify
|
|
97
|
+
]);
|
|
98
|
+
/**
|
|
99
|
+
* `url` is on the page's own origin (in a browser): a frame there with
|
|
100
|
+
* `allow-scripts` and `allow-same-origin` could remove its own sandbox.
|
|
101
|
+
*/
|
|
102
|
+
const sameOrigin = (url) => {
|
|
103
|
+
const origin = globalThis.location?.origin;
|
|
104
|
+
return typeof origin === 'string' && url.origin === origin;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* The player for `value` (a stored or pasted link): the first provider of
|
|
108
|
+
* `providers` that plays it, and its `https:` player URL. `null` when the
|
|
109
|
+
* link is not a {@link safeWebUrl}, no provider plays it, or the player URL
|
|
110
|
+
* a provider answers is not `https:` or is on the page's own origin. The frame's `src` is always this
|
|
111
|
+
* answer, never the stored value.
|
|
112
|
+
*/
|
|
113
|
+
export const embedSourceOf = (value, providers = EMBED_PROVIDERS) => {
|
|
114
|
+
const href = safeWebUrl(value);
|
|
115
|
+
if (!href)
|
|
116
|
+
return null;
|
|
117
|
+
const url = new URL(href);
|
|
118
|
+
for (const provider of providers) {
|
|
119
|
+
let src;
|
|
120
|
+
try {
|
|
121
|
+
src = provider.embed(new URL(url.href));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (src === null || src === undefined)
|
|
127
|
+
continue;
|
|
128
|
+
try {
|
|
129
|
+
const player = new URL(src);
|
|
130
|
+
if (player.protocol === 'https:' && safeWebUrl(src) === src && !sameOrigin(player))
|
|
131
|
+
return { src, provider };
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// An unparsable player URL is refused like any other non-https one.
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
};
|
|
140
|
+
/** A file's display name from its URL's last path segment, decoded. */
|
|
141
|
+
export const fileNameOf = (src) => {
|
|
142
|
+
try {
|
|
143
|
+
const last = new URL(src).pathname.split('/').filter(Boolean).at(-1);
|
|
144
|
+
return last ? decodeURIComponent(last) : src;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return src;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
/** A file block's name: its stored `name`, else its source's last segment (`''` without one). */
|
|
151
|
+
export const fileNameFor = (data, src) => String(data?.name || (src && fileNameOf(src)) || '');
|
|
152
|
+
/**
|
|
153
|
+
* The element a media kind's HTML import reads: `element` itself when it
|
|
154
|
+
* is a `tag` (not inside a `figure`, which claims it), or a `figure`'s own
|
|
155
|
+
* `tag` child.
|
|
156
|
+
*/
|
|
157
|
+
export const claimed = (element, tag) => {
|
|
158
|
+
if (element.localName === 'figure')
|
|
159
|
+
return [...element.children].find((child) => child.localName === tag);
|
|
160
|
+
return element.localName === tag && element.parentElement?.localName !== 'figure'
|
|
161
|
+
? element
|
|
162
|
+
: undefined;
|
|
163
|
+
};
|
|
164
|
+
/** An imported `video`'s or `audio`'s source: its `src`, else its first `source`'s. */
|
|
165
|
+
export const mediaSourceOf = (element) => safeMediaSrc(element?.getAttribute('src') ?? element?.querySelector('source[src]')?.getAttribute('src'));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* Loaded by the preview's module: a file uploading, drawn by the plugin when
|
|
2
|
+
the view has no empty markup (`createMediaEmptyPlugin`). */
|
|
3
|
+
[data-edytor-upload] {
|
|
4
|
+
display: flex;
|
|
5
|
+
position: relative;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: 6px;
|
|
8
|
+
}
|
|
9
|
+
[data-edytor-upload] img {
|
|
10
|
+
display: block;
|
|
11
|
+
max-width: 100%;
|
|
12
|
+
}
|
|
13
|
+
[data-edytor-upload] progress {
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 4px;
|
|
16
|
+
}
|
|
17
|
+
[data-edytor-upload-image] progress {
|
|
18
|
+
position: absolute;
|
|
19
|
+
right: 8px;
|
|
20
|
+
bottom: 8px;
|
|
21
|
+
left: 8px;
|
|
22
|
+
width: auto;
|
|
23
|
+
}
|
|
@@ -43,10 +43,11 @@ export declare class UrlPasteController extends ChromeList {
|
|
|
43
43
|
} | null;
|
|
44
44
|
constructor(edytor: Edytor);
|
|
45
45
|
protected get count(): number;
|
|
46
|
-
get readonly(): boolean;
|
|
47
46
|
close: () => void;
|
|
48
47
|
/** Pick a row: keep the link, or turn the line into the offer's kind. */
|
|
49
48
|
pick: (option?: UrlPasteOption | undefined) => void;
|
|
49
|
+
/** Where the menu stands (`ChromeAnchor`): the pasted line, which your `menu` places itself under. */
|
|
50
|
+
readonly anchor: import("../chrome.js").ChromeAnchor;
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
52
53
|
* Register `offer` on `edytor`'s URL paste menu. The first plugin to offer
|