@dou-ui/editor 0.0.0-stage → 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +10 -2
- package/dist/catalog.js +18 -0
- package/dist/catalog.js.map +1 -0
- package/dist/code/code-editor-block.js +6 -0
- package/dist/code/code-editor-block.js.map +1 -0
- package/dist/code/code-editor-block.vue_vue_type_script_setup_true_lang.js +116 -0
- package/dist/code/code-editor-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/code/index.js +69 -0
- package/dist/code/index.js.map +1 -0
- package/dist/code/languages.js +51 -0
- package/dist/code/languages.js.map +1 -0
- package/dist/code/view.js +255 -0
- package/dist/code/view.js.map +1 -0
- package/dist/collaboration/binding.js +381 -0
- package/dist/collaboration/binding.js.map +1 -0
- package/dist/collaboration/cursors.js +150 -0
- package/dist/collaboration/cursors.js.map +1 -0
- package/dist/collaboration/index.js +108 -0
- package/dist/collaboration/index.js.map +1 -0
- package/dist/collaboration/order.js +88 -0
- package/dist/collaboration/order.js.map +1 -0
- package/dist/collaboration/table.js +234 -0
- package/dist/collaboration/table.js.map +1 -0
- package/dist/collaboration/text.js +91 -0
- package/dist/collaboration/text.js.map +1 -0
- package/dist/components/block-handle.js +6 -0
- package/dist/components/block-handle.js.map +1 -0
- package/dist/components/block-handle.vue_vue_type_script_setup_true_lang.js +474 -0
- package/dist/components/block-handle.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/bubble-toolbar.js +6 -0
- package/dist/components/bubble-toolbar.js.map +1 -0
- package/dist/components/bubble-toolbar.vue_vue_type_script_setup_true_lang.js +381 -0
- package/dist/components/bubble-toolbar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/comment-button.js +6 -0
- package/dist/components/comment-button.js.map +1 -0
- package/dist/components/comment-button.vue_vue_type_script_setup_true_lang.js +83 -0
- package/dist/components/comment-button.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/comment-card.js +6 -0
- package/dist/components/comment-card.js.map +1 -0
- package/dist/components/comment-card.vue_vue_type_script_setup_true_lang.js +104 -0
- package/dist/components/comment-card.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/menu-escape.js +25 -0
- package/dist/components/menu-escape.js.map +1 -0
- package/dist/components/search-bar.js +6 -0
- package/dist/components/search-bar.js.map +1 -0
- package/dist/components/search-bar.vue_vue_type_script_setup_true_lang.js +192 -0
- package/dist/components/search-bar.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/slash-menu.js +6 -0
- package/dist/components/slash-menu.js.map +1 -0
- package/dist/components/slash-menu.vue_vue_type_script_setup_true_lang.js +164 -0
- package/dist/components/slash-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/core/align.js +94 -0
- package/dist/core/align.js.map +1 -0
- package/dist/core/block-selection.js +155 -0
- package/dist/core/block-selection.js.map +1 -0
- package/dist/core/blocks.js +132 -0
- package/dist/core/blocks.js.map +1 -0
- package/dist/core/changes.js +85 -0
- package/dist/core/changes.js.map +1 -0
- package/dist/core/colors.js +16 -0
- package/dist/core/colors.js.map +1 -0
- package/dist/core/commands.js +379 -0
- package/dist/core/commands.js.map +1 -0
- package/dist/core/comments.js +186 -0
- package/dist/core/comments.js.map +1 -0
- package/dist/core/compare.js +106 -0
- package/dist/core/compare.js.map +1 -0
- package/dist/core/convert.js +28 -0
- package/dist/core/convert.js.map +1 -0
- package/dist/core/diff.js +141 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/editor.js +138 -0
- package/dist/core/editor.js.map +1 -0
- package/dist/core/extension.js +99 -0
- package/dist/core/extension.js.map +1 -0
- package/dist/core/file-drop.js +56 -0
- package/dist/core/file-drop.js.map +1 -0
- package/dist/core/highlight.js +120 -0
- package/dist/core/highlight.js.map +1 -0
- package/dist/core/input-rules.js +61 -0
- package/dist/core/input-rules.js.map +1 -0
- package/dist/core/keymap.js +38 -0
- package/dist/core/keymap.js.map +1 -0
- package/dist/core/model.js +196 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/output.js +263 -0
- package/dist/core/output.js.map +1 -0
- package/dist/core/plugins.js +102 -0
- package/dist/core/plugins.js.map +1 -0
- package/dist/core/prepare.js +102 -0
- package/dist/core/prepare.js.map +1 -0
- package/dist/core/schema.js +158 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/search.js +197 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/selection.js +16 -0
- package/dist/core/selection.js.map +1 -0
- package/dist/core/slash.js +83 -0
- package/dist/core/slash.js.map +1 -0
- package/dist/core/transaction.js +311 -0
- package/dist/core/transaction.js.map +1 -0
- package/dist/define-block.js +177 -0
- package/dist/define-block.js.map +1 -0
- package/dist/document.js +157 -0
- package/dist/document.js.map +1 -0
- package/dist/dom.js +48 -0
- package/dist/dom.js.map +1 -0
- package/dist/editor.js +33 -0
- package/dist/editor.js.map +1 -0
- package/dist/editor.vue_vue_type_script_setup_true_lang.js +564 -0
- package/dist/editor.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/editor2.js +6 -0
- package/dist/editor2.js.map +1 -0
- package/dist/extensions/align/align-menu.js +6 -0
- package/dist/extensions/align/align-menu.js.map +1 -0
- package/dist/extensions/align/align-menu.vue_vue_type_script_setup_true_lang.js +82 -0
- package/dist/extensions/align/align-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/align/align-sub.js +6 -0
- package/dist/extensions/align/align-sub.js.map +1 -0
- package/dist/extensions/align/align-sub.vue_vue_type_script_setup_true_lang.js +57 -0
- package/dist/extensions/align/align-sub.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/align/commands.js +40 -0
- package/dist/extensions/align/commands.js.map +1 -0
- package/dist/extensions/align/index.js +27 -0
- package/dist/extensions/align/index.js.map +1 -0
- package/dist/extensions/attachment/attachment-block.js +6 -0
- package/dist/extensions/attachment/attachment-block.js.map +1 -0
- package/dist/extensions/attachment/attachment-block.vue_vue_type_script_setup_true_lang.js +119 -0
- package/dist/extensions/attachment/attachment-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/attachment/files.js +66 -0
- package/dist/extensions/attachment/files.js.map +1 -0
- package/dist/extensions/attachment/index.js +132 -0
- package/dist/extensions/attachment/index.js.map +1 -0
- package/dist/extensions/blocks.js +170 -0
- package/dist/extensions/blocks.js.map +1 -0
- package/dist/extensions/callout/callout-block.js +6 -0
- package/dist/extensions/callout/callout-block.js.map +1 -0
- package/dist/extensions/callout/callout-block.vue_vue_type_script_setup_true_lang.js +71 -0
- package/dist/extensions/callout/callout-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/callout/html.js +61 -0
- package/dist/extensions/callout/html.js.map +1 -0
- package/dist/extensions/callout/index.js +52 -0
- package/dist/extensions/callout/index.js.map +1 -0
- package/dist/extensions/callout/tones.js +12 -0
- package/dist/extensions/callout/tones.js.map +1 -0
- package/dist/extensions/code-block/code-block-view.js +6 -0
- package/dist/extensions/code-block/code-block-view.js.map +1 -0
- package/dist/extensions/code-block/code-block-view.vue_vue_type_script_setup_true_lang.js +87 -0
- package/dist/extensions/code-block/code-block-view.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/code-block/index.js +98 -0
- package/dist/extensions/code-block/index.js.map +1 -0
- package/dist/extensions/code-block/languages.js +116 -0
- package/dist/extensions/code-block/languages.js.map +1 -0
- package/dist/extensions/colors/color-menu.js +6 -0
- package/dist/extensions/colors/color-menu.js.map +1 -0
- package/dist/extensions/colors/color-menu.vue_vue_type_script_setup_true_lang.js +113 -0
- package/dist/extensions/colors/color-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/colors/index.js +49 -0
- package/dist/extensions/colors/index.js.map +1 -0
- package/dist/extensions/colors/palette.js +26 -0
- package/dist/extensions/colors/palette.js.map +1 -0
- package/dist/extensions/editor-kit.js +35 -0
- package/dist/extensions/editor-kit.js.map +1 -0
- package/dist/extensions/embed/embed-block.js +6 -0
- package/dist/extensions/embed/embed-block.js.map +1 -0
- package/dist/extensions/embed/embed-block.vue_vue_type_script_setup_true_lang.js +210 -0
- package/dist/extensions/embed/embed-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/embed/index.js +118 -0
- package/dist/extensions/embed/index.js.map +1 -0
- package/dist/extensions/embed/targets.js +79 -0
- package/dist/extensions/embed/targets.js.map +1 -0
- package/dist/extensions/formatting.js +153 -0
- package/dist/extensions/formatting.js.map +1 -0
- package/dist/extensions/image/image-block.js +6 -0
- package/dist/extensions/image/image-block.js.map +1 -0
- package/dist/extensions/image/image-block.vue_vue_type_script_setup_true_lang.js +206 -0
- package/dist/extensions/image/image-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/image/index.js +125 -0
- package/dist/extensions/image/index.js.map +1 -0
- package/dist/extensions/image/uploads.js +96 -0
- package/dist/extensions/image/uploads.js.map +1 -0
- package/dist/extensions/list-html.js +58 -0
- package/dist/extensions/list-html.js.map +1 -0
- package/dist/extensions/lists.js +182 -0
- package/dist/extensions/lists.js.map +1 -0
- package/dist/extensions/mention/index.js +56 -0
- package/dist/extensions/mention/index.js.map +1 -0
- package/dist/extensions/mention/mention-menu.js +6 -0
- package/dist/extensions/mention/mention-menu.js.map +1 -0
- package/dist/extensions/mention/mention-menu.vue_vue_type_script_setup_true_lang.js +132 -0
- package/dist/extensions/mention/mention-menu.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/mention/state.js +113 -0
- package/dist/extensions/mention/state.js.map +1 -0
- package/dist/extensions/table/commands.js +356 -0
- package/dist/extensions/table/commands.js.map +1 -0
- package/dist/extensions/table/html.js +134 -0
- package/dist/extensions/table/html.js.map +1 -0
- package/dist/extensions/table/index.js +56 -0
- package/dist/extensions/table/index.js.map +1 -0
- package/dist/extensions/table/plugin.js +81 -0
- package/dist/extensions/table/plugin.js.map +1 -0
- package/dist/extensions/table/schema.js +258 -0
- package/dist/extensions/table/schema.js.map +1 -0
- package/dist/extensions/table/table-controls.js +6 -0
- package/dist/extensions/table/table-controls.js.map +1 -0
- package/dist/extensions/table/table-controls.vue_vue_type_script_setup_true_lang.js +698 -0
- package/dist/extensions/table/table-controls.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/extensions/table/view.js +19 -0
- package/dist/extensions/table/view.js.map +1 -0
- package/dist/extensions/toggle/index.js +87 -0
- package/dist/extensions/toggle/index.js.map +1 -0
- package/dist/extensions/toggle/state.js +120 -0
- package/dist/extensions/toggle/state.js.map +1 -0
- package/dist/extensions/toggle/toggle-block.js +6 -0
- package/dist/extensions/toggle/toggle-block.js.map +1 -0
- package/dist/extensions/toggle/toggle-block.vue_vue_type_script_setup_true_lang.js +42 -0
- package/dist/extensions/toggle/toggle-block.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/locale.js +522 -0
- package/dist/locale.js.map +1 -0
- package/dist/markdown/index.js +24 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +8 -0
- package/dist/types/catalog.d.ts +8 -0
- package/dist/types/code/code-editor-block.vue.d.ts +17 -0
- package/dist/types/code/index.d.ts +18 -0
- package/dist/types/code/languages.d.ts +7 -0
- package/dist/types/code/types.d.ts +37 -0
- package/dist/types/code/view.d.ts +12 -0
- package/dist/types/collaboration/binding.d.ts +32 -0
- package/dist/types/collaboration/cursors.d.ts +26 -0
- package/dist/types/collaboration/index.d.ts +47 -0
- package/dist/types/collaboration/order.d.ts +20 -0
- package/dist/types/collaboration/table.d.ts +25 -0
- package/dist/types/collaboration/text.d.ts +9 -0
- package/dist/types/components/block-handle.vue.d.ts +3 -0
- package/dist/types/components/bubble-toolbar.vue.d.ts +7 -0
- package/dist/types/components/comment-button.vue.d.ts +3 -0
- package/dist/types/components/comment-card.vue.d.ts +3 -0
- package/dist/types/components/menu-escape.d.ts +7 -0
- package/dist/types/components/search-bar.vue.d.ts +22 -0
- package/dist/types/components/slash-menu.vue.d.ts +11 -0
- package/dist/types/core/align.d.ts +16 -0
- package/dist/types/core/block-selection.d.ts +22 -0
- package/dist/types/core/blocks.d.ts +52 -0
- package/dist/types/core/changes.d.ts +14 -0
- package/dist/types/core/colors.d.ts +4 -0
- package/dist/types/core/commands.d.ts +61 -0
- package/dist/types/core/comments.d.ts +44 -0
- package/dist/types/core/compare.d.ts +10 -0
- package/dist/types/core/convert.d.ts +12 -0
- package/dist/types/core/diff.d.ts +49 -0
- package/dist/types/core/editor.d.ts +64 -0
- package/dist/types/core/extension.d.ts +167 -0
- package/dist/types/core/file-drop.d.ts +19 -0
- package/dist/types/core/highlight.d.ts +32 -0
- package/dist/types/core/input-rules.d.ts +21 -0
- package/dist/types/core/keymap.d.ts +8 -0
- package/dist/types/core/model.d.ts +158 -0
- package/dist/types/core/output.d.ts +64 -0
- package/dist/types/core/plugins.d.ts +24 -0
- package/dist/types/core/prepare.d.ts +23 -0
- package/dist/types/core/schema.d.ts +38 -0
- package/dist/types/core/search.d.ts +83 -0
- package/dist/types/core/selection.d.ts +8 -0
- package/dist/types/core/slash.d.ts +21 -0
- package/dist/types/core/transaction.d.ts +97 -0
- package/dist/types/define-block.d.ts +66 -0
- package/dist/types/document.d.ts +46 -0
- package/dist/types/dom.d.ts +17 -0
- package/dist/types/editor.d.ts +252 -0
- package/dist/types/editor.vue.d.ts +210 -0
- package/dist/types/extensions/align/align-menu.vue.d.ts +3 -0
- package/dist/types/extensions/align/align-sub.vue.d.ts +7 -0
- package/dist/types/extensions/align/commands.d.ts +13 -0
- package/dist/types/extensions/align/index.d.ts +7 -0
- package/dist/types/extensions/attachment/attachment-block.vue.d.ts +17 -0
- package/dist/types/extensions/attachment/files.d.ts +6 -0
- package/dist/types/extensions/attachment/index.d.ts +20 -0
- package/dist/types/extensions/blocks.d.ts +8 -0
- package/dist/types/extensions/callout/callout-block.vue.d.ts +14 -0
- package/dist/types/extensions/callout/html.d.ts +4 -0
- package/dist/types/extensions/callout/index.d.ts +3 -0
- package/dist/types/extensions/callout/tones.d.ts +8 -0
- package/dist/types/extensions/code-block/code-block-view.vue.d.ts +16 -0
- package/dist/types/extensions/code-block/index.d.ts +19 -0
- package/dist/types/extensions/code-block/languages.d.ts +9 -0
- package/dist/types/extensions/colors/color-menu.vue.d.ts +3 -0
- package/dist/types/extensions/colors/index.d.ts +7 -0
- package/dist/types/extensions/colors/palette.d.ts +7 -0
- package/dist/types/extensions/editor-kit.d.ts +37 -0
- package/dist/types/extensions/embed/embed-block.vue.d.ts +16 -0
- package/dist/types/extensions/embed/index.d.ts +18 -0
- package/dist/types/extensions/embed/targets.d.ts +32 -0
- package/dist/types/extensions/formatting.d.ts +8 -0
- package/dist/types/extensions/image/image-block.vue.d.ts +16 -0
- package/dist/types/extensions/image/index.d.ts +16 -0
- package/dist/types/extensions/image/uploads.d.ts +35 -0
- package/dist/types/extensions/list-html.d.ts +11 -0
- package/dist/types/extensions/lists.d.ts +6 -0
- package/dist/types/extensions/mention/index.d.ts +11 -0
- package/dist/types/extensions/mention/mention-menu.vue.d.ts +7 -0
- package/dist/types/extensions/mention/state.d.ts +28 -0
- package/dist/types/extensions/table/commands.d.ts +69 -0
- package/dist/types/extensions/table/html.d.ts +4 -0
- package/dist/types/extensions/table/index.d.ts +8 -0
- package/dist/types/extensions/table/plugin.d.ts +4 -0
- package/dist/types/extensions/table/schema.d.ts +23 -0
- package/dist/types/extensions/table/table-controls.vue.d.ts +3 -0
- package/dist/types/extensions/table/view.d.ts +13 -0
- package/dist/types/extensions/toggle/index.d.ts +7 -0
- package/dist/types/extensions/toggle/state.d.ts +17 -0
- package/dist/types/extensions/toggle/toggle-block.vue.d.ts +14 -0
- package/dist/types/index.d.ts +266 -0
- package/dist/types/locale.d.ts +195 -0
- package/dist/types/markdown/index.d.ts +14 -0
- package/dist/types/use-comments.d.ts +17 -0
- package/dist/use-comments.js +141 -0
- package/dist/use-comments.js.map +1 -0
- package/package.json +98 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-block.js","names":["_sfc_main"],"sources":["../../../src/extensions/image/image-block.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ImageIcon, LinkIcon, UploadIcon } from '@dou-ui/icons'\nimport { DouButton, DouInput, DouSpinner } from 'dou-ui'\nimport { computed, onBeforeUnmount, shallowRef, useTemplateRef } from 'vue'\nimport type { BlockProps } from '../../core/model'\nimport { useEditor } from '../../editor'\nimport { isSafeImageSrc, type Uploads } from './uploads'\n\nconst props = defineProps<{\n block: { id: string; type: string; props: BlockProps }\n updateProps: (props: BlockProps) => void\n selected: boolean\n editable: boolean\n uploads: Uploads\n}>()\n\nconst { messages, view } = useEditor()\nconst text = computed(() => messages.value.image)\n\nconst src = computed(() => {\n const value = String(props.block.props.src ?? '')\n return value && isSafeImageSrc(value) ? value : ''\n})\nconst upload = computed(() => props.uploads.items.get(props.block.id))\nconst shown = computed(() => src.value || upload.value?.preview || '')\n// without a width the image shows at its own size, up to the full line\nconst width = computed(() => {\n const value = Number(props.block.props.width)\n return value > 0 ? Math.min(100, Math.max(20, value)) : null\n})\nconst caption = computed(() => String(props.block.props.caption ?? ''))\n\n/* adding an image */\n\nconst fileInput = useTemplateRef<HTMLInputElement>('fileInput')\nconst embedding = shallowRef(false)\nconst url = shallowRef('')\n\nfunction pick() {\n fileInput.value?.click()\n}\n\nfunction onFile(event: Event) {\n const input = event.target as HTMLInputElement\n const file = input.files?.[0]\n input.value = ''\n if (file && view.value) props.uploads.start(view.value, props.block.id, file)\n}\n\nfunction embed() {\n const value = url.value.trim()\n if (!value || !isSafeImageSrc(value)) return\n props.updateProps({ src: value })\n embedding.value = false\n url.value = ''\n}\n\n/* resizing: the image stays centered, so a handle moves both edges */\n\nconst figure = useTemplateRef<HTMLElement>('figure')\nconst dragWidth = shallowRef<number | null>(null)\nlet stop: (() => void) | null = null\n\nfunction startResize(event: PointerEvent, side: -1 | 1) {\n const container = figure.value?.parentElement\n if (!container || event.button !== 0) return\n event.preventDefault()\n const startX = event.clientX\n const total = container.getBoundingClientRect().width\n const startWidth = (figure.value!.getBoundingClientRect().width * 100) / total\n const onMove = (move: PointerEvent) => {\n const delta = ((move.clientX - startX) * side * 2 * 100) / total\n dragWidth.value = Math.round(Math.min(100, Math.max(20, startWidth + delta)))\n }\n const onUp = () => {\n stop?.()\n if (dragWidth.value !== null && dragWidth.value !== width.value)\n props.updateProps({ width: dragWidth.value })\n dragWidth.value = null\n }\n const onCancel = () => {\n stop?.()\n dragWidth.value = null\n }\n window.addEventListener('pointermove', onMove)\n window.addEventListener('pointerup', onUp)\n window.addEventListener('pointercancel', onCancel)\n stop = () => {\n window.removeEventListener('pointermove', onMove)\n window.removeEventListener('pointerup', onUp)\n window.removeEventListener('pointercancel', onCancel)\n stop = null\n }\n}\n\nonBeforeUnmount(() => stop?.())\n\n/** Enter ends the caption, unless it picks a word in an input method. */\nfunction onCaptionEnter(event: KeyboardEvent) {\n if (event.isComposing || event.keyCode === 229) return\n event.preventDefault()\n ;(event.target as HTMLInputElement).blur()\n}\n\nfunction onCaption(value: string) {\n props.updateProps({ caption: value })\n}\n</script>\n\n<template>\n <div class=\"dou-editor-image\" :class=\"{ 'is-selected': selected }\">\n <figure\n v-if=\"shown\"\n ref=\"figure\"\n class=\"dou-editor-image__figure\"\n :class=\"{ 'is-sized': (dragWidth ?? width) !== null }\"\n :style=\"(dragWidth ?? width) !== null ? { width: `${dragWidth ?? width}%` } : undefined\"\n >\n <div class=\"dou-editor-image__frame\">\n <img\n class=\"dou-editor-image__img\"\n :src=\"shown\"\n :alt=\"String(block.props.alt ?? '') || caption\"\n draggable=\"false\"\n />\n <div v-if=\"upload && !upload.failed\" class=\"dou-editor-image__status\">\n <DouSpinner size=\"sm\" />\n {{ text.uploading }}\n </div>\n <div v-else-if=\"upload?.failed\" class=\"dou-editor-image__status is-failed\">\n {{ text.failed }}\n <DouButton v-if=\"editable\" size=\"sm\" variant=\"outline\" @click=\"pick\">\n <template #leading><UploadIcon /></template>\n {{ text.upload }}\n </DouButton>\n </div>\n <template v-if=\"editable && src\">\n <span\n v-for=\"side in [-1, 1] as const\"\n :key=\"side\"\n class=\"dou-editor-image__handle\"\n :class=\"side < 0 ? 'is-left' : 'is-right'\"\n :title=\"text.resize\"\n data-editor-control\n @pointerdown=\"startResize($event, side)\"\n />\n </template>\n </div>\n <figcaption v-if=\"editable\" class=\"dou-editor-image__caption\">\n <input\n class=\"dou-editor-image__caption-input\"\n :value=\"caption\"\n :placeholder=\"text.caption\"\n @change=\"onCaption(($event.target as HTMLInputElement).value)\"\n @keydown.enter=\"onCaptionEnter\"\n />\n </figcaption>\n <figcaption v-else-if=\"caption\" class=\"dou-editor-image__caption\">{{ caption }}</figcaption>\n </figure>\n\n <div v-else-if=\"editable\" class=\"dou-editor-image__empty\">\n <ImageIcon class=\"dou-editor-image__empty-icon\" />\n <template v-if=\"!embedding\">\n <span class=\"dou-editor-image__empty-text\">{{ text.empty }}</span>\n <div class=\"dou-editor-image__actions\">\n <DouButton size=\"sm\" variant=\"outline\" @click=\"pick\">\n <template #leading><UploadIcon /></template>\n {{ text.upload }}\n </DouButton>\n <DouButton size=\"sm\" variant=\"ghost\" @click=\"embedding = true\">\n <template #leading><LinkIcon /></template>\n {{ text.embed }}\n </DouButton>\n </div>\n </template>\n <form v-else class=\"dou-editor-image__embed\" @submit.prevent=\"embed\">\n <DouInput\n v-model=\"url\"\n size=\"sm\"\n :placeholder=\"text.urlPlaceholder\"\n autofocus\n @keydown.esc=\"embedding = false\"\n />\n <DouButton size=\"sm\" variant=\"solid\" native-type=\"submit\">{{ text.apply }}</DouButton>\n </form>\n </div>\n\n <input v-if=\"editable\" ref=\"fileInput\" type=\"file\" accept=\"image/*\" hidden @change=\"onFile\" />\n </div>\n</template>\n"],"mappings":""}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { useEditor } from "../../editor.js";
|
|
2
|
+
import { isSafeImageSrc } from "./uploads.js";
|
|
3
|
+
import { DouButton, DouInput, DouSpinner } from "dou-ui";
|
|
4
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, renderList, shallowRef, toDisplayString, unref, useTemplateRef, withCtx, withKeys, withModifiers } from "vue";
|
|
5
|
+
import { ImageIcon, LinkIcon, UploadIcon } from "@dou-ui/icons";
|
|
6
|
+
// src/extensions/image/image-block.vue?vue&type=script&setup=true&lang.ts
|
|
7
|
+
var _hoisted_1 = { class: "dou-editor-image__frame" };
|
|
8
|
+
var _hoisted_2 = ["src", "alt"];
|
|
9
|
+
var _hoisted_3 = {
|
|
10
|
+
key: 0,
|
|
11
|
+
class: "dou-editor-image__status"
|
|
12
|
+
};
|
|
13
|
+
var _hoisted_4 = {
|
|
14
|
+
key: 1,
|
|
15
|
+
class: "dou-editor-image__status is-failed"
|
|
16
|
+
};
|
|
17
|
+
var _hoisted_5 = ["title", "onPointerdown"];
|
|
18
|
+
var _hoisted_6 = {
|
|
19
|
+
key: 0,
|
|
20
|
+
class: "dou-editor-image__caption"
|
|
21
|
+
};
|
|
22
|
+
var _hoisted_7 = ["value", "placeholder"];
|
|
23
|
+
var _hoisted_8 = {
|
|
24
|
+
key: 1,
|
|
25
|
+
class: "dou-editor-image__caption"
|
|
26
|
+
};
|
|
27
|
+
var _hoisted_9 = {
|
|
28
|
+
key: 1,
|
|
29
|
+
class: "dou-editor-image__empty"
|
|
30
|
+
};
|
|
31
|
+
var _hoisted_10 = { class: "dou-editor-image__empty-text" };
|
|
32
|
+
var _hoisted_11 = { class: "dou-editor-image__actions" };
|
|
33
|
+
var image_block_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
34
|
+
__name: "image-block",
|
|
35
|
+
props: {
|
|
36
|
+
block: {},
|
|
37
|
+
updateProps: { type: Function },
|
|
38
|
+
selected: { type: Boolean },
|
|
39
|
+
editable: { type: Boolean },
|
|
40
|
+
uploads: {}
|
|
41
|
+
},
|
|
42
|
+
setup(__props) {
|
|
43
|
+
const props = __props;
|
|
44
|
+
const { messages, view } = useEditor();
|
|
45
|
+
const text = computed(() => messages.value.image);
|
|
46
|
+
const src = computed(() => {
|
|
47
|
+
const value = String(props.block.props.src ?? "");
|
|
48
|
+
return value && isSafeImageSrc(value) ? value : "";
|
|
49
|
+
});
|
|
50
|
+
const upload = computed(() => props.uploads.items.get(props.block.id));
|
|
51
|
+
const shown = computed(() => src.value || upload.value?.preview || "");
|
|
52
|
+
const width = computed(() => {
|
|
53
|
+
const value = Number(props.block.props.width);
|
|
54
|
+
return value > 0 ? Math.min(100, Math.max(20, value)) : null;
|
|
55
|
+
});
|
|
56
|
+
const caption = computed(() => String(props.block.props.caption ?? ""));
|
|
57
|
+
const fileInput = useTemplateRef("fileInput");
|
|
58
|
+
const embedding = shallowRef(false);
|
|
59
|
+
const url = shallowRef("");
|
|
60
|
+
function pick() {
|
|
61
|
+
fileInput.value?.click();
|
|
62
|
+
}
|
|
63
|
+
function onFile(event) {
|
|
64
|
+
const input = event.target;
|
|
65
|
+
const file = input.files?.[0];
|
|
66
|
+
input.value = "";
|
|
67
|
+
if (file && view.value) props.uploads.start(view.value, props.block.id, file);
|
|
68
|
+
}
|
|
69
|
+
function embed() {
|
|
70
|
+
const value = url.value.trim();
|
|
71
|
+
if (!value || !isSafeImageSrc(value)) return;
|
|
72
|
+
props.updateProps({ src: value });
|
|
73
|
+
embedding.value = false;
|
|
74
|
+
url.value = "";
|
|
75
|
+
}
|
|
76
|
+
const figure = useTemplateRef("figure");
|
|
77
|
+
const dragWidth = shallowRef(null);
|
|
78
|
+
let stop = null;
|
|
79
|
+
function startResize(event, side) {
|
|
80
|
+
const container = figure.value?.parentElement;
|
|
81
|
+
if (!container || event.button !== 0) return;
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
const startX = event.clientX;
|
|
84
|
+
const total = container.getBoundingClientRect().width;
|
|
85
|
+
const startWidth = figure.value.getBoundingClientRect().width * 100 / total;
|
|
86
|
+
const onMove = (move) => {
|
|
87
|
+
const delta = (move.clientX - startX) * side * 2 * 100 / total;
|
|
88
|
+
dragWidth.value = Math.round(Math.min(100, Math.max(20, startWidth + delta)));
|
|
89
|
+
};
|
|
90
|
+
const onUp = () => {
|
|
91
|
+
stop?.();
|
|
92
|
+
if (dragWidth.value !== null && dragWidth.value !== width.value) props.updateProps({ width: dragWidth.value });
|
|
93
|
+
dragWidth.value = null;
|
|
94
|
+
};
|
|
95
|
+
const onCancel = () => {
|
|
96
|
+
stop?.();
|
|
97
|
+
dragWidth.value = null;
|
|
98
|
+
};
|
|
99
|
+
window.addEventListener("pointermove", onMove);
|
|
100
|
+
window.addEventListener("pointerup", onUp);
|
|
101
|
+
window.addEventListener("pointercancel", onCancel);
|
|
102
|
+
stop = () => {
|
|
103
|
+
window.removeEventListener("pointermove", onMove);
|
|
104
|
+
window.removeEventListener("pointerup", onUp);
|
|
105
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
106
|
+
stop = null;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
onBeforeUnmount(() => stop?.());
|
|
110
|
+
/** Enter ends the caption, unless it picks a word in an input method. */
|
|
111
|
+
function onCaptionEnter(event) {
|
|
112
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
event.target.blur();
|
|
115
|
+
}
|
|
116
|
+
function onCaption(value) {
|
|
117
|
+
props.updateProps({ caption: value });
|
|
118
|
+
}
|
|
119
|
+
return (_ctx, _cache) => {
|
|
120
|
+
return openBlock(), createElementBlock("div", { class: normalizeClass(["dou-editor-image", { "is-selected": __props.selected }]) }, [shown.value ? (openBlock(), createElementBlock("figure", {
|
|
121
|
+
key: 0,
|
|
122
|
+
ref_key: "figure",
|
|
123
|
+
ref: figure,
|
|
124
|
+
class: normalizeClass(["dou-editor-image__figure", { "is-sized": (dragWidth.value ?? width.value) !== null }]),
|
|
125
|
+
style: normalizeStyle((dragWidth.value ?? width.value) !== null ? { width: `${dragWidth.value ?? width.value}%` } : void 0)
|
|
126
|
+
}, [createElementVNode("div", _hoisted_1, [
|
|
127
|
+
createElementVNode("img", {
|
|
128
|
+
class: "dou-editor-image__img",
|
|
129
|
+
src: shown.value,
|
|
130
|
+
alt: String(__props.block.props.alt ?? "") || caption.value,
|
|
131
|
+
draggable: "false"
|
|
132
|
+
}, null, 8, _hoisted_2),
|
|
133
|
+
upload.value && !upload.value.failed ? (openBlock(), createElementBlock("div", _hoisted_3, [createVNode(unref(DouSpinner), { size: "sm" }), createTextVNode(" " + toDisplayString(text.value.uploading), 1)])) : upload.value?.failed ? (openBlock(), createElementBlock("div", _hoisted_4, [createTextVNode(toDisplayString(text.value.failed) + " ", 1), __props.editable ? (openBlock(), createBlock(unref(DouButton), {
|
|
134
|
+
key: 0,
|
|
135
|
+
size: "sm",
|
|
136
|
+
variant: "outline",
|
|
137
|
+
onClick: pick
|
|
138
|
+
}, {
|
|
139
|
+
leading: withCtx(() => [createVNode(unref(UploadIcon))]),
|
|
140
|
+
default: withCtx(() => [createTextVNode(" " + toDisplayString(text.value.upload), 1)]),
|
|
141
|
+
_: 1
|
|
142
|
+
})) : createCommentVNode("", true)])) : createCommentVNode("", true),
|
|
143
|
+
__props.editable && src.value ? (openBlock(), createElementBlock(Fragment, { key: 2 }, renderList([-1, 1], (side) => {
|
|
144
|
+
return createElementVNode("span", {
|
|
145
|
+
key: side,
|
|
146
|
+
class: normalizeClass(["dou-editor-image__handle", side < 0 ? "is-left" : "is-right"]),
|
|
147
|
+
title: text.value.resize,
|
|
148
|
+
"data-editor-control": "",
|
|
149
|
+
onPointerdown: ($event) => startResize($event, side)
|
|
150
|
+
}, null, 42, _hoisted_5);
|
|
151
|
+
}), 64)) : createCommentVNode("", true)
|
|
152
|
+
]), __props.editable ? (openBlock(), createElementBlock("figcaption", _hoisted_6, [createElementVNode("input", {
|
|
153
|
+
class: "dou-editor-image__caption-input",
|
|
154
|
+
value: caption.value,
|
|
155
|
+
placeholder: text.value.caption,
|
|
156
|
+
onChange: _cache[0] || (_cache[0] = ($event) => onCaption($event.target.value)),
|
|
157
|
+
onKeydown: withKeys(onCaptionEnter, ["enter"])
|
|
158
|
+
}, null, 40, _hoisted_7)])) : caption.value ? (openBlock(), createElementBlock("figcaption", _hoisted_8, toDisplayString(caption.value), 1)) : createCommentVNode("", true)], 6)) : __props.editable ? (openBlock(), createElementBlock("div", _hoisted_9, [createVNode(unref(ImageIcon), { class: "dou-editor-image__empty-icon" }), !embedding.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createElementVNode("span", _hoisted_10, toDisplayString(text.value.empty), 1), createElementVNode("div", _hoisted_11, [createVNode(unref(DouButton), {
|
|
159
|
+
size: "sm",
|
|
160
|
+
variant: "outline",
|
|
161
|
+
onClick: pick
|
|
162
|
+
}, {
|
|
163
|
+
leading: withCtx(() => [createVNode(unref(UploadIcon))]),
|
|
164
|
+
default: withCtx(() => [createTextVNode(" " + toDisplayString(text.value.upload), 1)]),
|
|
165
|
+
_: 1
|
|
166
|
+
}), createVNode(unref(DouButton), {
|
|
167
|
+
size: "sm",
|
|
168
|
+
variant: "ghost",
|
|
169
|
+
onClick: _cache[1] || (_cache[1] = ($event) => embedding.value = true)
|
|
170
|
+
}, {
|
|
171
|
+
leading: withCtx(() => [createVNode(unref(LinkIcon))]),
|
|
172
|
+
default: withCtx(() => [createTextVNode(" " + toDisplayString(text.value.embed), 1)]),
|
|
173
|
+
_: 1
|
|
174
|
+
})])], 64)) : (openBlock(), createElementBlock("form", {
|
|
175
|
+
key: 1,
|
|
176
|
+
class: "dou-editor-image__embed",
|
|
177
|
+
onSubmit: withModifiers(embed, ["prevent"])
|
|
178
|
+
}, [createVNode(unref(DouInput), {
|
|
179
|
+
modelValue: url.value,
|
|
180
|
+
"onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => url.value = $event),
|
|
181
|
+
size: "sm",
|
|
182
|
+
placeholder: text.value.urlPlaceholder,
|
|
183
|
+
autofocus: "",
|
|
184
|
+
onKeydown: _cache[3] || (_cache[3] = withKeys(($event) => embedding.value = false, ["esc"]))
|
|
185
|
+
}, null, 8, ["modelValue", "placeholder"]), createVNode(unref(DouButton), {
|
|
186
|
+
size: "sm",
|
|
187
|
+
variant: "solid",
|
|
188
|
+
"native-type": "submit"
|
|
189
|
+
}, {
|
|
190
|
+
default: withCtx(() => [createTextVNode(toDisplayString(text.value.apply), 1)]),
|
|
191
|
+
_: 1
|
|
192
|
+
})], 32))])) : createCommentVNode("", true), __props.editable ? (openBlock(), createElementBlock("input", {
|
|
193
|
+
key: 2,
|
|
194
|
+
ref_key: "fileInput",
|
|
195
|
+
ref: fileInput,
|
|
196
|
+
type: "file",
|
|
197
|
+
accept: "image/*",
|
|
198
|
+
hidden: "",
|
|
199
|
+
onChange: onFile
|
|
200
|
+
}, null, 544)) : createCommentVNode("", true)], 2);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
export { image_block_vue_vue_type_script_setup_true_lang_default as default };
|
|
205
|
+
|
|
206
|
+
//# sourceMappingURL=image-block.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-block.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_normalizeClass","_normalizeStyle","_createElementVNode","_createVNode","_unref","_createTextVNode","_toDisplayString","_createBlock","_withCtx","_createCommentVNode","_Fragment","_renderList","_withKeys","_withModifiers"],"sources":["../../../src/extensions/image/image-block.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ImageIcon, LinkIcon, UploadIcon } from '@dou-ui/icons'\nimport { DouButton, DouInput, DouSpinner } from 'dou-ui'\nimport { computed, onBeforeUnmount, shallowRef, useTemplateRef } from 'vue'\nimport type { BlockProps } from '../../core/model'\nimport { useEditor } from '../../editor'\nimport { isSafeImageSrc, type Uploads } from './uploads'\n\nconst props = defineProps<{\n block: { id: string; type: string; props: BlockProps }\n updateProps: (props: BlockProps) => void\n selected: boolean\n editable: boolean\n uploads: Uploads\n}>()\n\nconst { messages, view } = useEditor()\nconst text = computed(() => messages.value.image)\n\nconst src = computed(() => {\n const value = String(props.block.props.src ?? '')\n return value && isSafeImageSrc(value) ? value : ''\n})\nconst upload = computed(() => props.uploads.items.get(props.block.id))\nconst shown = computed(() => src.value || upload.value?.preview || '')\n// without a width the image shows at its own size, up to the full line\nconst width = computed(() => {\n const value = Number(props.block.props.width)\n return value > 0 ? Math.min(100, Math.max(20, value)) : null\n})\nconst caption = computed(() => String(props.block.props.caption ?? ''))\n\n/* adding an image */\n\nconst fileInput = useTemplateRef<HTMLInputElement>('fileInput')\nconst embedding = shallowRef(false)\nconst url = shallowRef('')\n\nfunction pick() {\n fileInput.value?.click()\n}\n\nfunction onFile(event: Event) {\n const input = event.target as HTMLInputElement\n const file = input.files?.[0]\n input.value = ''\n if (file && view.value) props.uploads.start(view.value, props.block.id, file)\n}\n\nfunction embed() {\n const value = url.value.trim()\n if (!value || !isSafeImageSrc(value)) return\n props.updateProps({ src: value })\n embedding.value = false\n url.value = ''\n}\n\n/* resizing: the image stays centered, so a handle moves both edges */\n\nconst figure = useTemplateRef<HTMLElement>('figure')\nconst dragWidth = shallowRef<number | null>(null)\nlet stop: (() => void) | null = null\n\nfunction startResize(event: PointerEvent, side: -1 | 1) {\n const container = figure.value?.parentElement\n if (!container || event.button !== 0) return\n event.preventDefault()\n const startX = event.clientX\n const total = container.getBoundingClientRect().width\n const startWidth = (figure.value!.getBoundingClientRect().width * 100) / total\n const onMove = (move: PointerEvent) => {\n const delta = ((move.clientX - startX) * side * 2 * 100) / total\n dragWidth.value = Math.round(Math.min(100, Math.max(20, startWidth + delta)))\n }\n const onUp = () => {\n stop?.()\n if (dragWidth.value !== null && dragWidth.value !== width.value)\n props.updateProps({ width: dragWidth.value })\n dragWidth.value = null\n }\n const onCancel = () => {\n stop?.()\n dragWidth.value = null\n }\n window.addEventListener('pointermove', onMove)\n window.addEventListener('pointerup', onUp)\n window.addEventListener('pointercancel', onCancel)\n stop = () => {\n window.removeEventListener('pointermove', onMove)\n window.removeEventListener('pointerup', onUp)\n window.removeEventListener('pointercancel', onCancel)\n stop = null\n }\n}\n\nonBeforeUnmount(() => stop?.())\n\n/** Enter ends the caption, unless it picks a word in an input method. */\nfunction onCaptionEnter(event: KeyboardEvent) {\n if (event.isComposing || event.keyCode === 229) return\n event.preventDefault()\n ;(event.target as HTMLInputElement).blur()\n}\n\nfunction onCaption(value: string) {\n props.updateProps({ caption: value })\n}\n</script>\n\n<template>\n <div class=\"dou-editor-image\" :class=\"{ 'is-selected': selected }\">\n <figure\n v-if=\"shown\"\n ref=\"figure\"\n class=\"dou-editor-image__figure\"\n :class=\"{ 'is-sized': (dragWidth ?? width) !== null }\"\n :style=\"(dragWidth ?? width) !== null ? { width: `${dragWidth ?? width}%` } : undefined\"\n >\n <div class=\"dou-editor-image__frame\">\n <img\n class=\"dou-editor-image__img\"\n :src=\"shown\"\n :alt=\"String(block.props.alt ?? '') || caption\"\n draggable=\"false\"\n />\n <div v-if=\"upload && !upload.failed\" class=\"dou-editor-image__status\">\n <DouSpinner size=\"sm\" />\n {{ text.uploading }}\n </div>\n <div v-else-if=\"upload?.failed\" class=\"dou-editor-image__status is-failed\">\n {{ text.failed }}\n <DouButton v-if=\"editable\" size=\"sm\" variant=\"outline\" @click=\"pick\">\n <template #leading><UploadIcon /></template>\n {{ text.upload }}\n </DouButton>\n </div>\n <template v-if=\"editable && src\">\n <span\n v-for=\"side in [-1, 1] as const\"\n :key=\"side\"\n class=\"dou-editor-image__handle\"\n :class=\"side < 0 ? 'is-left' : 'is-right'\"\n :title=\"text.resize\"\n data-editor-control\n @pointerdown=\"startResize($event, side)\"\n />\n </template>\n </div>\n <figcaption v-if=\"editable\" class=\"dou-editor-image__caption\">\n <input\n class=\"dou-editor-image__caption-input\"\n :value=\"caption\"\n :placeholder=\"text.caption\"\n @change=\"onCaption(($event.target as HTMLInputElement).value)\"\n @keydown.enter=\"onCaptionEnter\"\n />\n </figcaption>\n <figcaption v-else-if=\"caption\" class=\"dou-editor-image__caption\">{{ caption }}</figcaption>\n </figure>\n\n <div v-else-if=\"editable\" class=\"dou-editor-image__empty\">\n <ImageIcon class=\"dou-editor-image__empty-icon\" />\n <template v-if=\"!embedding\">\n <span class=\"dou-editor-image__empty-text\">{{ text.empty }}</span>\n <div class=\"dou-editor-image__actions\">\n <DouButton size=\"sm\" variant=\"outline\" @click=\"pick\">\n <template #leading><UploadIcon /></template>\n {{ text.upload }}\n </DouButton>\n <DouButton size=\"sm\" variant=\"ghost\" @click=\"embedding = true\">\n <template #leading><LinkIcon /></template>\n {{ text.embed }}\n </DouButton>\n </div>\n </template>\n <form v-else class=\"dou-editor-image__embed\" @submit.prevent=\"embed\">\n <DouInput\n v-model=\"url\"\n size=\"sm\"\n :placeholder=\"text.urlPlaceholder\"\n autofocus\n @keydown.esc=\"embedding = false\"\n />\n <DouButton size=\"sm\" variant=\"solid\" native-type=\"submit\">{{ text.apply }}</DouButton>\n </form>\n </div>\n\n <input v-if=\"editable\" ref=\"fileInput\" type=\"file\" accept=\"image/*\" hidden @change=\"onFile\" />\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQA,MAAM,QAAQ;EAQd,MAAM,EAAE,UAAU,SAAS,UAAU;EACrC,MAAM,OAAO,eAAe,SAAS,MAAM,KAAK;EAEhD,MAAM,MAAM,eAAe;GACzB,MAAM,QAAQ,OAAO,MAAM,MAAM,MAAM,OAAO,EAAE;GAChD,OAAO,SAAS,eAAe,KAAK,IAAI,QAAQ;EAClD,CAAC;EACD,MAAM,SAAS,eAAe,MAAM,QAAQ,MAAM,IAAI,MAAM,MAAM,EAAE,CAAC;EACrE,MAAM,QAAQ,eAAe,IAAI,SAAS,OAAO,OAAO,WAAW,EAAE;EAErE,MAAM,QAAQ,eAAe;GAC3B,MAAM,QAAQ,OAAO,MAAM,MAAM,MAAM,KAAK;GAC5C,OAAO,QAAQ,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI;EAC1D,CAAC;EACD,MAAM,UAAU,eAAe,OAAO,MAAM,MAAM,MAAM,WAAW,EAAE,CAAC;EAItE,MAAM,YAAY,eAAiC,WAAW;EAC9D,MAAM,YAAY,WAAW,KAAK;EAClC,MAAM,MAAM,WAAW,EAAE;EAEzB,SAAS,OAAO;GACd,UAAU,OAAO,MAAM;EACzB;EAEA,SAAS,OAAO,OAAc;GAC5B,MAAM,QAAQ,MAAM;GACpB,MAAM,OAAO,MAAM,QAAQ;GAC3B,MAAM,QAAQ;GACd,IAAI,QAAQ,KAAK,OAAO,MAAM,QAAQ,MAAM,KAAK,OAAO,MAAM,MAAM,IAAI,IAAI;EAC9E;EAEA,SAAS,QAAQ;GACf,MAAM,QAAQ,IAAI,MAAM,KAAK;GAC7B,IAAI,CAAC,SAAS,CAAC,eAAe,KAAK,GAAG;GACtC,MAAM,YAAY,EAAE,KAAK,MAAM,CAAC;GAChC,UAAU,QAAQ;GAClB,IAAI,QAAQ;EACd;EAIA,MAAM,SAAS,eAA4B,QAAQ;EACnD,MAAM,YAAY,WAA0B,IAAI;EAChD,IAAI,OAA4B;EAEhC,SAAS,YAAY,OAAqB,MAAc;GACtD,MAAM,YAAY,OAAO,OAAO;GAChC,IAAI,CAAC,aAAa,MAAM,WAAW,GAAG;GACtC,MAAM,eAAe;GACrB,MAAM,SAAS,MAAM;GACrB,MAAM,QAAQ,UAAU,sBAAsB,CAAC,CAAC;GAChD,MAAM,aAAc,OAAO,MAAO,sBAAsB,CAAC,CAAC,QAAQ,MAAO;GACzE,MAAM,UAAU,SAAuB;IACrC,MAAM,SAAU,KAAK,UAAU,UAAU,OAAO,IAAI,MAAO;IAC3D,UAAU,QAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,aAAa,KAAK,CAAC,CAAC;GAC9E;GACA,MAAM,aAAa;IACjB,OAAO;IACP,IAAI,UAAU,UAAU,QAAQ,UAAU,UAAU,MAAM,OACxD,MAAM,YAAY,EAAE,OAAO,UAAU,MAAM,CAAC;IAC9C,UAAU,QAAQ;GACpB;GACA,MAAM,iBAAiB;IACrB,OAAO;IACP,UAAU,QAAQ;GACpB;GACA,OAAO,iBAAiB,eAAe,MAAM;GAC7C,OAAO,iBAAiB,aAAa,IAAI;GACzC,OAAO,iBAAiB,iBAAiB,QAAQ;GACjD,aAAa;IACX,OAAO,oBAAoB,eAAe,MAAM;IAChD,OAAO,oBAAoB,aAAa,IAAI;IAC5C,OAAO,oBAAoB,iBAAiB,QAAQ;IACpD,OAAO;GACT;EACF;EAEA,sBAAsB,OAAO,CAAC;;EAG9B,SAAS,eAAe,OAAsB;GAC5C,IAAI,MAAM,eAAe,MAAM,YAAY,KAAK;GAChD,MAAM,eAAe;GACpB,MAAO,OAA4B,KAAK;EAC3C;EAEA,SAAS,UAAU,OAAe;GAChC,MAAM,YAAY,EAAE,SAAS,MAAM,CAAC;EACtC;;GAIE,OAAAA,UAAA,GAAAC,mBA8EM,OAAA,EA9ED,OAAKC,eAAA,CAAC,oBAAkB,EAAA,eAA0B,QAAA,SAAQ,CAAA,CAAA,E,IAErD,MAAA,SADRF,UAAA,GAAAC,mBA+CS,UAAA;;IA7CH,SAAA;IAAJ,KAAI;IACJ,OAAKC,eAAA,CAAC,4BAA0B,EAAA,aACT,UAAA,SAAa,MAAA,WAAK,KAAA,CAAA,CAAA;IACxC,OAAKC,gBAAG,UAAA,SAAa,MAAA,WAAK,OAAA,EAAA,OAAA,GAAyB,UAAA,SAAa,MAAA,MAAK,GAAA,IAAQ,KAAA,CAAS;G,IAEvFC,mBA6BM,OA7BN,YA6BM;IA5BJA,mBAKE,OAAA;KAJA,OAAM;KACL,KAAK,MAAA;KACL,KAAK,OAAO,QAAA,MAAM,MAAM,OAAG,EAAA,KAAW,QAAA;KACvC,WAAU;;IAED,OAAA,SAAM,CAAK,OAAA,MAAO,UAA7BJ,UAAA,GAAAC,mBAGM,OAHN,YAGM,CAFJI,YAAwBC,MAAA,UAAA,GAAA,EAAZ,MAAK,KAAI,CAAA,GAAGC,gBAAA,MACxBC,gBAAG,KAAA,MAAK,SAAS,GAAA,CAAA,C,MAEH,OAAA,OAAQ,UAAxBR,UAAA,GAAAC,mBAMM,OANN,YAMM,CALDM,gBAAAC,gBAAA,KAAA,MAAK,MAAM,IAAG,KACjB,CAAA,GAAiB,QAAA,YAAjBR,UAAA,GAAAS,YAGYH,MAAA,SAAA,GAAA;;KAHe,MAAK;KAAK,SAAQ;KAAW,SAAO;;KAClD,SAAOI,cAAe,CAAdL,YAAcC,MAAA,UAAA,CAAA,C;KAAW,SAAAI,cAC5C,CAD4CH,gBAAA,MAC5CC,gBAAG,KAAA,MAAK,MAAM,GAAA,CAAA,C;;;IAGF,QAAA,YAAY,IAAA,SAC1BR,UAAA,GAAAC,mBAQEW,UAAA,EAAA,KAAA,EAAA,GAAAC,WAPe,CAAA,IAAA,CAAA,IAAR,SAAI;KADb,OAAAT,mBAQE,QAAA;MANC,KAAK;MACN,OAAKF,eAAA,CAAC,4BACE,OAAI,IAAA,YAAA,UAAA,CAAA;MACX,OAAO,KAAA,MAAK;MACb,uBAAA;MACC,gBAAW,WAAE,YAAY,QAAQ,IAAI;;;G,IAI1B,QAAA,YAAlBF,UAAA,GAAAC,mBAQa,cARb,YAQa,CAPXG,mBAME,SAAA;IALA,OAAM;IACL,OAAO,QAAA;IACP,aAAa,KAAA,MAAK;IAClB,UAAM,OAAA,OAAA,OAAA,MAAA,WAAE,UAAW,OAAO,OAA4B,KAAK;IAC3D,WAAOU,SAAQ,gBAAc,CAAA,OAAA,CAAA;G,8BAGX,QAAA,SAAvBd,UAAA,GAAAC,mBAA4F,cAA5F,YAA4FO,gBAAvB,QAAA,KAAO,GAAA,CAAA,K,qCAG9D,QAAA,YAAhBR,UAAA,GAAAC,mBAyBM,OAzBN,YAyBM,CAxBJI,YAAkDC,MAAA,SAAA,GAAA,EAAvC,OAAM,+BAA8B,CAAA,GAC9B,CAAA,UAAA,SAAjBN,UAAA,GAAAC,mBAYWW,UAAA,EAAA,KAAA,EAAA,GAAA,CAXTR,mBAAkE,QAAlE,aAAkEI,gBAApB,KAAA,MAAK,KAAK,GAAA,CAAA,GACxDJ,mBASM,OATN,aASM,CARJC,YAGYC,MAAA,SAAA,GAAA;IAHD,MAAK;IAAK,SAAQ;IAAW,SAAO;;IAClC,SAAOI,cAAe,CAAdL,YAAcC,MAAA,UAAA,CAAA,C;IAAW,SAAAI,cAC5C,CAD4CH,gBAAA,MAC5CC,gBAAG,KAAA,MAAK,MAAM,GAAA,CAAA,C;;G,IAEhBH,YAGYC,MAAA,SAAA,GAAA;IAHD,MAAK;IAAK,SAAQ;IAAS,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,UAAA,QAAS;;IACzC,SAAOI,cAAa,CAAZL,YAAYC,MAAA,QAAA,CAAA,C;IAAW,SAAAI,cAC1C,CAD0CH,gBAAA,MAC1CC,gBAAG,KAAA,MAAK,KAAK,GAAA,CAAA,C;;G,eAInBR,UAAA,GAAAC,mBASO,QAAA;;IATM,OAAM;IAA2B,UAAMc,cAAU,OAAK,CAAA,SAAA,CAAA;G,IACjEV,YAMEC,MAAA,QAAA,GAAA;IALS,YAAA,IAAA;IAAA,uBAAA,OAAA,OAAA,OAAA,MAAA,WAAA,IAAG,QAAA;IACZ,MAAK;IACJ,aAAa,KAAA,MAAK;IACnB,WAAA;IACC,WAAO,OAAA,OAAA,OAAA,KAAAQ,UAAA,WAAM,UAAA,QAAS,OAAA,CAAA,KAAA,CAAA;G,4CAEzBT,YAAsFC,MAAA,SAAA,GAAA;IAA3E,MAAK;IAAK,SAAQ;IAAQ,eAAY;;IAAS,SAAAI,cAAgB,CAAbH,gBAAAC,gBAAA,KAAA,MAAK,KAAK,GAAA,CAAA,C;;G,6CAI9D,QAAA,YAAbR,UAAA,GAAAC,mBAA8F,SAAA;;IAAnE,SAAA;IAAJ,KAAI;IAAY,MAAK;IAAO,QAAO;IAAU,QAAA;IAAQ,UAAQ"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { escapeHTML, escapeMarkdown } from "../../core/output.js";
|
|
2
|
+
import { defineBlock } from "../../define-block.js";
|
|
3
|
+
import { fileDropPlugin } from "../../core/file-drop.js";
|
|
4
|
+
import { createUploads, isSafeImageSrc, readAsDataUrl } from "./uploads.js";
|
|
5
|
+
import image_block_default from "./image-block.js";
|
|
6
|
+
import { defineComponent, h } from "vue";
|
|
7
|
+
import { ImageIcon } from "@dou-ui/icons";
|
|
8
|
+
// src/extensions/image/index.ts
|
|
9
|
+
/** A width in percent, as `blocksToHTML` writes it on the figure. */
|
|
10
|
+
function widthOf(dom) {
|
|
11
|
+
const match = /^(\d+(?:\.\d+)?)%$/.exec(dom.style?.width ?? "");
|
|
12
|
+
return match ? Math.min(100, Math.max(20, Number(match[1]))) : null;
|
|
13
|
+
}
|
|
14
|
+
var attrsOf = (dom) => {
|
|
15
|
+
const img = dom.tagName === "IMG" ? dom : dom.querySelector("img");
|
|
16
|
+
const src = img?.getAttribute("src") ?? "";
|
|
17
|
+
if (!src || !isSafeImageSrc(src)) return false;
|
|
18
|
+
return {
|
|
19
|
+
src,
|
|
20
|
+
alt: img?.getAttribute("alt") ?? "",
|
|
21
|
+
caption: dom.querySelector("figcaption")?.textContent?.trim() ?? img?.getAttribute("title") ?? "",
|
|
22
|
+
width: widthOf(dom)
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Image blocks: upload a file, paste or drop images, or embed a link. The
|
|
27
|
+
* image can be resized and given a caption.
|
|
28
|
+
*/
|
|
29
|
+
function image(options = {}) {
|
|
30
|
+
const uploads = createUploads(options.upload ?? readAsDataUrl);
|
|
31
|
+
const View = defineComponent({
|
|
32
|
+
name: "DouEditorImageView",
|
|
33
|
+
inheritAttrs: false,
|
|
34
|
+
setup(_props, { attrs }) {
|
|
35
|
+
return () => h(image_block_default, {
|
|
36
|
+
...attrs,
|
|
37
|
+
uploads
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
...defineBlock({
|
|
43
|
+
type: "image",
|
|
44
|
+
props: {
|
|
45
|
+
src: "",
|
|
46
|
+
alt: "",
|
|
47
|
+
caption: "",
|
|
48
|
+
width: null
|
|
49
|
+
},
|
|
50
|
+
content: "none",
|
|
51
|
+
component: View,
|
|
52
|
+
menu: {
|
|
53
|
+
label: (m) => m.types.image,
|
|
54
|
+
hint: (m) => m.hints.image,
|
|
55
|
+
icon: ImageIcon,
|
|
56
|
+
keywords: [
|
|
57
|
+
"image",
|
|
58
|
+
"picture",
|
|
59
|
+
"img",
|
|
60
|
+
"tp",
|
|
61
|
+
"tupian",
|
|
62
|
+
"zhaopian"
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
spec: {
|
|
66
|
+
parseDOM: [{
|
|
67
|
+
tag: "figure",
|
|
68
|
+
getAttrs: (dom) => attrsOf(dom)
|
|
69
|
+
}, {
|
|
70
|
+
tag: "img[src]",
|
|
71
|
+
getAttrs: (dom) => attrsOf(dom)
|
|
72
|
+
}],
|
|
73
|
+
toDOM: (node) => {
|
|
74
|
+
const img = ["img", {
|
|
75
|
+
src: node.attrs.src,
|
|
76
|
+
alt: node.attrs.alt || node.attrs.caption
|
|
77
|
+
}];
|
|
78
|
+
return [
|
|
79
|
+
"figure",
|
|
80
|
+
{
|
|
81
|
+
class: "dou-editor__content dou-editor__content--image",
|
|
82
|
+
"data-content-type": "image",
|
|
83
|
+
style: node.attrs.width ? `width: ${node.attrs.width}%` : null
|
|
84
|
+
},
|
|
85
|
+
img,
|
|
86
|
+
...node.attrs.caption ? [["figcaption", node.attrs.caption]] : []
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}),
|
|
91
|
+
output: { blocks: { image: {
|
|
92
|
+
html: (node, { children }) => {
|
|
93
|
+
const { src, alt, caption, width } = node.props;
|
|
94
|
+
if (!src || !isSafeImageSrc(String(src))) return children;
|
|
95
|
+
const style = width ? ` style="width: ${Number(width)}%"` : "";
|
|
96
|
+
const label = escapeHTML(String(alt || caption || ""));
|
|
97
|
+
const figcaption = caption ? `<figcaption>${escapeHTML(String(caption))}</figcaption>` : "";
|
|
98
|
+
return `<figure${style}><img src="${escapeHTML(String(src))}" alt="${label}">${figcaption}</figure>${children}`;
|
|
99
|
+
},
|
|
100
|
+
markdown: (node, { children }) => {
|
|
101
|
+
const { src, alt, caption } = node.props;
|
|
102
|
+
if (!src || !isSafeImageSrc(String(src))) return children;
|
|
103
|
+
const label = escapeMarkdown(String(alt || caption || ""));
|
|
104
|
+
const title = caption ? ` "${String(caption).replace(/["\\]/g, "\\$&")}"` : "";
|
|
105
|
+
const written = `<>]/.test(String(src)) ? `<${String(src)}>` : String(src)}${title})`;
|
|
106
|
+
return children ? `${written}\n\n${children}` : written;
|
|
107
|
+
}
|
|
108
|
+
} } },
|
|
109
|
+
prepareHTML: (root) => {
|
|
110
|
+
for (const img of Array.from(root.querySelectorAll("p > img"))) {
|
|
111
|
+
const paragraph = img.parentElement;
|
|
112
|
+
if (paragraph.textContent?.trim() || paragraph.children.length > 1) continue;
|
|
113
|
+
paragraph.replaceWith(img);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
plugins: () => [fileDropPlugin({
|
|
117
|
+
accepts: (file) => file.type.startsWith("image/"),
|
|
118
|
+
insert: uploads.insert,
|
|
119
|
+
skipPaste: (event) => /<img/i.test(event.clipboardData?.getData("text/html") ?? "")
|
|
120
|
+
})]
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export { image, isSafeImageSrc, readAsDataUrl };
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["ImageBlock"],"sources":["../../../src/extensions/image/index.ts"],"sourcesContent":["import { ImageIcon } from '@dou-ui/icons'\nimport { fileDropPlugin } from '../../core/file-drop'\nimport { defineComponent, h } from 'vue'\nimport type { EditorExtension } from '../../core/extension'\nimport { escapeHTML, escapeMarkdown } from '../../core/output'\nimport { defineBlock } from '../../define-block'\nimport ImageBlock from './image-block.vue'\nimport { createUploads, isSafeImageSrc, readAsDataUrl, type FileUploader } from './uploads'\n\nexport interface ImageOptions {\n /**\n * Store a picked, pasted or dropped file and resolve with its address.\n * Without it, images are kept in the document as data URLs, which suits\n * trying things out but makes documents large.\n */\n upload?: FileUploader\n}\n\n/** A width in percent, as `blocksToHTML` writes it on the figure. */\nfunction widthOf(dom: HTMLElement): number | null {\n const match = /^(\\d+(?:\\.\\d+)?)%$/.exec(dom.style?.width ?? '')\n return match ? Math.min(100, Math.max(20, Number(match[1]))) : null\n}\n\nconst attrsOf = (dom: HTMLElement) => {\n const img = dom.tagName === 'IMG' ? (dom as HTMLImageElement) : dom.querySelector('img')\n const src = img?.getAttribute('src') ?? ''\n if (!src || !isSafeImageSrc(src)) return false\n return {\n src,\n alt: img?.getAttribute('alt') ?? '',\n caption:\n dom.querySelector('figcaption')?.textContent?.trim() ?? img?.getAttribute('title') ?? '',\n width: widthOf(dom),\n }\n}\n\n/**\n * Image blocks: upload a file, paste or drop images, or embed a link. The\n * image can be resized and given a caption.\n */\nexport function image(options: ImageOptions = {}): EditorExtension {\n const uploads = createUploads(options.upload ?? readAsDataUrl)\n const View = defineComponent({\n name: 'DouEditorImageView',\n inheritAttrs: false,\n setup(_props, { attrs }) {\n return () => h(ImageBlock, { ...attrs, uploads } as never)\n },\n })\n const block = defineBlock({\n type: 'image',\n props: { src: '', alt: '', caption: '', width: null },\n content: 'none',\n component: View,\n menu: {\n label: (m) => m.types.image,\n hint: (m) => m.hints.image,\n icon: ImageIcon,\n keywords: ['image', 'picture', 'img', 'tp', 'tupian', 'zhaopian'],\n },\n spec: {\n parseDOM: [\n { tag: 'figure', getAttrs: (dom) => attrsOf(dom as HTMLElement) },\n { tag: 'img[src]', getAttrs: (dom) => attrsOf(dom as HTMLElement) },\n ],\n toDOM: (node) => {\n const img = ['img', { src: node.attrs.src, alt: node.attrs.alt || node.attrs.caption }]\n const style = node.attrs.width ? `width: ${node.attrs.width}%` : null\n return [\n 'figure',\n {\n class: 'dou-editor__content dou-editor__content--image',\n 'data-content-type': 'image',\n style,\n },\n img,\n ...(node.attrs.caption ? [['figcaption', node.attrs.caption]] : []),\n ] as never\n },\n },\n })\n return {\n ...block,\n output: {\n blocks: {\n image: {\n html: (node, { children }) => {\n const { src, alt, caption, width } = node.props\n if (!src || !isSafeImageSrc(String(src))) return children\n const style = width ? ` style=\"width: ${Number(width)}%\"` : ''\n const label = escapeHTML(String(alt || caption || ''))\n const figcaption = caption\n ? `<figcaption>${escapeHTML(String(caption))}</figcaption>`\n : ''\n return `<figure${style}><img src=\"${escapeHTML(String(src))}\" alt=\"${label}\">${figcaption}</figure>${children}`\n },\n markdown: (node, { children }) => {\n const { src, alt, caption } = node.props\n if (!src || !isSafeImageSrc(String(src))) return children\n const label = escapeMarkdown(String(alt || caption || ''))\n const title = caption ? ` \"${String(caption).replace(/[\"\\\\]/g, '\\\\$&')}\"` : ''\n const address = /[\\s()<>]/.test(String(src)) ? `<${String(src)}>` : String(src)\n const written = ``\n return children ? `${written}\\n\\n${children}` : written\n },\n },\n },\n },\n // Markdown puts an image inside a paragraph of its own\n prepareHTML: (root) => {\n for (const img of Array.from(root.querySelectorAll('p > img'))) {\n const paragraph = img.parentElement!\n if (paragraph.textContent?.trim() || paragraph.children.length > 1) continue\n paragraph.replaceWith(img)\n }\n },\n plugins: () => [\n fileDropPlugin({\n accepts: (file) => file.type.startsWith('image/'),\n insert: uploads.insert,\n // an image copied from a page comes with its address: keep that\n skipPaste: (event) => /<img/i.test(event.clipboardData?.getData('text/html') ?? ''),\n }),\n ],\n }\n}\n\nexport { isSafeImageSrc, readAsDataUrl, type FileUploader }\n"],"mappings":";;;;;;;;;AAmBA,SAAS,QAAQ,KAAiC;CAChD,MAAM,QAAQ,qBAAqB,KAAK,IAAI,OAAO,SAAS,EAAE;CAC9D,OAAO,QAAQ,KAAK,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,MAAM,EAAE,CAAC,CAAC,IAAI;AACjE;AAEA,IAAM,WAAW,QAAqB;CACpC,MAAM,MAAM,IAAI,YAAY,QAAS,MAA2B,IAAI,cAAc,KAAK;CACvF,MAAM,MAAM,KAAK,aAAa,KAAK,KAAK;CACxC,IAAI,CAAC,OAAO,CAAC,eAAe,GAAG,GAAG,OAAO;CACzC,OAAO;EACL;EACA,KAAK,KAAK,aAAa,KAAK,KAAK;EACjC,SACE,IAAI,cAAc,YAAY,CAAC,EAAE,aAAa,KAAK,KAAK,KAAK,aAAa,OAAO,KAAK;EACxF,OAAO,QAAQ,GAAG;CACpB;AACF;;;;;AAMA,SAAgB,MAAM,UAAwB,CAAC,GAAoB;CACjE,MAAM,UAAU,cAAc,QAAQ,UAAU,aAAa;CAC7D,MAAM,OAAO,gBAAgB;EAC3B,MAAM;EACN,cAAc;EACd,MAAM,QAAQ,EAAE,SAAS;GACvB,aAAa,EAAEA,qBAAY;IAAE,GAAG;IAAO;GAAQ,CAAU;EAC3D;CACF,CAAC;CAiCD,OAAO;EACL,GAjCY,YAAY;GACxB,MAAM;GACN,OAAO;IAAE,KAAK;IAAI,KAAK;IAAI,SAAS;IAAI,OAAO;GAAK;GACpD,SAAS;GACT,WAAW;GACX,MAAM;IACJ,QAAQ,MAAM,EAAE,MAAM;IACtB,OAAO,MAAM,EAAE,MAAM;IACrB,MAAM;IACN,UAAU;KAAC;KAAS;KAAW;KAAO;KAAM;KAAU;IAAU;GAClE;GACA,MAAM;IACJ,UAAU,CACR;KAAE,KAAK;KAAU,WAAW,QAAQ,QAAQ,GAAkB;IAAE,GAChE;KAAE,KAAK;KAAY,WAAW,QAAQ,QAAQ,GAAkB;IAAE,CACpE;IACA,QAAQ,SAAS;KACf,MAAM,MAAM,CAAC,OAAO;MAAE,KAAK,KAAK,MAAM;MAAK,KAAK,KAAK,MAAM,OAAO,KAAK,MAAM;KAAQ,CAAC;KAEtF,OAAO;MACL;MACA;OACE,OAAO;OACP,qBAAqB;OACrB,OANU,KAAK,MAAM,QAAQ,UAAU,KAAK,MAAM,MAAM,KAAK;MAO/D;MACA;MACA,GAAI,KAAK,MAAM,UAAU,CAAC,CAAC,cAAc,KAAK,MAAM,OAAO,CAAC,IAAI,CAAC;KACnE;IACF;GACF;EACF,CAEK;EACH,QAAQ,EACN,QAAQ,EACN,OAAO;GACL,OAAO,MAAM,EAAE,eAAe;IAC5B,MAAM,EAAE,KAAK,KAAK,SAAS,UAAU,KAAK;IAC1C,IAAI,CAAC,OAAO,CAAC,eAAe,OAAO,GAAG,CAAC,GAAG,OAAO;IACjD,MAAM,QAAQ,QAAQ,kBAAkB,OAAO,KAAK,EAAE,MAAM;IAC5D,MAAM,QAAQ,WAAW,OAAO,OAAO,WAAW,EAAE,CAAC;IACrD,MAAM,aAAa,UACf,eAAe,WAAW,OAAO,OAAO,CAAC,EAAE,iBAC3C;IACJ,OAAO,UAAU,MAAM,aAAa,WAAW,OAAO,GAAG,CAAC,EAAE,SAAS,MAAM,IAAI,WAAW,WAAW;GACvG;GACA,WAAW,MAAM,EAAE,eAAe;IAChC,MAAM,EAAE,KAAK,KAAK,YAAY,KAAK;IACnC,IAAI,CAAC,OAAO,CAAC,eAAe,OAAO,GAAG,CAAC,GAAG,OAAO;IACjD,MAAM,QAAQ,eAAe,OAAO,OAAO,WAAW,EAAE,CAAC;IACzD,MAAM,QAAQ,UAAU,KAAK,OAAO,OAAO,CAAC,CAAC,QAAQ,UAAU,MAAM,EAAE,KAAK;IAE5E,MAAM,UAAU,KAAK,MAAM,IADX,WAAW,KAAK,OAAO,GAAG,CAAC,IAAI,IAAI,OAAO,GAAG,EAAE,KAAK,OAAO,GAAG,IACrC,MAAM;IAC/C,OAAO,WAAW,GAAG,QAAQ,MAAM,aAAa;GAClD;EACF,EACF,EACF;EAEA,cAAc,SAAS;GACrB,KAAK,MAAM,OAAO,MAAM,KAAK,KAAK,iBAAiB,SAAS,CAAC,GAAG;IAC9D,MAAM,YAAY,IAAI;IACtB,IAAI,UAAU,aAAa,KAAK,KAAK,UAAU,SAAS,SAAS,GAAG;IACpE,UAAU,YAAY,GAAG;GAC3B;EACF;EACA,eAAe,CACb,eAAe;GACb,UAAU,SAAS,KAAK,KAAK,WAAW,QAAQ;GAChD,QAAQ,QAAQ;GAEhB,YAAY,UAAU,QAAQ,KAAK,MAAM,eAAe,QAAQ,WAAW,KAAK,EAAE;EACpF,CAAC,CACH;CACF;AACF"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createId, warnOnce } from "../../core/model.js";
|
|
2
|
+
import { blockAround, findBlockById } from "../../core/blocks.js";
|
|
3
|
+
import { reactive } from "vue";
|
|
4
|
+
// src/extensions/image/uploads.ts
|
|
5
|
+
/** Without an uploader the image is kept in the document as a data URL. */
|
|
6
|
+
var readAsDataUrl = (file) => new Promise((resolve, reject) => {
|
|
7
|
+
const reader = new FileReader();
|
|
8
|
+
reader.addEventListener("load", () => resolve(String(reader.result)));
|
|
9
|
+
reader.addEventListener("error", () => reject(reader.error));
|
|
10
|
+
reader.readAsDataURL(file);
|
|
11
|
+
});
|
|
12
|
+
/** Image addresses that are safe to show: web, relative, and images held in the page. */
|
|
13
|
+
function isSafeImageSrc(src) {
|
|
14
|
+
const value = src.replace(/[\u0000- ]/g, "");
|
|
15
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
|
|
16
|
+
if (!scheme) return true;
|
|
17
|
+
if (scheme === "data") return /^data:image\//i.test(value);
|
|
18
|
+
return [
|
|
19
|
+
"http",
|
|
20
|
+
"https",
|
|
21
|
+
"blob"
|
|
22
|
+
].includes(scheme);
|
|
23
|
+
}
|
|
24
|
+
var imageTarget = {
|
|
25
|
+
type: "image",
|
|
26
|
+
isSafe: (src) => isSafeImageSrc(src),
|
|
27
|
+
allowed: "http, https, blob, relative addresses or data:image/ URLs"
|
|
28
|
+
};
|
|
29
|
+
function setAttrs(view, id, type, attrs) {
|
|
30
|
+
const block = findBlockById(view.state.doc, id);
|
|
31
|
+
if (!block || block.content.type.name !== type) return;
|
|
32
|
+
const tr = view.state.tr.setNodeMarkup(block.contentPos, void 0, {
|
|
33
|
+
...block.content.attrs,
|
|
34
|
+
...attrs
|
|
35
|
+
});
|
|
36
|
+
tr.setMeta("addToHistory", false);
|
|
37
|
+
view.dispatch(tr);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Uploads in progress, by block id. They live outside the document: only
|
|
41
|
+
* the finished address is written into it, so other collaborators never
|
|
42
|
+
* see a half-done image.
|
|
43
|
+
*/
|
|
44
|
+
function createUploads(upload, target = imageTarget) {
|
|
45
|
+
const items = reactive(/* @__PURE__ */ new Map());
|
|
46
|
+
const setSrc = (view, id, src) => setAttrs(view, id, target.type, { src });
|
|
47
|
+
/** Upload a file into the block `id`; `describe` writes its props (name, size) first. */
|
|
48
|
+
function start(view, id, file, describe = true) {
|
|
49
|
+
if (describe && target.propsOf) setAttrs(view, id, target.type, target.propsOf(file));
|
|
50
|
+
const old = items.get(id);
|
|
51
|
+
if (old) URL.revokeObjectURL(old.preview);
|
|
52
|
+
const preview = URL.createObjectURL(file);
|
|
53
|
+
items.set(id, {
|
|
54
|
+
preview,
|
|
55
|
+
failed: false
|
|
56
|
+
});
|
|
57
|
+
upload(file).then((src) => {
|
|
58
|
+
if (!target.isSafe(src)) {
|
|
59
|
+
const message = `the ${target.type} upload returned "${src}", which is not allowed: use ${target.allowed}`;
|
|
60
|
+
warnOnce(message);
|
|
61
|
+
throw new Error(`[@dou-ui/editor] ${message}`);
|
|
62
|
+
}
|
|
63
|
+
setSrc(view, id, src);
|
|
64
|
+
items.delete(id);
|
|
65
|
+
URL.revokeObjectURL(preview);
|
|
66
|
+
}).catch(() => {
|
|
67
|
+
if (items.get(id)?.preview === preview) items.set(id, {
|
|
68
|
+
preview,
|
|
69
|
+
failed: true
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** Insert one block per file after the block at `pos` and upload them; returns a position in the last one. */
|
|
74
|
+
function insert(view, files, pos) {
|
|
75
|
+
const { schema } = view.state;
|
|
76
|
+
const info = blockAround(view.state.doc.resolve(Math.min(pos, view.state.doc.content.size)));
|
|
77
|
+
const tr = view.state.tr;
|
|
78
|
+
const ids = files.map(() => createId());
|
|
79
|
+
const blocks = ids.map((id, index) => schema.nodes.block.create({ id }, schema.nodes[target.type].create(target.propsOf?.(files[index]) ?? null)));
|
|
80
|
+
if (!info) tr.insert(view.state.doc.content.size, blocks);
|
|
81
|
+
else if (info.content.type.name === "paragraph" && info.content.content.size === 0 && !info.group) tr.replaceWith(info.pos, info.pos + info.node.nodeSize, blocks);
|
|
82
|
+
else tr.insert(info.pos + info.node.nodeSize, blocks);
|
|
83
|
+
view.dispatch(tr.scrollIntoView());
|
|
84
|
+
files.forEach((file, index) => start(view, ids[index], file, false));
|
|
85
|
+
return findBlockById(view.state.doc, ids.at(-1))?.contentPos ?? pos;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
items,
|
|
89
|
+
start,
|
|
90
|
+
insert,
|
|
91
|
+
setSrc
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export { createUploads, isSafeImageSrc, readAsDataUrl };
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=uploads.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uploads.js","names":[],"sources":["../../../src/extensions/image/uploads.ts"],"sourcesContent":["import type { Node as PMNode } from 'prosemirror-model'\nimport type { EditorView } from 'prosemirror-view'\nimport { reactive } from 'vue'\nimport { blockAround, findBlockById } from '../../core/blocks'\nimport { createId, warnOnce } from '../../core/model'\n\n/** Stores a picked, pasted or dropped file (an image or an attachment) and resolves with its address. */\nexport type FileUploader = (file: File) => Promise<string>\n\n/** Without an uploader the image is kept in the document as a data URL. */\nexport const readAsDataUrl: FileUploader = (file) =>\n new Promise((resolve, reject) => {\n const reader = new FileReader()\n reader.addEventListener('load', () => resolve(String(reader.result)))\n reader.addEventListener('error', () => reject(reader.error))\n reader.readAsDataURL(file)\n })\n\n/** Image addresses that are safe to show: web, relative, and images held in the page. */\nexport function isSafeImageSrc(src: string): boolean {\n // eslint-disable-next-line no-control-regex\n const value = src.replace(/[\\u0000- ]/g, '')\n const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase()\n if (!scheme) return true\n // an image element never runs scripts, so SVG is fine here too\n if (scheme === 'data') return /^data:image\\//i.test(value)\n return ['http', 'https', 'blob'].includes(scheme)\n}\n\nexport interface Upload {\n /** A local copy shown while uploading. */\n preview: string\n failed: boolean\n}\n\n/** What blocks hold uploaded files: an image or an attachment. */\nexport interface UploadTarget {\n /** The block type. */\n type: string\n /** Whether an uploader's address may be stored. */\n isSafe: (src: string) => boolean\n /** Props taken from the file when it is picked, e.g. its name and size. */\n propsOf?: (file: File) => Record<string, unknown>\n /** The addresses `isSafe` takes, for the warning when an uploader returns another. */\n allowed: string\n}\n\nconst imageTarget: UploadTarget = {\n type: 'image',\n isSafe: (src) => isSafeImageSrc(src),\n allowed: 'http, https, blob, relative addresses or data:image/ URLs',\n}\n\nfunction setAttrs(view: EditorView, id: string, type: string, attrs: Record<string, unknown>) {\n const block = findBlockById(view.state.doc, id)\n if (!block || block.content.type.name !== type) return\n const tr = view.state.tr.setNodeMarkup(block.contentPos, undefined, {\n ...block.content.attrs,\n ...attrs,\n })\n // the file appearing is part of adding it, not a step of its own\n tr.setMeta('addToHistory', false)\n view.dispatch(tr)\n}\n\n/**\n * Uploads in progress, by block id. They live outside the document: only\n * the finished address is written into it, so other collaborators never\n * see a half-done image.\n */\nexport function createUploads(upload: FileUploader, target: UploadTarget = imageTarget) {\n const items = reactive(new Map<string, Upload>())\n const setSrc = (view: EditorView, id: string, src: string) =>\n setAttrs(view, id, target.type, { src })\n\n /** Upload a file into the block `id`; `describe` writes its props (name, size) first. */\n function start(view: EditorView, id: string, file: File, describe = true) {\n if (describe && target.propsOf) setAttrs(view, id, target.type, target.propsOf(file))\n const old = items.get(id)\n if (old) URL.revokeObjectURL(old.preview)\n const preview = URL.createObjectURL(file)\n items.set(id, { preview, failed: false })\n upload(file)\n .then((src) => {\n if (!target.isSafe(src)) {\n const message = `the ${target.type} upload returned \"${src}\", which is not allowed: use ${target.allowed}`\n warnOnce(message)\n throw new Error(`[@dou-ui/editor] ${message}`)\n }\n setSrc(view, id, src)\n items.delete(id)\n URL.revokeObjectURL(preview)\n })\n .catch(() => {\n if (items.get(id)?.preview === preview) items.set(id, { preview, failed: true })\n })\n }\n\n /** Insert one block per file after the block at `pos` and upload them; returns a position in the last one. */\n function insert(view: EditorView, files: File[], pos: number): number {\n const { schema } = view.state\n const info = blockAround(view.state.doc.resolve(Math.min(pos, view.state.doc.content.size)))\n const tr = view.state.tr\n const ids = files.map(() => createId())\n const blocks: PMNode[] = ids.map((id, index) =>\n schema.nodes.block!.create(\n { id },\n schema.nodes[target.type]!.create(target.propsOf?.(files[index]!) ?? null),\n ),\n )\n if (!info) {\n tr.insert(view.state.doc.content.size, blocks)\n } else if (\n info.content.type.name === 'paragraph' &&\n info.content.content.size === 0 &&\n !info.group\n ) {\n // an empty paragraph makes room for the images\n tr.replaceWith(info.pos, info.pos + info.node.nodeSize, blocks)\n } else {\n tr.insert(info.pos + info.node.nodeSize, blocks)\n }\n view.dispatch(tr.scrollIntoView())\n files.forEach((file, index) => start(view, ids[index]!, file, false))\n return findBlockById(view.state.doc, ids.at(-1)!)?.contentPos ?? pos\n }\n\n return { items, start, insert, setSrc }\n}\n\nexport type Uploads = ReturnType<typeof createUploads>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { blockElement, splitContainer } from "../core/prepare.js";
|
|
2
|
+
import { indentLines } from "../core/output.js";
|
|
3
|
+
// src/extensions/list-html.ts
|
|
4
|
+
var TASK = /^\s*\[( |x|X)\]\s/;
|
|
5
|
+
/**
|
|
6
|
+
* Lists from other apps nest a list inside an item (`<li>text<ul>…</ul></li>`),
|
|
7
|
+
* but a list item here is a block of text with child blocks. Rewrite them
|
|
8
|
+
* into blocks before parsing, so nesting survives. Task lists
|
|
9
|
+
* (`<li><input type="checkbox">`, or `[ ]` / `[x]` before the text as
|
|
10
|
+
* Markdown writes them) become to-do items.
|
|
11
|
+
*/
|
|
12
|
+
function prepareLists(root) {
|
|
13
|
+
const outer = Array.from(root.querySelectorAll("ul, ol")).filter((list) => !list.parentElement?.closest("ul, ol"));
|
|
14
|
+
for (const list of outer) list.replaceWith(...convertList(list));
|
|
15
|
+
}
|
|
16
|
+
function convertList(list) {
|
|
17
|
+
const doc = list.ownerDocument;
|
|
18
|
+
const numbered = list.tagName === "OL";
|
|
19
|
+
const blocks = [];
|
|
20
|
+
for (const item of Array.from(list.children)) {
|
|
21
|
+
if (item.tagName !== "LI") continue;
|
|
22
|
+
let type = numbered ? "numberedItem" : "bulletItem";
|
|
23
|
+
const data = {};
|
|
24
|
+
const checkbox = item.querySelector(":scope > input[type=\"checkbox\"], :scope > p > input[type=\"checkbox\"]");
|
|
25
|
+
if (checkbox) {
|
|
26
|
+
type = "checkItem";
|
|
27
|
+
data.checked = String(checkbox.checked || checkbox.hasAttribute("checked"));
|
|
28
|
+
checkbox.remove();
|
|
29
|
+
} else {
|
|
30
|
+
const text = firstText(item);
|
|
31
|
+
const match = text && TASK.exec(text.data);
|
|
32
|
+
if (text && match) {
|
|
33
|
+
type = "checkItem";
|
|
34
|
+
data.checked = String(match[1] !== " ");
|
|
35
|
+
text.data = text.data.slice(match[0].length);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const nested of Array.from(item.children)) if (nested.tagName === "UL" || nested.tagName === "OL") nested.replaceWith(...convertList(nested));
|
|
39
|
+
const { content, children } = splitContainer(item);
|
|
40
|
+
blocks.push(blockElement(doc, type, content, children, data));
|
|
41
|
+
}
|
|
42
|
+
return blocks;
|
|
43
|
+
}
|
|
44
|
+
function firstText(item) {
|
|
45
|
+
const walker = item.ownerDocument.createTreeWalker(item, NodeFilter.SHOW_TEXT);
|
|
46
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) if (node.textContent?.trim()) return node;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/** A list item in Markdown: the marker, the text, and the children indented under it. */
|
|
50
|
+
function listItemMarkdown(marker, { content, children }) {
|
|
51
|
+
const width = marker.startsWith("- [") ? 2 : marker.length;
|
|
52
|
+
const item = indentLines(content, marker, " ".repeat(width));
|
|
53
|
+
if (!children) return item;
|
|
54
|
+
return `${item}${/^([-*+]|\d+[.)])( |$)/.test(children) ? "\n" : "\n\n"}${indentLines(children, " ".repeat(width))}`;
|
|
55
|
+
}
|
|
56
|
+
export { listItemMarkdown, prepareLists };
|
|
57
|
+
|
|
58
|
+
//# sourceMappingURL=list-html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-html.js","names":[],"sources":["../../src/extensions/list-html.ts"],"sourcesContent":["import { indentLines, type BlockOutputContext } from '../core/output'\nimport { blockElement, splitContainer } from '../core/prepare'\n\nconst TASK = /^\\s*\\[( |x|X)\\]\\s/\n\n/**\n * Lists from other apps nest a list inside an item (`<li>text<ul>…</ul></li>`),\n * but a list item here is a block of text with child blocks. Rewrite them\n * into blocks before parsing, so nesting survives. Task lists\n * (`<li><input type=\"checkbox\">`, or `[ ]` / `[x]` before the text as\n * Markdown writes them) become to-do items.\n */\nexport function prepareLists(root: HTMLElement) {\n const outer = Array.from(root.querySelectorAll('ul, ol')).filter(\n (list) => !list.parentElement?.closest('ul, ol'),\n )\n for (const list of outer) list.replaceWith(...convertList(list))\n}\n\nfunction convertList(list: Element): HTMLElement[] {\n const doc = list.ownerDocument\n const numbered = list.tagName === 'OL'\n const blocks: HTMLElement[] = []\n for (const item of Array.from(list.children)) {\n if (item.tagName !== 'LI') continue\n let type = numbered ? 'numberedItem' : 'bulletItem'\n const data: Record<string, string> = {}\n const checkbox = item.querySelector(\n ':scope > input[type=\"checkbox\"], :scope > p > input[type=\"checkbox\"]',\n )\n if (checkbox) {\n type = 'checkItem'\n data.checked = String(\n (checkbox as HTMLInputElement).checked || checkbox.hasAttribute('checked'),\n )\n checkbox.remove()\n } else {\n const text = firstText(item)\n const match = text && TASK.exec(text.data)\n if (text && match) {\n type = 'checkItem'\n data.checked = String(match[1] !== ' ')\n text.data = text.data.slice(match[0].length)\n }\n }\n // the first paragraph is the item's text; what follows becomes its children\n for (const nested of Array.from(item.children))\n if (nested.tagName === 'UL' || nested.tagName === 'OL')\n nested.replaceWith(...convertList(nested))\n const { content, children } = splitContainer(item)\n blocks.push(blockElement(doc, type, content, children, data))\n }\n return blocks\n}\n\nfunction firstText(item: Element): Text | null {\n const walker = item.ownerDocument.createTreeWalker(item, NodeFilter.SHOW_TEXT)\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n if (node.textContent?.trim()) return node as Text\n }\n return null\n}\n\n/** A list item in Markdown: the marker, the text, and the children indented under it. */\nexport function listItemMarkdown(marker: string, { content, children }: BlockOutputContext) {\n // a checkbox marker indents its children like the bullet in front of it\n const width = marker.startsWith('- [') ? 2 : marker.length\n const item = indentLines(content, marker, ' '.repeat(width))\n if (!children) return item\n // a nested list follows right after; other blocks need a blank line, or they join the text\n const gap = /^([-*+]|\\d+[.)])( |$)/.test(children) ? '\\n' : '\\n\\n'\n return `${item}${gap}${indentLines(children, ' '.repeat(width))}`\n}\n"],"mappings":";;;AAGA,IAAM,OAAO;;;;;;;;AASb,SAAgB,aAAa,MAAmB;CAC9C,MAAM,QAAQ,MAAM,KAAK,KAAK,iBAAiB,QAAQ,CAAC,CAAC,CAAC,QACvD,SAAS,CAAC,KAAK,eAAe,QAAQ,QAAQ,CACjD;CACA,KAAK,MAAM,QAAQ,OAAO,KAAK,YAAY,GAAG,YAAY,IAAI,CAAC;AACjE;AAEA,SAAS,YAAY,MAA8B;CACjD,MAAM,MAAM,KAAK;CACjB,MAAM,WAAW,KAAK,YAAY;CAClC,MAAM,SAAwB,CAAC;CAC/B,KAAK,MAAM,QAAQ,MAAM,KAAK,KAAK,QAAQ,GAAG;EAC5C,IAAI,KAAK,YAAY,MAAM;EAC3B,IAAI,OAAO,WAAW,iBAAiB;EACvC,MAAM,OAA+B,CAAC;EACtC,MAAM,WAAW,KAAK,cACpB,0EACF;EACA,IAAI,UAAU;GACZ,OAAO;GACP,KAAK,UAAU,OACZ,SAA8B,WAAW,SAAS,aAAa,SAAS,CAC3E;GACA,SAAS,OAAO;EAClB,OAAO;GACL,MAAM,OAAO,UAAU,IAAI;GAC3B,MAAM,QAAQ,QAAQ,KAAK,KAAK,KAAK,IAAI;GACzC,IAAI,QAAQ,OAAO;IACjB,OAAO;IACP,KAAK,UAAU,OAAO,MAAM,OAAO,GAAG;IACtC,KAAK,OAAO,KAAK,KAAK,MAAM,MAAM,EAAE,CAAC,MAAM;GAC7C;EACF;EAEA,KAAK,MAAM,UAAU,MAAM,KAAK,KAAK,QAAQ,GAC3C,IAAI,OAAO,YAAY,QAAQ,OAAO,YAAY,MAChD,OAAO,YAAY,GAAG,YAAY,MAAM,CAAC;EAC7C,MAAM,EAAE,SAAS,aAAa,eAAe,IAAI;EACjD,OAAO,KAAK,aAAa,KAAK,MAAM,SAAS,UAAU,IAAI,CAAC;CAC9D;CACA,OAAO;AACT;AAEA,SAAS,UAAU,MAA4B;CAC7C,MAAM,SAAS,KAAK,cAAc,iBAAiB,MAAM,WAAW,SAAS;CAC7E,KAAK,IAAI,OAAO,OAAO,SAAS,GAAG,MAAM,OAAO,OAAO,SAAS,GAC9D,IAAI,KAAK,aAAa,KAAK,GAAG,OAAO;CAEvC,OAAO;AACT;;AAGA,SAAgB,iBAAiB,QAAgB,EAAE,SAAS,YAAgC;CAE1F,MAAM,QAAQ,OAAO,WAAW,KAAK,IAAI,IAAI,OAAO;CACpD,MAAM,OAAO,YAAY,SAAS,QAAQ,IAAI,OAAO,KAAK,CAAC;CAC3D,IAAI,CAAC,UAAU,OAAO;CAGtB,OAAO,GAAG,OADE,wBAAwB,KAAK,QAAQ,IAAI,OAAO,SACrC,YAAY,UAAU,IAAI,OAAO,KAAK,CAAC;AAChE"}
|