@dou-ui/editor 0.0.0-stage → 0.1.0-alpha.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/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":"block-handle.js","names":["_sfc_main"],"sources":["../../src/components/block-handle.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n ArrowDownIcon,\n ArrowUpIcon,\n CheckIcon,\n CopyIcon,\n GripVerticalIcon,\n PlusIcon,\n TrashIcon,\n} from '@dou-ui/icons'\nimport { DouDropdown, DouDropdownItem, DouDropdownSub } from 'dou-ui'\nimport { TextSelection } from 'prosemirror-state'\nimport { computed, inject, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'\nimport { isCurrent } from '../catalog'\nimport { labelOf, type BlockOption } from '../core/extension'\nimport {\n blockSelectionKey,\n blockSelectionRange,\n getBlockSelection,\n selectBlocks,\n} from '../core/block-selection'\nimport {\n blockAround,\n blocksIn,\n findBlockById,\n siblingRangeBetween,\n type SiblingRange,\n} from '../core/blocks'\nimport {\n duplicateBlocks,\n insertBlockAfter,\n swapBlocks,\n moveRangeTo,\n deleteBlocks,\n setBlockType,\n} from '../core/commands'\nimport { blockFromNode } from '../core/model'\nimport { slashKey } from '../core/slash'\nimport { blockElementOf, contentAt, firstLine, scrollParent } from '../dom'\nimport { editorKey } from '../editor'\nimport { useMenuEscape } from './menu-escape'\n\nconst { view, state, focused, messages, root, dragging, kit } = inject(editorKey)!\n\n/**\n * Touch screens have no hover: there the handle shows only the grip, next\n * to the block with the cursor (or the selected blocks) while the editor\n * has focus. A tap on it opens the menu, a press and drag moves the block.\n */\nconst touch = shallowRef(false)\nlet media: MediaQueryList | null = null\nconst onMedia = () => (touch.value = !!media?.matches)\n\n/** The block the handle is next to, by id. */\nconst hovered = shallowRef<string | null>(null)\nconst position = shallowRef<{ top: number; left: number; height: number } | null>(null)\nconst menuOpen = shallowRef(false)\nuseMenuEscape(menuOpen, () => (menuOpen.value = false))\n\nconst drop = shallowRef<{ pos: number; top: number; left: number; width: number } | null>(null)\n// typing hides the handle until the pointer moves again\nconst resting = shallowRef(false)\n\nconst visible = computed(() => {\n if (!position.value) return false\n if (menuOpen.value || dragging.value) return true\n if (touch.value) return focused.value || (!!state.value && !!getBlockSelection(state.value))\n return !resting.value\n})\n\nfunction place(content: HTMLElement) {\n const block = blockElementOf(content)\n const id = block?.dataset.blockId\n const host = root.value\n if (!id || !host) return\n const box = host.getBoundingClientRect()\n const line = firstLine(content)\n hovered.value = id\n position.value = {\n top: line.top - box.top,\n left: content.getBoundingClientRect().left - box.left,\n height: line.height,\n }\n}\n\nlet frame = 0\nlet lastY = 0\nlet lastX = 0\n\nfunction track(event: PointerEvent) {\n // a finger only moves the handle by moving the cursor; a drag reports itself\n if (event.pointerType === 'touch') return\n lastY = event.clientY\n lastX = event.clientX\n resting.value = false\n if (menuOpen.value || frame) return\n frame = requestAnimationFrame(() => {\n frame = 0\n if (dragging.value) return updateDrop()\n const current = view.value\n if (!current || menuOpen.value) return\n const content = contentAt(current, lastY)\n if (content) place(content)\n })\n}\n\nfunction leave(event: PointerEvent) {\n if (menuOpen.value || dragging.value || event.pointerType === 'touch') return\n position.value = null\n hovered.value = null\n}\n\n/** On touch screens: put the handle next to the block with the cursor. */\nfunction follow() {\n const current = view.value\n if (!current || dragging.value) return\n const selected = blockSelectionRange(current.state)\n const info = selected ? blocksIn(selected)[0] : blockAround(current.state.selection.$from)\n const element = info\n ? (current.nodeDOM(info.pos) as HTMLElement | null)?.querySelector<HTMLElement>(\n ':scope > .dou-editor__content',\n )\n : null\n if (element) place(element)\n else {\n position.value = null\n hovered.value = null\n }\n}\n\nlet followFrame = 0\nfunction scheduleFollow() {\n if (!touch.value || followFrame) return\n followFrame = requestAnimationFrame(() => {\n followFrame = 0\n if (touch.value && !menuOpen.value) follow()\n })\n}\n\nwatch([state, focused, touch], scheduleFollow)\n\nonMounted(() => {\n media = window.matchMedia('(pointer: coarse)')\n onMedia()\n media.addEventListener('change', onMedia)\n window.addEventListener('resize', scheduleFollow)\n root.value?.addEventListener('pointermove', track)\n root.value?.addEventListener('pointerleave', leave)\n})\nonBeforeUnmount(() => {\n media?.removeEventListener('change', onMedia)\n window.removeEventListener('resize', scheduleFollow)\n if (followFrame) cancelAnimationFrame(followFrame)\n root.value?.removeEventListener('pointermove', track)\n root.value?.removeEventListener('pointerleave', leave)\n if (frame) cancelAnimationFrame(frame)\n stopDrag()\n})\n\n// follow edits: keep the handle on its block, or hide it while typing\nwatch(state, (next, previous) => {\n if (touch.value || !next || !previous || next.doc === previous.doc || dragging.value) return\n if (!menuOpen.value) resting.value = true\n requestAnimationFrame(() => {\n const current = view.value\n if (!current || !hovered.value) return\n const element = current.dom.querySelector<HTMLElement>(\n `[data-block-id=\"${CSS.escape(hovered.value)}\"] > .dou-editor__content`,\n )\n if (element) place(element)\n else position.value = null\n })\n})\n\nconst target = computed(() => {\n const doc = state.value?.doc\n return doc && hovered.value ? findBlockById(doc, hovered.value) : null\n})\n\n/** The hovered block, or every selected block when it is one of them. */\nfunction rangeForHovered(): SiblingRange | null {\n const current = view.value\n const info = target.value\n if (!current || !info) return null\n const selected = blockSelectionRange(current.state)\n if (selected && info.pos >= selected.start && info.pos < selected.end) return selected\n return siblingRangeBetween(current.state.doc, info.pos + 1, info.pos + 1)\n}\n\n/** Select the blocks the menu or a drag acts on. */\nfunction selectTarget(): SiblingRange | null {\n const current = view.value\n const range = rangeForHovered()\n if (!current || !range) return null\n const blocks = blocksIn(range)\n selectBlocks(blocks[0]!.node.attrs.id, blocks.at(-1)!.node.attrs.id)(\n current.state,\n current.dispatch,\n )\n return blockSelectionRange(current.state)\n}\n\n/* the + button */\n\nfunction add(event: MouseEvent) {\n const current = view.value\n const info = target.value\n if (!current || !info) return\n const empty = info.content.isTextblock && info.content.content.size === 0\n if (!empty || info.content.type.name !== 'paragraph') {\n insertBlockAfter(info.pos)(current.state, current.dispatch)\n // Alt+click adds the block above instead\n if (event.altKey) swapBlocks(-1)(current.state, current.dispatch)\n } else {\n const tr = current.state.tr.setSelection(\n TextSelection.create(current.state.doc, info.contentPos + 1),\n )\n current.dispatch(tr)\n }\n const from = current.state.selection.from\n current.dispatch(current.state.tr.insertText('/').setMeta(slashKey, { from, query: '' }))\n current.focus()\n}\n\n/* the block menu */\n\nconst turnOptions = kit.blockOptions.filter(\n (option) => !!kit.schema.nodes[option.type]?.isTextblock,\n)\nconst current = computed(() => {\n const info = target.value\n return info ? blockFromNode(info.node) : null\n})\n\nfunction after(action: () => void) {\n action()\n // the menu gives focus back to the handle; the editor takes it after\n requestAnimationFrame(() => view.value?.focus())\n}\n\nfunction keep(run: (current: NonNullable<typeof view.value>) => void) {\n after(() => {\n const instance = view.value\n if (!instance) return\n const selection = getBlockSelection(instance.state)\n run(instance)\n if (selection) selectBlocks(selection.anchor, selection.head)(instance.state, instance.dispatch)\n })\n}\n\nconst turnInto = (option: BlockOption) =>\n keep((instance) => setBlockType(option.type, option.props)(instance.state, instance.dispatch))\nconst duplicate = () => keep((instance) => duplicateBlocks(instance.state, instance.dispatch))\nconst move = (direction: -1 | 1) =>\n keep((instance) => swapBlocks(direction)(instance.state, instance.dispatch))\n\nfunction remove() {\n after(() => {\n const instance = view.value\n const range = instance && blockSelectionRange(instance.state)\n if (!instance || !range) return\n const tr = deleteBlocks(instance.state.tr, blocksIn(range)).setMeta(blockSelectionKey, null)\n instance.dispatch(tr.scrollIntoView())\n position.value = null\n })\n}\n\nconst canMove = (direction: -1 | 1) => {\n const instance = view.value\n return !!instance && !!state.value && swapBlocks(direction)(instance.state)\n}\n\n/* dragging */\n\nlet start: { x: number; y: number; pointer: number; touch: boolean } | null = null\nlet range: SiblingRange | null = null\nlet scroller: HTMLElement | null = null\nlet scrollFrame = 0\n/** A copy of the dragged blocks that follows the pointer. */\nlet ghost: HTMLElement | null = null\n\nfunction showGhost(moving: SiblingRange) {\n const instance = view.value\n const host = root.value\n if (!instance || !host) return\n ghost = document.createElement('div')\n ghost.className = 'dou-editor__surface dou-editor__ghost'\n ghost.setAttribute('aria-hidden', 'true')\n let width = 0\n for (const block of blocksIn(moving)) {\n const element = instance.nodeDOM(block.pos) as HTMLElement | null\n if (!element) continue\n width = Math.max(width, element.getBoundingClientRect().width)\n const copy = element.cloneNode(true) as HTMLElement\n copy.classList.remove('is-selected')\n for (const node of [copy, ...copy.querySelectorAll('[id], .is-selected')]) {\n node.removeAttribute('id')\n node.classList.remove('is-selected')\n }\n ghost.append(copy)\n }\n ghost.style.width = `${Math.min(width, 360)}px`\n host.append(ghost)\n moveGhost()\n}\n\nfunction moveGhost() {\n if (!ghost) return\n // above a finger, which would hide it; below and right of a mouse pointer\n const y = start?.touch ? lastY - ghost.offsetHeight - 24 : lastY + 12\n ghost.style.transform = `translate(${lastX + 12}px, ${y}px)`\n}\n\nfunction onGripDown(event: PointerEvent) {\n if (event.button !== 0) return\n start = {\n x: event.clientX,\n y: event.clientY,\n pointer: event.pointerId,\n touch: event.pointerType === 'touch',\n }\n ;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)\n}\n\nfunction onGripMove(event: PointerEvent) {\n if (!start || event.pointerId !== start.pointer) return\n lastX = event.clientX\n lastY = event.clientY\n if (!dragging.value) {\n // a finger wobbles more than a mouse while tapping\n const slop = start.touch ? 8 : 4\n if (Math.hypot(event.clientX - start.x, event.clientY - start.y) < slop) return\n range = selectTarget()\n if (!range) return stopDrag()\n dragging.value = true\n scroller = root.value && scrollParent(root.value)\n window.addEventListener('keydown', onDragKey, true)\n showGhost(range)\n autoScroll()\n }\n moveGhost()\n updateDrop()\n}\n\nfunction onGripUp(event: PointerEvent) {\n if (!start || event.pointerId !== start.pointer) return\n if (dragging.value) finishDrag()\n else if (menuOpen.value) menuOpen.value = false\n else {\n selectTarget()\n menuOpen.value = true\n }\n start = null\n}\n\nfunction onDragKey(event: KeyboardEvent) {\n if (event.key !== 'Escape') return\n event.preventDefault()\n event.stopPropagation()\n stopDrag()\n}\n\nfunction updateDrop() {\n const instance = view.value\n const host = root.value\n if (!instance || !host || !range) return\n const content = contentAt(instance, lastY)\n const id = content && blockElementOf(content)?.dataset.blockId\n const info = id ? findBlockById(instance.state.doc, id) : null\n if (!content || !info) return (drop.value = null)\n const box = host.getBoundingClientRect()\n const rect = content.getBoundingClientRect()\n const below = lastY > rect.top + rect.height / 2\n let pos: number\n let y: number\n let left = rect.left\n if (!below) {\n pos = info.pos\n y = rect.top\n } else if (info.group) {\n // the lower half of a block with children: in front of its first child\n pos = info.contentPos + info.content.nodeSize + 1\n y = rect.bottom\n const child = blockElementOf(content)?.querySelector<HTMLElement>(\n ':scope > .dou-editor__group > .dou-editor__block > .dou-editor__content',\n )\n left = child?.getBoundingClientRect().left ?? rect.left\n } else {\n pos = info.pos + info.node.nodeSize\n y = blockElementOf(content)!.getBoundingClientRect().bottom\n }\n if (!moveRangeTo(range, pos)(instance.state)) return (drop.value = null)\n drop.value = { pos, top: y - box.top, left: left - box.left, width: box.right - left }\n}\n\nfunction autoScroll() {\n scrollFrame = requestAnimationFrame(() => {\n if (!dragging.value) return\n const edge = 48\n const top = scroller ? scroller.getBoundingClientRect().top : 0\n const bottom = scroller ? scroller.getBoundingClientRect().bottom : window.innerHeight\n let speed = 0\n if (lastY < top + edge) speed = -Math.ceil(((top + edge - lastY) / edge) * 16)\n else if (lastY > bottom - edge) speed = Math.ceil(((lastY - bottom + edge) / edge) * 16)\n if (speed) {\n if (scroller) scroller.scrollTop += speed\n else window.scrollBy(0, speed)\n updateDrop()\n }\n autoScroll()\n })\n}\n\nfunction finishDrag() {\n const instance = view.value\n const spot = drop.value\n const moving = range\n stopDrag()\n if (!instance || !spot || !moving) return\n const ids = blocksIn(moving).map((block) => block.node.attrs.id as string)\n if (!moveRangeTo(moving, spot.pos)(instance.state, instance.dispatch)) return\n selectBlocks(ids[0]!, ids.at(-1)!)(instance.state, instance.dispatch)\n instance.focus()\n}\n\nfunction stopDrag() {\n dragging.value = false\n drop.value = null\n range = null\n start = null\n ghost?.remove()\n ghost = null\n if (scrollFrame) cancelAnimationFrame(scrollFrame)\n scrollFrame = 0\n window.removeEventListener('keydown', onDragKey, true)\n}\n\nwatch(menuOpen, (value) => {\n if (value) return\n if (touch.value) scheduleFollow()\n else if (!root.value?.matches(':hover')) position.value = null\n})\n</script>\n\n<template>\n <div\n v-show=\"visible\"\n class=\"dou-editor__handle\"\n :style=\"\n position\n ? {\n top: `${position.top}px`,\n left: `${position.left}px`,\n '--dou-editor-line': `${position.height}px`,\n }\n : undefined\n \"\n @mousedown.prevent\n >\n <button\n type=\"button\"\n tabindex=\"-1\"\n class=\"dou-editor__handle-button dou-editor__add\"\n :aria-label=\"messages.addBlock\"\n :title=\"messages.addBlock\"\n @click=\"add\"\n >\n <PlusIcon />\n </button>\n <DouDropdown\n v-model:open=\"menuOpen\"\n trigger=\"manual\"\n placement=\"bottom-start\"\n class=\"dou-editor-menu\"\n >\n <button\n type=\"button\"\n tabindex=\"-1\"\n class=\"dou-editor__handle-button dou-editor__grip\"\n :aria-label=\"messages.dragHandle\"\n :title=\"messages.dragHandle\"\n @pointerdown=\"onGripDown\"\n @pointermove=\"onGripMove\"\n @pointerup=\"onGripUp\"\n @pointercancel=\"stopDrag\"\n >\n <GripVerticalIcon />\n </button>\n <template #menu>\n <DouDropdownSub :title=\"messages.turnInto\">\n <DouDropdownItem\n v-for=\"option in turnOptions\"\n :key=\"option.key\"\n @click=\"turnInto(option)\"\n >\n <template #prefix><component :is=\"option.icon\" /></template>\n {{ labelOf(option.label, messages) }}\n <template v-if=\"current && isCurrent(option, current.type, current.props)\" #suffix>\n <CheckIcon />\n </template>\n </DouDropdownItem>\n </DouDropdownSub>\n <template v-if=\"current\">\n <component\n :is=\"entry\"\n v-for=\"(entry, index) in kit.blockMenu\"\n :key=\"index\"\n :block=\"current\"\n />\n </template>\n <DouDropdownItem :shortcut=\"touch ? undefined : 'mod+d'\" @click=\"duplicate\">\n <template #prefix><CopyIcon /></template>\n {{ messages.duplicate }}\n </DouDropdownItem>\n <DouDropdownItem\n :disabled=\"!canMove(-1)\"\n :shortcut=\"touch ? undefined : 'mod+shift+up'\"\n @click=\"move(-1)\"\n >\n <template #prefix><ArrowUpIcon /></template>\n {{ messages.moveUp }}\n </DouDropdownItem>\n <DouDropdownItem\n :disabled=\"!canMove(1)\"\n :shortcut=\"touch ? undefined : 'mod+shift+down'\"\n @click=\"move(1)\"\n >\n <template #prefix><ArrowDownIcon /></template>\n {{ messages.moveDown }}\n </DouDropdownItem>\n <DouDropdownItem\n divided\n tone=\"danger\"\n :shortcut=\"touch ? undefined : 'delete'\"\n @click=\"remove\"\n >\n <template #prefix><TrashIcon /></template>\n {{ messages.delete }}\n </DouDropdownItem>\n </template>\n </DouDropdown>\n </div>\n <div\n v-if=\"drop\"\n class=\"dou-editor__drop\"\n :style=\"{ top: `${drop.top}px`, left: `${drop.left}px`, width: `${drop.width}px` }\"\n />\n</template>\n"],"mappings":""}
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { blockFromNode } from "../core/model.js";
|
|
2
|
+
import { labelOf } from "../core/extension.js";
|
|
3
|
+
import { isCurrent } from "../catalog.js";
|
|
4
|
+
import { blockAround, blocksIn, findBlockById, siblingRangeBetween } from "../core/blocks.js";
|
|
5
|
+
import { deleteBlocks, duplicateBlocks, insertBlockAfter, moveRangeTo, setBlockType, swapBlocks } from "../core/commands.js";
|
|
6
|
+
import { blockSelectionKey, blockSelectionRange, getBlockSelection, selectBlocks } from "../core/block-selection.js";
|
|
7
|
+
import { slashKey } from "../core/slash.js";
|
|
8
|
+
import { blockElementOf, contentAt, firstLine, scrollParent } from "../dom.js";
|
|
9
|
+
import { editorKey } from "../editor.js";
|
|
10
|
+
import { useMenuEscape } from "./menu-escape.js";
|
|
11
|
+
import { DouDropdown, DouDropdownItem, DouDropdownSub } from "dou-ui";
|
|
12
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, inject, normalizeStyle, onBeforeUnmount, onMounted, openBlock, renderList, resolveDynamicComponent, shallowRef, toDisplayString, unref, vShow, watch, withCtx, withDirectives, withModifiers } from "vue";
|
|
13
|
+
import { TextSelection } from "prosemirror-state";
|
|
14
|
+
import { ArrowDownIcon, ArrowUpIcon, CheckIcon, CopyIcon, GripVerticalIcon, PlusIcon, TrashIcon } from "@dou-ui/icons";
|
|
15
|
+
// src/components/block-handle.vue?vue&type=script&setup=true&lang.ts
|
|
16
|
+
var _hoisted_1 = ["aria-label", "title"];
|
|
17
|
+
var _hoisted_2 = ["aria-label", "title"];
|
|
18
|
+
var block_handle_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
|
|
19
|
+
__name: "block-handle",
|
|
20
|
+
setup(__props) {
|
|
21
|
+
const { view, state, focused, messages, root, dragging, kit } = inject(editorKey);
|
|
22
|
+
/**
|
|
23
|
+
* Touch screens have no hover: there the handle shows only the grip, next
|
|
24
|
+
* to the block with the cursor (or the selected blocks) while the editor
|
|
25
|
+
* has focus. A tap on it opens the menu, a press and drag moves the block.
|
|
26
|
+
*/
|
|
27
|
+
const touch = shallowRef(false);
|
|
28
|
+
let media = null;
|
|
29
|
+
const onMedia = () => touch.value = !!media?.matches;
|
|
30
|
+
/** The block the handle is next to, by id. */
|
|
31
|
+
const hovered = shallowRef(null);
|
|
32
|
+
const position = shallowRef(null);
|
|
33
|
+
const menuOpen = shallowRef(false);
|
|
34
|
+
useMenuEscape(menuOpen, () => menuOpen.value = false);
|
|
35
|
+
const drop = shallowRef(null);
|
|
36
|
+
const resting = shallowRef(false);
|
|
37
|
+
const visible = computed(() => {
|
|
38
|
+
if (!position.value) return false;
|
|
39
|
+
if (menuOpen.value || dragging.value) return true;
|
|
40
|
+
if (touch.value) return focused.value || !!state.value && !!getBlockSelection(state.value);
|
|
41
|
+
return !resting.value;
|
|
42
|
+
});
|
|
43
|
+
function place(content) {
|
|
44
|
+
const id = blockElementOf(content)?.dataset.blockId;
|
|
45
|
+
const host = root.value;
|
|
46
|
+
if (!id || !host) return;
|
|
47
|
+
const box = host.getBoundingClientRect();
|
|
48
|
+
const line = firstLine(content);
|
|
49
|
+
hovered.value = id;
|
|
50
|
+
position.value = {
|
|
51
|
+
top: line.top - box.top,
|
|
52
|
+
left: content.getBoundingClientRect().left - box.left,
|
|
53
|
+
height: line.height
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
let frame = 0;
|
|
57
|
+
let lastY = 0;
|
|
58
|
+
let lastX = 0;
|
|
59
|
+
function track(event) {
|
|
60
|
+
if (event.pointerType === "touch") return;
|
|
61
|
+
lastY = event.clientY;
|
|
62
|
+
lastX = event.clientX;
|
|
63
|
+
resting.value = false;
|
|
64
|
+
if (menuOpen.value || frame) return;
|
|
65
|
+
frame = requestAnimationFrame(() => {
|
|
66
|
+
frame = 0;
|
|
67
|
+
if (dragging.value) return updateDrop();
|
|
68
|
+
const current = view.value;
|
|
69
|
+
if (!current || menuOpen.value) return;
|
|
70
|
+
const content = contentAt(current, lastY);
|
|
71
|
+
if (content) place(content);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function leave(event) {
|
|
75
|
+
if (menuOpen.value || dragging.value || event.pointerType === "touch") return;
|
|
76
|
+
position.value = null;
|
|
77
|
+
hovered.value = null;
|
|
78
|
+
}
|
|
79
|
+
/** On touch screens: put the handle next to the block with the cursor. */
|
|
80
|
+
function follow() {
|
|
81
|
+
const current = view.value;
|
|
82
|
+
if (!current || dragging.value) return;
|
|
83
|
+
const selected = blockSelectionRange(current.state);
|
|
84
|
+
const info = selected ? blocksIn(selected)[0] : blockAround(current.state.selection.$from);
|
|
85
|
+
const element = info ? current.nodeDOM(info.pos)?.querySelector(":scope > .dou-editor__content") : null;
|
|
86
|
+
if (element) place(element);
|
|
87
|
+
else {
|
|
88
|
+
position.value = null;
|
|
89
|
+
hovered.value = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
let followFrame = 0;
|
|
93
|
+
function scheduleFollow() {
|
|
94
|
+
if (!touch.value || followFrame) return;
|
|
95
|
+
followFrame = requestAnimationFrame(() => {
|
|
96
|
+
followFrame = 0;
|
|
97
|
+
if (touch.value && !menuOpen.value) follow();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
watch([
|
|
101
|
+
state,
|
|
102
|
+
focused,
|
|
103
|
+
touch
|
|
104
|
+
], scheduleFollow);
|
|
105
|
+
onMounted(() => {
|
|
106
|
+
media = window.matchMedia("(pointer: coarse)");
|
|
107
|
+
onMedia();
|
|
108
|
+
media.addEventListener("change", onMedia);
|
|
109
|
+
window.addEventListener("resize", scheduleFollow);
|
|
110
|
+
root.value?.addEventListener("pointermove", track);
|
|
111
|
+
root.value?.addEventListener("pointerleave", leave);
|
|
112
|
+
});
|
|
113
|
+
onBeforeUnmount(() => {
|
|
114
|
+
media?.removeEventListener("change", onMedia);
|
|
115
|
+
window.removeEventListener("resize", scheduleFollow);
|
|
116
|
+
if (followFrame) cancelAnimationFrame(followFrame);
|
|
117
|
+
root.value?.removeEventListener("pointermove", track);
|
|
118
|
+
root.value?.removeEventListener("pointerleave", leave);
|
|
119
|
+
if (frame) cancelAnimationFrame(frame);
|
|
120
|
+
stopDrag();
|
|
121
|
+
});
|
|
122
|
+
watch(state, (next, previous) => {
|
|
123
|
+
if (touch.value || !next || !previous || next.doc === previous.doc || dragging.value) return;
|
|
124
|
+
if (!menuOpen.value) resting.value = true;
|
|
125
|
+
requestAnimationFrame(() => {
|
|
126
|
+
const current = view.value;
|
|
127
|
+
if (!current || !hovered.value) return;
|
|
128
|
+
const element = current.dom.querySelector(`[data-block-id="${CSS.escape(hovered.value)}"] > .dou-editor__content`);
|
|
129
|
+
if (element) place(element);
|
|
130
|
+
else position.value = null;
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
const target = computed(() => {
|
|
134
|
+
const doc = state.value?.doc;
|
|
135
|
+
return doc && hovered.value ? findBlockById(doc, hovered.value) : null;
|
|
136
|
+
});
|
|
137
|
+
/** The hovered block, or every selected block when it is one of them. */
|
|
138
|
+
function rangeForHovered() {
|
|
139
|
+
const current = view.value;
|
|
140
|
+
const info = target.value;
|
|
141
|
+
if (!current || !info) return null;
|
|
142
|
+
const selected = blockSelectionRange(current.state);
|
|
143
|
+
if (selected && info.pos >= selected.start && info.pos < selected.end) return selected;
|
|
144
|
+
return siblingRangeBetween(current.state.doc, info.pos + 1, info.pos + 1);
|
|
145
|
+
}
|
|
146
|
+
/** Select the blocks the menu or a drag acts on. */
|
|
147
|
+
function selectTarget() {
|
|
148
|
+
const current = view.value;
|
|
149
|
+
const range = rangeForHovered();
|
|
150
|
+
if (!current || !range) return null;
|
|
151
|
+
const blocks = blocksIn(range);
|
|
152
|
+
selectBlocks(blocks[0].node.attrs.id, blocks.at(-1).node.attrs.id)(current.state, current.dispatch);
|
|
153
|
+
return blockSelectionRange(current.state);
|
|
154
|
+
}
|
|
155
|
+
function add(event) {
|
|
156
|
+
const current = view.value;
|
|
157
|
+
const info = target.value;
|
|
158
|
+
if (!current || !info) return;
|
|
159
|
+
if (!(info.content.isTextblock && info.content.content.size === 0) || info.content.type.name !== "paragraph") {
|
|
160
|
+
insertBlockAfter(info.pos)(current.state, current.dispatch);
|
|
161
|
+
if (event.altKey) swapBlocks(-1)(current.state, current.dispatch);
|
|
162
|
+
} else {
|
|
163
|
+
const tr = current.state.tr.setSelection(TextSelection.create(current.state.doc, info.contentPos + 1));
|
|
164
|
+
current.dispatch(tr);
|
|
165
|
+
}
|
|
166
|
+
const from = current.state.selection.from;
|
|
167
|
+
current.dispatch(current.state.tr.insertText("/").setMeta(slashKey, {
|
|
168
|
+
from,
|
|
169
|
+
query: ""
|
|
170
|
+
}));
|
|
171
|
+
current.focus();
|
|
172
|
+
}
|
|
173
|
+
const turnOptions = kit.blockOptions.filter((option) => !!kit.schema.nodes[option.type]?.isTextblock);
|
|
174
|
+
const current = computed(() => {
|
|
175
|
+
const info = target.value;
|
|
176
|
+
return info ? blockFromNode(info.node) : null;
|
|
177
|
+
});
|
|
178
|
+
function after(action) {
|
|
179
|
+
action();
|
|
180
|
+
requestAnimationFrame(() => view.value?.focus());
|
|
181
|
+
}
|
|
182
|
+
function keep(run) {
|
|
183
|
+
after(() => {
|
|
184
|
+
const instance = view.value;
|
|
185
|
+
if (!instance) return;
|
|
186
|
+
const selection = getBlockSelection(instance.state);
|
|
187
|
+
run(instance);
|
|
188
|
+
if (selection) selectBlocks(selection.anchor, selection.head)(instance.state, instance.dispatch);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const turnInto = (option) => keep((instance) => setBlockType(option.type, option.props)(instance.state, instance.dispatch));
|
|
192
|
+
const duplicate = () => keep((instance) => duplicateBlocks(instance.state, instance.dispatch));
|
|
193
|
+
const move = (direction) => keep((instance) => swapBlocks(direction)(instance.state, instance.dispatch));
|
|
194
|
+
function remove() {
|
|
195
|
+
after(() => {
|
|
196
|
+
const instance = view.value;
|
|
197
|
+
const range = instance && blockSelectionRange(instance.state);
|
|
198
|
+
if (!instance || !range) return;
|
|
199
|
+
const tr = deleteBlocks(instance.state.tr, blocksIn(range)).setMeta(blockSelectionKey, null);
|
|
200
|
+
instance.dispatch(tr.scrollIntoView());
|
|
201
|
+
position.value = null;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const canMove = (direction) => {
|
|
205
|
+
const instance = view.value;
|
|
206
|
+
return !!instance && !!state.value && swapBlocks(direction)(instance.state);
|
|
207
|
+
};
|
|
208
|
+
let start = null;
|
|
209
|
+
let range = null;
|
|
210
|
+
let scroller = null;
|
|
211
|
+
let scrollFrame = 0;
|
|
212
|
+
/** A copy of the dragged blocks that follows the pointer. */
|
|
213
|
+
let ghost = null;
|
|
214
|
+
function showGhost(moving) {
|
|
215
|
+
const instance = view.value;
|
|
216
|
+
const host = root.value;
|
|
217
|
+
if (!instance || !host) return;
|
|
218
|
+
ghost = document.createElement("div");
|
|
219
|
+
ghost.className = "dou-editor__surface dou-editor__ghost";
|
|
220
|
+
ghost.setAttribute("aria-hidden", "true");
|
|
221
|
+
let width = 0;
|
|
222
|
+
for (const block of blocksIn(moving)) {
|
|
223
|
+
const element = instance.nodeDOM(block.pos);
|
|
224
|
+
if (!element) continue;
|
|
225
|
+
width = Math.max(width, element.getBoundingClientRect().width);
|
|
226
|
+
const copy = element.cloneNode(true);
|
|
227
|
+
copy.classList.remove("is-selected");
|
|
228
|
+
for (const node of [copy, ...copy.querySelectorAll("[id], .is-selected")]) {
|
|
229
|
+
node.removeAttribute("id");
|
|
230
|
+
node.classList.remove("is-selected");
|
|
231
|
+
}
|
|
232
|
+
ghost.append(copy);
|
|
233
|
+
}
|
|
234
|
+
ghost.style.width = `${Math.min(width, 360)}px`;
|
|
235
|
+
host.append(ghost);
|
|
236
|
+
moveGhost();
|
|
237
|
+
}
|
|
238
|
+
function moveGhost() {
|
|
239
|
+
if (!ghost) return;
|
|
240
|
+
const y = start?.touch ? lastY - ghost.offsetHeight - 24 : lastY + 12;
|
|
241
|
+
ghost.style.transform = `translate(${lastX + 12}px, ${y}px)`;
|
|
242
|
+
}
|
|
243
|
+
function onGripDown(event) {
|
|
244
|
+
if (event.button !== 0) return;
|
|
245
|
+
start = {
|
|
246
|
+
x: event.clientX,
|
|
247
|
+
y: event.clientY,
|
|
248
|
+
pointer: event.pointerId,
|
|
249
|
+
touch: event.pointerType === "touch"
|
|
250
|
+
};
|
|
251
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
252
|
+
}
|
|
253
|
+
function onGripMove(event) {
|
|
254
|
+
if (!start || event.pointerId !== start.pointer) return;
|
|
255
|
+
lastX = event.clientX;
|
|
256
|
+
lastY = event.clientY;
|
|
257
|
+
if (!dragging.value) {
|
|
258
|
+
const slop = start.touch ? 8 : 4;
|
|
259
|
+
if (Math.hypot(event.clientX - start.x, event.clientY - start.y) < slop) return;
|
|
260
|
+
range = selectTarget();
|
|
261
|
+
if (!range) return stopDrag();
|
|
262
|
+
dragging.value = true;
|
|
263
|
+
scroller = root.value && scrollParent(root.value);
|
|
264
|
+
window.addEventListener("keydown", onDragKey, true);
|
|
265
|
+
showGhost(range);
|
|
266
|
+
autoScroll();
|
|
267
|
+
}
|
|
268
|
+
moveGhost();
|
|
269
|
+
updateDrop();
|
|
270
|
+
}
|
|
271
|
+
function onGripUp(event) {
|
|
272
|
+
if (!start || event.pointerId !== start.pointer) return;
|
|
273
|
+
if (dragging.value) finishDrag();
|
|
274
|
+
else if (menuOpen.value) menuOpen.value = false;
|
|
275
|
+
else {
|
|
276
|
+
selectTarget();
|
|
277
|
+
menuOpen.value = true;
|
|
278
|
+
}
|
|
279
|
+
start = null;
|
|
280
|
+
}
|
|
281
|
+
function onDragKey(event) {
|
|
282
|
+
if (event.key !== "Escape") return;
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
event.stopPropagation();
|
|
285
|
+
stopDrag();
|
|
286
|
+
}
|
|
287
|
+
function updateDrop() {
|
|
288
|
+
const instance = view.value;
|
|
289
|
+
const host = root.value;
|
|
290
|
+
if (!instance || !host || !range) return;
|
|
291
|
+
const content = contentAt(instance, lastY);
|
|
292
|
+
const id = content && blockElementOf(content)?.dataset.blockId;
|
|
293
|
+
const info = id ? findBlockById(instance.state.doc, id) : null;
|
|
294
|
+
if (!content || !info) return drop.value = null;
|
|
295
|
+
const box = host.getBoundingClientRect();
|
|
296
|
+
const rect = content.getBoundingClientRect();
|
|
297
|
+
const below = lastY > rect.top + rect.height / 2;
|
|
298
|
+
let pos;
|
|
299
|
+
let y;
|
|
300
|
+
let left = rect.left;
|
|
301
|
+
if (!below) {
|
|
302
|
+
pos = info.pos;
|
|
303
|
+
y = rect.top;
|
|
304
|
+
} else if (info.group) {
|
|
305
|
+
pos = info.contentPos + info.content.nodeSize + 1;
|
|
306
|
+
y = rect.bottom;
|
|
307
|
+
left = (blockElementOf(content)?.querySelector(":scope > .dou-editor__group > .dou-editor__block > .dou-editor__content"))?.getBoundingClientRect().left ?? rect.left;
|
|
308
|
+
} else {
|
|
309
|
+
pos = info.pos + info.node.nodeSize;
|
|
310
|
+
y = blockElementOf(content).getBoundingClientRect().bottom;
|
|
311
|
+
}
|
|
312
|
+
if (!moveRangeTo(range, pos)(instance.state)) return drop.value = null;
|
|
313
|
+
drop.value = {
|
|
314
|
+
pos,
|
|
315
|
+
top: y - box.top,
|
|
316
|
+
left: left - box.left,
|
|
317
|
+
width: box.right - left
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function autoScroll() {
|
|
321
|
+
scrollFrame = requestAnimationFrame(() => {
|
|
322
|
+
if (!dragging.value) return;
|
|
323
|
+
const edge = 48;
|
|
324
|
+
const top = scroller ? scroller.getBoundingClientRect().top : 0;
|
|
325
|
+
const bottom = scroller ? scroller.getBoundingClientRect().bottom : window.innerHeight;
|
|
326
|
+
let speed = 0;
|
|
327
|
+
if (lastY < top + edge) speed = -Math.ceil((top + edge - lastY) / edge * 16);
|
|
328
|
+
else if (lastY > bottom - edge) speed = Math.ceil((lastY - bottom + edge) / edge * 16);
|
|
329
|
+
if (speed) {
|
|
330
|
+
if (scroller) scroller.scrollTop += speed;
|
|
331
|
+
else window.scrollBy(0, speed);
|
|
332
|
+
updateDrop();
|
|
333
|
+
}
|
|
334
|
+
autoScroll();
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function finishDrag() {
|
|
338
|
+
const instance = view.value;
|
|
339
|
+
const spot = drop.value;
|
|
340
|
+
const moving = range;
|
|
341
|
+
stopDrag();
|
|
342
|
+
if (!instance || !spot || !moving) return;
|
|
343
|
+
const ids = blocksIn(moving).map((block) => block.node.attrs.id);
|
|
344
|
+
if (!moveRangeTo(moving, spot.pos)(instance.state, instance.dispatch)) return;
|
|
345
|
+
selectBlocks(ids[0], ids.at(-1))(instance.state, instance.dispatch);
|
|
346
|
+
instance.focus();
|
|
347
|
+
}
|
|
348
|
+
function stopDrag() {
|
|
349
|
+
dragging.value = false;
|
|
350
|
+
drop.value = null;
|
|
351
|
+
range = null;
|
|
352
|
+
start = null;
|
|
353
|
+
ghost?.remove();
|
|
354
|
+
ghost = null;
|
|
355
|
+
if (scrollFrame) cancelAnimationFrame(scrollFrame);
|
|
356
|
+
scrollFrame = 0;
|
|
357
|
+
window.removeEventListener("keydown", onDragKey, true);
|
|
358
|
+
}
|
|
359
|
+
watch(menuOpen, (value) => {
|
|
360
|
+
if (value) return;
|
|
361
|
+
if (touch.value) scheduleFollow();
|
|
362
|
+
else if (!root.value?.matches(":hover")) position.value = null;
|
|
363
|
+
});
|
|
364
|
+
return (_ctx, _cache) => {
|
|
365
|
+
return openBlock(), createElementBlock(Fragment, null, [withDirectives(createElementVNode("div", {
|
|
366
|
+
class: "dou-editor__handle",
|
|
367
|
+
style: normalizeStyle(position.value ? {
|
|
368
|
+
top: `${position.value.top}px`,
|
|
369
|
+
left: `${position.value.left}px`,
|
|
370
|
+
"--dou-editor-line": `${position.value.height}px`
|
|
371
|
+
} : void 0),
|
|
372
|
+
onMousedown: _cache[3] || (_cache[3] = withModifiers(() => {}, ["prevent"]))
|
|
373
|
+
}, [createElementVNode("button", {
|
|
374
|
+
type: "button",
|
|
375
|
+
tabindex: "-1",
|
|
376
|
+
class: "dou-editor__handle-button dou-editor__add",
|
|
377
|
+
"aria-label": unref(messages).addBlock,
|
|
378
|
+
title: unref(messages).addBlock,
|
|
379
|
+
onClick: add
|
|
380
|
+
}, [createVNode(unref(PlusIcon))], 8, _hoisted_1), createVNode(unref(DouDropdown), {
|
|
381
|
+
open: menuOpen.value,
|
|
382
|
+
"onUpdate:open": _cache[2] || (_cache[2] = ($event) => menuOpen.value = $event),
|
|
383
|
+
trigger: "manual",
|
|
384
|
+
placement: "bottom-start",
|
|
385
|
+
class: "dou-editor-menu"
|
|
386
|
+
}, {
|
|
387
|
+
menu: withCtx(() => [
|
|
388
|
+
createVNode(unref(DouDropdownSub), { title: unref(messages).turnInto }, {
|
|
389
|
+
default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(unref(turnOptions), (option) => {
|
|
390
|
+
return openBlock(), createBlock(unref(DouDropdownItem), {
|
|
391
|
+
key: option.key,
|
|
392
|
+
onClick: ($event) => turnInto(option)
|
|
393
|
+
}, createSlots({
|
|
394
|
+
prefix: withCtx(() => [(openBlock(), createBlock(resolveDynamicComponent(option.icon)))]),
|
|
395
|
+
default: withCtx(() => [createTextVNode(" " + toDisplayString(unref(labelOf)(option.label, unref(messages))) + " ", 1)]),
|
|
396
|
+
_: 2
|
|
397
|
+
}, [current.value && unref(isCurrent)(option, current.value.type, current.value.props) ? {
|
|
398
|
+
name: "suffix",
|
|
399
|
+
fn: withCtx(() => [createVNode(unref(CheckIcon))]),
|
|
400
|
+
key: "0"
|
|
401
|
+
} : void 0]), 1032, ["onClick"]);
|
|
402
|
+
}), 128))]),
|
|
403
|
+
_: 1
|
|
404
|
+
}, 8, ["title"]),
|
|
405
|
+
current.value ? (openBlock(true), createElementBlock(Fragment, { key: 0 }, renderList(unref(kit).blockMenu, (entry, index) => {
|
|
406
|
+
return openBlock(), createBlock(resolveDynamicComponent(entry), {
|
|
407
|
+
key: index,
|
|
408
|
+
block: current.value
|
|
409
|
+
}, null, 8, ["block"]);
|
|
410
|
+
}), 128)) : createCommentVNode("", true),
|
|
411
|
+
createVNode(unref(DouDropdownItem), {
|
|
412
|
+
shortcut: touch.value ? void 0 : "mod+d",
|
|
413
|
+
onClick: duplicate
|
|
414
|
+
}, {
|
|
415
|
+
prefix: withCtx(() => [createVNode(unref(CopyIcon))]),
|
|
416
|
+
default: withCtx(() => [createTextVNode(" " + toDisplayString(unref(messages).duplicate), 1)]),
|
|
417
|
+
_: 1
|
|
418
|
+
}, 8, ["shortcut"]),
|
|
419
|
+
createVNode(unref(DouDropdownItem), {
|
|
420
|
+
disabled: !canMove(-1),
|
|
421
|
+
shortcut: touch.value ? void 0 : "mod+shift+up",
|
|
422
|
+
onClick: _cache[0] || (_cache[0] = ($event) => move(-1))
|
|
423
|
+
}, {
|
|
424
|
+
prefix: withCtx(() => [createVNode(unref(ArrowUpIcon))]),
|
|
425
|
+
default: withCtx(() => [createTextVNode(" " + toDisplayString(unref(messages).moveUp), 1)]),
|
|
426
|
+
_: 1
|
|
427
|
+
}, 8, ["disabled", "shortcut"]),
|
|
428
|
+
createVNode(unref(DouDropdownItem), {
|
|
429
|
+
disabled: !canMove(1),
|
|
430
|
+
shortcut: touch.value ? void 0 : "mod+shift+down",
|
|
431
|
+
onClick: _cache[1] || (_cache[1] = ($event) => move(1))
|
|
432
|
+
}, {
|
|
433
|
+
prefix: withCtx(() => [createVNode(unref(ArrowDownIcon))]),
|
|
434
|
+
default: withCtx(() => [createTextVNode(" " + toDisplayString(unref(messages).moveDown), 1)]),
|
|
435
|
+
_: 1
|
|
436
|
+
}, 8, ["disabled", "shortcut"]),
|
|
437
|
+
createVNode(unref(DouDropdownItem), {
|
|
438
|
+
divided: "",
|
|
439
|
+
tone: "danger",
|
|
440
|
+
shortcut: touch.value ? void 0 : "delete",
|
|
441
|
+
onClick: remove
|
|
442
|
+
}, {
|
|
443
|
+
prefix: withCtx(() => [createVNode(unref(TrashIcon))]),
|
|
444
|
+
default: withCtx(() => [createTextVNode(" " + toDisplayString(unref(messages).delete), 1)]),
|
|
445
|
+
_: 1
|
|
446
|
+
}, 8, ["shortcut"])
|
|
447
|
+
]),
|
|
448
|
+
default: withCtx(() => [createElementVNode("button", {
|
|
449
|
+
type: "button",
|
|
450
|
+
tabindex: "-1",
|
|
451
|
+
class: "dou-editor__handle-button dou-editor__grip",
|
|
452
|
+
"aria-label": unref(messages).dragHandle,
|
|
453
|
+
title: unref(messages).dragHandle,
|
|
454
|
+
onPointerdown: onGripDown,
|
|
455
|
+
onPointermove: onGripMove,
|
|
456
|
+
onPointerup: onGripUp,
|
|
457
|
+
onPointercancel: stopDrag
|
|
458
|
+
}, [createVNode(unref(GripVerticalIcon))], 40, _hoisted_2)]),
|
|
459
|
+
_: 1
|
|
460
|
+
}, 8, ["open"])], 36), [[vShow, visible.value]]), drop.value ? (openBlock(), createElementBlock("div", {
|
|
461
|
+
key: 0,
|
|
462
|
+
class: "dou-editor__drop",
|
|
463
|
+
style: normalizeStyle({
|
|
464
|
+
top: `${drop.value.top}px`,
|
|
465
|
+
left: `${drop.value.left}px`,
|
|
466
|
+
width: `${drop.value.width}px`
|
|
467
|
+
})
|
|
468
|
+
}, null, 4)) : createCommentVNode("", true)], 64);
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
export { block_handle_vue_vue_type_script_setup_true_lang_default as default };
|
|
473
|
+
|
|
474
|
+
//# sourceMappingURL=block-handle.vue_vue_type_script_setup_true_lang.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-handle.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_Fragment","_withDirectives","_createElementVNode","_normalizeStyle","_withModifiers","_unref","_createVNode","_withCtx","_renderList","_createBlock","_createSlots","_resolveDynamicComponent","_createTextVNode","_toDisplayString","_createCommentVNode","_vShow"],"sources":["../../src/components/block-handle.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport {\n ArrowDownIcon,\n ArrowUpIcon,\n CheckIcon,\n CopyIcon,\n GripVerticalIcon,\n PlusIcon,\n TrashIcon,\n} from '@dou-ui/icons'\nimport { DouDropdown, DouDropdownItem, DouDropdownSub } from 'dou-ui'\nimport { TextSelection } from 'prosemirror-state'\nimport { computed, inject, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'\nimport { isCurrent } from '../catalog'\nimport { labelOf, type BlockOption } from '../core/extension'\nimport {\n blockSelectionKey,\n blockSelectionRange,\n getBlockSelection,\n selectBlocks,\n} from '../core/block-selection'\nimport {\n blockAround,\n blocksIn,\n findBlockById,\n siblingRangeBetween,\n type SiblingRange,\n} from '../core/blocks'\nimport {\n duplicateBlocks,\n insertBlockAfter,\n swapBlocks,\n moveRangeTo,\n deleteBlocks,\n setBlockType,\n} from '../core/commands'\nimport { blockFromNode } from '../core/model'\nimport { slashKey } from '../core/slash'\nimport { blockElementOf, contentAt, firstLine, scrollParent } from '../dom'\nimport { editorKey } from '../editor'\nimport { useMenuEscape } from './menu-escape'\n\nconst { view, state, focused, messages, root, dragging, kit } = inject(editorKey)!\n\n/**\n * Touch screens have no hover: there the handle shows only the grip, next\n * to the block with the cursor (or the selected blocks) while the editor\n * has focus. A tap on it opens the menu, a press and drag moves the block.\n */\nconst touch = shallowRef(false)\nlet media: MediaQueryList | null = null\nconst onMedia = () => (touch.value = !!media?.matches)\n\n/** The block the handle is next to, by id. */\nconst hovered = shallowRef<string | null>(null)\nconst position = shallowRef<{ top: number; left: number; height: number } | null>(null)\nconst menuOpen = shallowRef(false)\nuseMenuEscape(menuOpen, () => (menuOpen.value = false))\n\nconst drop = shallowRef<{ pos: number; top: number; left: number; width: number } | null>(null)\n// typing hides the handle until the pointer moves again\nconst resting = shallowRef(false)\n\nconst visible = computed(() => {\n if (!position.value) return false\n if (menuOpen.value || dragging.value) return true\n if (touch.value) return focused.value || (!!state.value && !!getBlockSelection(state.value))\n return !resting.value\n})\n\nfunction place(content: HTMLElement) {\n const block = blockElementOf(content)\n const id = block?.dataset.blockId\n const host = root.value\n if (!id || !host) return\n const box = host.getBoundingClientRect()\n const line = firstLine(content)\n hovered.value = id\n position.value = {\n top: line.top - box.top,\n left: content.getBoundingClientRect().left - box.left,\n height: line.height,\n }\n}\n\nlet frame = 0\nlet lastY = 0\nlet lastX = 0\n\nfunction track(event: PointerEvent) {\n // a finger only moves the handle by moving the cursor; a drag reports itself\n if (event.pointerType === 'touch') return\n lastY = event.clientY\n lastX = event.clientX\n resting.value = false\n if (menuOpen.value || frame) return\n frame = requestAnimationFrame(() => {\n frame = 0\n if (dragging.value) return updateDrop()\n const current = view.value\n if (!current || menuOpen.value) return\n const content = contentAt(current, lastY)\n if (content) place(content)\n })\n}\n\nfunction leave(event: PointerEvent) {\n if (menuOpen.value || dragging.value || event.pointerType === 'touch') return\n position.value = null\n hovered.value = null\n}\n\n/** On touch screens: put the handle next to the block with the cursor. */\nfunction follow() {\n const current = view.value\n if (!current || dragging.value) return\n const selected = blockSelectionRange(current.state)\n const info = selected ? blocksIn(selected)[0] : blockAround(current.state.selection.$from)\n const element = info\n ? (current.nodeDOM(info.pos) as HTMLElement | null)?.querySelector<HTMLElement>(\n ':scope > .dou-editor__content',\n )\n : null\n if (element) place(element)\n else {\n position.value = null\n hovered.value = null\n }\n}\n\nlet followFrame = 0\nfunction scheduleFollow() {\n if (!touch.value || followFrame) return\n followFrame = requestAnimationFrame(() => {\n followFrame = 0\n if (touch.value && !menuOpen.value) follow()\n })\n}\n\nwatch([state, focused, touch], scheduleFollow)\n\nonMounted(() => {\n media = window.matchMedia('(pointer: coarse)')\n onMedia()\n media.addEventListener('change', onMedia)\n window.addEventListener('resize', scheduleFollow)\n root.value?.addEventListener('pointermove', track)\n root.value?.addEventListener('pointerleave', leave)\n})\nonBeforeUnmount(() => {\n media?.removeEventListener('change', onMedia)\n window.removeEventListener('resize', scheduleFollow)\n if (followFrame) cancelAnimationFrame(followFrame)\n root.value?.removeEventListener('pointermove', track)\n root.value?.removeEventListener('pointerleave', leave)\n if (frame) cancelAnimationFrame(frame)\n stopDrag()\n})\n\n// follow edits: keep the handle on its block, or hide it while typing\nwatch(state, (next, previous) => {\n if (touch.value || !next || !previous || next.doc === previous.doc || dragging.value) return\n if (!menuOpen.value) resting.value = true\n requestAnimationFrame(() => {\n const current = view.value\n if (!current || !hovered.value) return\n const element = current.dom.querySelector<HTMLElement>(\n `[data-block-id=\"${CSS.escape(hovered.value)}\"] > .dou-editor__content`,\n )\n if (element) place(element)\n else position.value = null\n })\n})\n\nconst target = computed(() => {\n const doc = state.value?.doc\n return doc && hovered.value ? findBlockById(doc, hovered.value) : null\n})\n\n/** The hovered block, or every selected block when it is one of them. */\nfunction rangeForHovered(): SiblingRange | null {\n const current = view.value\n const info = target.value\n if (!current || !info) return null\n const selected = blockSelectionRange(current.state)\n if (selected && info.pos >= selected.start && info.pos < selected.end) return selected\n return siblingRangeBetween(current.state.doc, info.pos + 1, info.pos + 1)\n}\n\n/** Select the blocks the menu or a drag acts on. */\nfunction selectTarget(): SiblingRange | null {\n const current = view.value\n const range = rangeForHovered()\n if (!current || !range) return null\n const blocks = blocksIn(range)\n selectBlocks(blocks[0]!.node.attrs.id, blocks.at(-1)!.node.attrs.id)(\n current.state,\n current.dispatch,\n )\n return blockSelectionRange(current.state)\n}\n\n/* the + button */\n\nfunction add(event: MouseEvent) {\n const current = view.value\n const info = target.value\n if (!current || !info) return\n const empty = info.content.isTextblock && info.content.content.size === 0\n if (!empty || info.content.type.name !== 'paragraph') {\n insertBlockAfter(info.pos)(current.state, current.dispatch)\n // Alt+click adds the block above instead\n if (event.altKey) swapBlocks(-1)(current.state, current.dispatch)\n } else {\n const tr = current.state.tr.setSelection(\n TextSelection.create(current.state.doc, info.contentPos + 1),\n )\n current.dispatch(tr)\n }\n const from = current.state.selection.from\n current.dispatch(current.state.tr.insertText('/').setMeta(slashKey, { from, query: '' }))\n current.focus()\n}\n\n/* the block menu */\n\nconst turnOptions = kit.blockOptions.filter(\n (option) => !!kit.schema.nodes[option.type]?.isTextblock,\n)\nconst current = computed(() => {\n const info = target.value\n return info ? blockFromNode(info.node) : null\n})\n\nfunction after(action: () => void) {\n action()\n // the menu gives focus back to the handle; the editor takes it after\n requestAnimationFrame(() => view.value?.focus())\n}\n\nfunction keep(run: (current: NonNullable<typeof view.value>) => void) {\n after(() => {\n const instance = view.value\n if (!instance) return\n const selection = getBlockSelection(instance.state)\n run(instance)\n if (selection) selectBlocks(selection.anchor, selection.head)(instance.state, instance.dispatch)\n })\n}\n\nconst turnInto = (option: BlockOption) =>\n keep((instance) => setBlockType(option.type, option.props)(instance.state, instance.dispatch))\nconst duplicate = () => keep((instance) => duplicateBlocks(instance.state, instance.dispatch))\nconst move = (direction: -1 | 1) =>\n keep((instance) => swapBlocks(direction)(instance.state, instance.dispatch))\n\nfunction remove() {\n after(() => {\n const instance = view.value\n const range = instance && blockSelectionRange(instance.state)\n if (!instance || !range) return\n const tr = deleteBlocks(instance.state.tr, blocksIn(range)).setMeta(blockSelectionKey, null)\n instance.dispatch(tr.scrollIntoView())\n position.value = null\n })\n}\n\nconst canMove = (direction: -1 | 1) => {\n const instance = view.value\n return !!instance && !!state.value && swapBlocks(direction)(instance.state)\n}\n\n/* dragging */\n\nlet start: { x: number; y: number; pointer: number; touch: boolean } | null = null\nlet range: SiblingRange | null = null\nlet scroller: HTMLElement | null = null\nlet scrollFrame = 0\n/** A copy of the dragged blocks that follows the pointer. */\nlet ghost: HTMLElement | null = null\n\nfunction showGhost(moving: SiblingRange) {\n const instance = view.value\n const host = root.value\n if (!instance || !host) return\n ghost = document.createElement('div')\n ghost.className = 'dou-editor__surface dou-editor__ghost'\n ghost.setAttribute('aria-hidden', 'true')\n let width = 0\n for (const block of blocksIn(moving)) {\n const element = instance.nodeDOM(block.pos) as HTMLElement | null\n if (!element) continue\n width = Math.max(width, element.getBoundingClientRect().width)\n const copy = element.cloneNode(true) as HTMLElement\n copy.classList.remove('is-selected')\n for (const node of [copy, ...copy.querySelectorAll('[id], .is-selected')]) {\n node.removeAttribute('id')\n node.classList.remove('is-selected')\n }\n ghost.append(copy)\n }\n ghost.style.width = `${Math.min(width, 360)}px`\n host.append(ghost)\n moveGhost()\n}\n\nfunction moveGhost() {\n if (!ghost) return\n // above a finger, which would hide it; below and right of a mouse pointer\n const y = start?.touch ? lastY - ghost.offsetHeight - 24 : lastY + 12\n ghost.style.transform = `translate(${lastX + 12}px, ${y}px)`\n}\n\nfunction onGripDown(event: PointerEvent) {\n if (event.button !== 0) return\n start = {\n x: event.clientX,\n y: event.clientY,\n pointer: event.pointerId,\n touch: event.pointerType === 'touch',\n }\n ;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)\n}\n\nfunction onGripMove(event: PointerEvent) {\n if (!start || event.pointerId !== start.pointer) return\n lastX = event.clientX\n lastY = event.clientY\n if (!dragging.value) {\n // a finger wobbles more than a mouse while tapping\n const slop = start.touch ? 8 : 4\n if (Math.hypot(event.clientX - start.x, event.clientY - start.y) < slop) return\n range = selectTarget()\n if (!range) return stopDrag()\n dragging.value = true\n scroller = root.value && scrollParent(root.value)\n window.addEventListener('keydown', onDragKey, true)\n showGhost(range)\n autoScroll()\n }\n moveGhost()\n updateDrop()\n}\n\nfunction onGripUp(event: PointerEvent) {\n if (!start || event.pointerId !== start.pointer) return\n if (dragging.value) finishDrag()\n else if (menuOpen.value) menuOpen.value = false\n else {\n selectTarget()\n menuOpen.value = true\n }\n start = null\n}\n\nfunction onDragKey(event: KeyboardEvent) {\n if (event.key !== 'Escape') return\n event.preventDefault()\n event.stopPropagation()\n stopDrag()\n}\n\nfunction updateDrop() {\n const instance = view.value\n const host = root.value\n if (!instance || !host || !range) return\n const content = contentAt(instance, lastY)\n const id = content && blockElementOf(content)?.dataset.blockId\n const info = id ? findBlockById(instance.state.doc, id) : null\n if (!content || !info) return (drop.value = null)\n const box = host.getBoundingClientRect()\n const rect = content.getBoundingClientRect()\n const below = lastY > rect.top + rect.height / 2\n let pos: number\n let y: number\n let left = rect.left\n if (!below) {\n pos = info.pos\n y = rect.top\n } else if (info.group) {\n // the lower half of a block with children: in front of its first child\n pos = info.contentPos + info.content.nodeSize + 1\n y = rect.bottom\n const child = blockElementOf(content)?.querySelector<HTMLElement>(\n ':scope > .dou-editor__group > .dou-editor__block > .dou-editor__content',\n )\n left = child?.getBoundingClientRect().left ?? rect.left\n } else {\n pos = info.pos + info.node.nodeSize\n y = blockElementOf(content)!.getBoundingClientRect().bottom\n }\n if (!moveRangeTo(range, pos)(instance.state)) return (drop.value = null)\n drop.value = { pos, top: y - box.top, left: left - box.left, width: box.right - left }\n}\n\nfunction autoScroll() {\n scrollFrame = requestAnimationFrame(() => {\n if (!dragging.value) return\n const edge = 48\n const top = scroller ? scroller.getBoundingClientRect().top : 0\n const bottom = scroller ? scroller.getBoundingClientRect().bottom : window.innerHeight\n let speed = 0\n if (lastY < top + edge) speed = -Math.ceil(((top + edge - lastY) / edge) * 16)\n else if (lastY > bottom - edge) speed = Math.ceil(((lastY - bottom + edge) / edge) * 16)\n if (speed) {\n if (scroller) scroller.scrollTop += speed\n else window.scrollBy(0, speed)\n updateDrop()\n }\n autoScroll()\n })\n}\n\nfunction finishDrag() {\n const instance = view.value\n const spot = drop.value\n const moving = range\n stopDrag()\n if (!instance || !spot || !moving) return\n const ids = blocksIn(moving).map((block) => block.node.attrs.id as string)\n if (!moveRangeTo(moving, spot.pos)(instance.state, instance.dispatch)) return\n selectBlocks(ids[0]!, ids.at(-1)!)(instance.state, instance.dispatch)\n instance.focus()\n}\n\nfunction stopDrag() {\n dragging.value = false\n drop.value = null\n range = null\n start = null\n ghost?.remove()\n ghost = null\n if (scrollFrame) cancelAnimationFrame(scrollFrame)\n scrollFrame = 0\n window.removeEventListener('keydown', onDragKey, true)\n}\n\nwatch(menuOpen, (value) => {\n if (value) return\n if (touch.value) scheduleFollow()\n else if (!root.value?.matches(':hover')) position.value = null\n})\n</script>\n\n<template>\n <div\n v-show=\"visible\"\n class=\"dou-editor__handle\"\n :style=\"\n position\n ? {\n top: `${position.top}px`,\n left: `${position.left}px`,\n '--dou-editor-line': `${position.height}px`,\n }\n : undefined\n \"\n @mousedown.prevent\n >\n <button\n type=\"button\"\n tabindex=\"-1\"\n class=\"dou-editor__handle-button dou-editor__add\"\n :aria-label=\"messages.addBlock\"\n :title=\"messages.addBlock\"\n @click=\"add\"\n >\n <PlusIcon />\n </button>\n <DouDropdown\n v-model:open=\"menuOpen\"\n trigger=\"manual\"\n placement=\"bottom-start\"\n class=\"dou-editor-menu\"\n >\n <button\n type=\"button\"\n tabindex=\"-1\"\n class=\"dou-editor__handle-button dou-editor__grip\"\n :aria-label=\"messages.dragHandle\"\n :title=\"messages.dragHandle\"\n @pointerdown=\"onGripDown\"\n @pointermove=\"onGripMove\"\n @pointerup=\"onGripUp\"\n @pointercancel=\"stopDrag\"\n >\n <GripVerticalIcon />\n </button>\n <template #menu>\n <DouDropdownSub :title=\"messages.turnInto\">\n <DouDropdownItem\n v-for=\"option in turnOptions\"\n :key=\"option.key\"\n @click=\"turnInto(option)\"\n >\n <template #prefix><component :is=\"option.icon\" /></template>\n {{ labelOf(option.label, messages) }}\n <template v-if=\"current && isCurrent(option, current.type, current.props)\" #suffix>\n <CheckIcon />\n </template>\n </DouDropdownItem>\n </DouDropdownSub>\n <template v-if=\"current\">\n <component\n :is=\"entry\"\n v-for=\"(entry, index) in kit.blockMenu\"\n :key=\"index\"\n :block=\"current\"\n />\n </template>\n <DouDropdownItem :shortcut=\"touch ? undefined : 'mod+d'\" @click=\"duplicate\">\n <template #prefix><CopyIcon /></template>\n {{ messages.duplicate }}\n </DouDropdownItem>\n <DouDropdownItem\n :disabled=\"!canMove(-1)\"\n :shortcut=\"touch ? undefined : 'mod+shift+up'\"\n @click=\"move(-1)\"\n >\n <template #prefix><ArrowUpIcon /></template>\n {{ messages.moveUp }}\n </DouDropdownItem>\n <DouDropdownItem\n :disabled=\"!canMove(1)\"\n :shortcut=\"touch ? undefined : 'mod+shift+down'\"\n @click=\"move(1)\"\n >\n <template #prefix><ArrowDownIcon /></template>\n {{ messages.moveDown }}\n </DouDropdownItem>\n <DouDropdownItem\n divided\n tone=\"danger\"\n :shortcut=\"touch ? undefined : 'delete'\"\n @click=\"remove\"\n >\n <template #prefix><TrashIcon /></template>\n {{ messages.delete }}\n </DouDropdownItem>\n </template>\n </DouDropdown>\n </div>\n <div\n v-if=\"drop\"\n class=\"dou-editor__drop\"\n :style=\"{ top: `${drop.top}px`, left: `${drop.left}px`, width: `${drop.width}px` }\"\n />\n</template>\n"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import bubble_toolbar_vue_vue_type_script_setup_true_lang_default from "./bubble-toolbar.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
// src/components/bubble-toolbar.vue
|
|
3
|
+
var bubble_toolbar_default = bubble_toolbar_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
export { bubble_toolbar_default as default };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=bubble-toolbar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bubble-toolbar.js","names":["_sfc_main"],"sources":["../../src/components/bubble-toolbar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { CheckIcon, ChevronDownIcon, LinkIcon, MessagePlusIcon, UnlinkIcon } from '@dou-ui/icons'\nimport {\n DouButton,\n DouDropdown,\n DouDropdownItem,\n DouInput,\n DouToggle,\n DouToggleGroup,\n DouToolbar,\n DouToolbarSeparator,\n DouTooltip,\n isComposing,\n useFloating,\n useZIndex,\n type ToggleValue,\n type VirtualElement,\n} from 'dou-ui'\nimport { toggleMark } from 'prosemirror-commands'\nimport { TextSelection } from 'prosemirror-state'\nimport {\n computed,\n inject,\n nextTick,\n onBeforeUnmount,\n onMounted,\n shallowRef,\n useTemplateRef,\n watch,\n} from 'vue'\nimport { isCurrent } from '../catalog'\nimport { labelOf, type BlockOption } from '../core/extension'\nimport { getBlockSelection } from '../core/block-selection'\nimport { blockAround } from '../core/blocks'\nimport { setBlockType } from '../core/commands'\nimport { blockFromNode } from '../core/model'\nimport { isSafeHref } from '../core/schema'\nimport { editorKey, provideToolbarMenus } from '../editor'\n\nconst { view, state, focused, dragging, messages, kit, comments } = inject(editorKey)!\nconst panel = useTemplateRef<HTMLElement>('panel')\n\n// while the pointer is down the selection is still being made\nconst pressing = shallowRef(false)\nconst menuOpen = shallowRef(false)\n// the extensions' menus keep the toolbar too\nconst toolbarMenus = provideToolbarMenus()\n// so does the focus in the toolbar, e.g. back on a menu's button after Escape\nconst panelFocused = shallowRef(false)\nwatch(focused, (value) => value && (panelFocused.value = false))\nfunction onPanelFocusOut(event: FocusEvent) {\n if (!panel.value?.contains(event.relatedTarget as Node | null)) panelFocused.value = false\n}\nfunction onPanelKeyDown(event: KeyboardEvent) {\n if (event.key !== 'Escape' || linking.value || event.defaultPrevented) return\n event.preventDefault()\n view.value?.focus()\n}\nconst linking = shallowRef<{ from: number; to: number } | null>(null)\nconst href = shallowRef('')\nconst linkInput = useTemplateRef<InstanceType<typeof DouInput>>('linkInput')\n\nconst formattable = computed(() => {\n const current = state.value\n if (!current || getBlockSelection(current)) return false\n const { selection } = current\n if (!(selection instanceof TextSelection) || selection.empty) return false\n // nothing to format in code, or across a divider only\n let text = false\n current.doc.nodesBetween(selection.from, selection.to, (node) => {\n if (node.isTextblock && !node.type.spec.code) text = true\n return !text\n })\n return text\n})\n\nconst open = computed(\n () =>\n !!linking.value ||\n menuOpen.value ||\n [...toolbarMenus].some((menu) => menu.value) ||\n panelFocused.value ||\n (focused.value && formattable.value && !pressing.value && !dragging.value),\n)\n\nconst anchor: VirtualElement = {\n getBoundingClientRect() {\n const current = view.value\n const range = linking.value ?? current?.state.selection\n if (!current || !range) return new DOMRect()\n const start = current.coordsAtPos(range.from, 1)\n const end = current.coordsAtPos(range.to, -1)\n // a selection over several lines: centre on the first one\n const sameLine = Math.abs(start.top - end.top) < 4\n const left = sameLine ? start.left : start.left\n const right = sameLine\n ? end.right\n : Math.max(start.left + 1, current.dom.getBoundingClientRect().right)\n return new DOMRect(left, start.top, Math.max(1, right - left), start.bottom - start.top)\n },\n}\nconst { style, placement, update } = useFloating(() => (open.value ? anchor : null), panel, {\n open,\n placement: 'top',\n offset: 8,\n})\n// stacked like DouUI's own popups, above whatever opened before\nconst { next } = useZIndex()\nconst zIndex = shallowRef(0)\nwatch(open, (value) => value && (zIndex.value = next()), { immediate: true })\nwatch(state, () => open.value && void nextTick(update))\n\nfunction onPointerDown(event: PointerEvent) {\n if (event.button === 0 && view.value?.dom.contains(event.target as Node)) pressing.value = true\n}\nfunction onPointerUp() {\n if (pressing.value) pressing.value = false\n}\n/*\n * A menu opened with the mouse leaves the focus in the text: until it\n * closes, Escape closes it and the arrow keys and Enter go into it,\n * instead of acting on the text.\n */\nfunction onKeyDown(event: KeyboardEvent) {\n if (!view.value?.dom.contains(event.target as Node)) return\n const trigger = panel.value?.querySelector<HTMLElement>('[aria-expanded=\"true\"][aria-controls]')\n if (!trigger) return\n const menu = document.getElementById(trigger.getAttribute('aria-controls')!)\n if (event.key === 'Escape') {\n event.preventDefault()\n event.stopPropagation()\n menuOpen.value = false\n for (const item of toolbarMenus) item.value = false\n return\n }\n if (!menu || !['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) return\n event.preventDefault()\n event.stopPropagation()\n const items = Array.from(\n menu.querySelectorAll<HTMLElement>('[role^=\"menuitem\"]:not([aria-disabled=\"true\"])'),\n )\n const last = event.key === 'ArrowUp' || event.key === 'End'\n ;(last ? items.at(-1) : items[0])?.focus()\n}\n\nonMounted(() => {\n document.addEventListener('pointerdown', onPointerDown, true)\n document.addEventListener('pointerup', onPointerUp, true)\n document.addEventListener('keydown', onKeyDown, true)\n})\nonBeforeUnmount(() => {\n document.removeEventListener('pointerdown', onPointerDown, true)\n document.removeEventListener('pointerup', onPointerUp, true)\n document.removeEventListener('keydown', onKeyDown, true)\n})\n\n/* marks */\n\nconst schema = kit.schema\nconst marks = kit.formats.filter((format) => schema.marks[format.mark])\n\nconst active = computed<ToggleValue[]>(() => {\n const current = state.value\n if (!current) return []\n const { from, to } = current.selection\n return marks\n .filter((mark) => current.doc.rangeHasMark(from, to, schema.marks[mark.mark]!))\n .map((mark) => mark.mark)\n})\n\nfunction onMarks(value: ToggleValue | ToggleValue[] | null) {\n const pressed = Array.isArray(value) ? value : []\n const changed = marks.find(\n (mark) => pressed.includes(mark.mark) !== active.value.includes(mark.mark),\n )\n const current = view.value\n if (!changed || !current) return\n toggleMark(schema.marks[changed.mark]!)(current.state, current.dispatch)\n}\n\n/* turning the block into another type */\n\nconst turnOptions = kit.blockOptions.filter(\n (option) => !!kit.schema.nodes[option.type]?.isTextblock,\n)\nconst currentBlock = computed(() => {\n const current = state.value\n const info = current && blockAround(current.selection.$from)\n return info ? blockFromNode(info.node) : null\n})\n// text inside a table cell keeps its cell; only whole blocks change type\nconst turnable = computed(() => {\n const $from = state.value?.selection.$from\n return !!$from && $from.depth > 0 && $from.node($from.depth - 1).type.name === 'block'\n})\nconst currentOption = computed(() => {\n const block = currentBlock.value\n return block\n ? turnOptions.find((option) => isCurrent(option, block.type, block.props))\n : undefined\n})\n\nfunction turnInto(option: BlockOption) {\n const current = view.value\n if (!current) return\n setBlockType(option.type, option.props)(current.state, current.dispatch)\n requestAnimationFrame(() => current.focus())\n}\n\n/* links */\n\nconst linkMark = schema.marks.link\nconst currentHref = computed(() => {\n const current = state.value\n if (!current) return ''\n const { from, to } = current.selection\n let found = ''\n current.doc.nodesBetween(from, to, (node) => {\n const mark = linkMark?.isInSet(node.marks)\n if (mark) found ||= mark.attrs.href as string\n })\n return found\n})\n\n/** `Mod+K` and the link button: edit the link of the selected text. */\nfunction editLink(): boolean {\n const current = view.value\n if (!current || !linkMark || !formattable.value) return false\n const { from, to } = current.state.selection\n linking.value = { from, to }\n href.value = currentHref.value\n void nextTick(() => linkInput.value?.focus())\n return true\n}\n\n/** `example.com` becomes `https://example.com`; other relative links stay. */\nfunction normalise(value: string): string {\n const text = value.trim()\n if (/^[\\w-]+(\\.[\\w-]+)+(?:[/?#:]|$)/.test(text) && !text.includes(' ')) return `https://${text}`\n return text\n}\n\nfunction applyLink() {\n const current = view.value\n const range = linking.value\n if (!current || !range || !linkMark) return\n const value = normalise(href.value)\n const tr = current.state.tr.removeMark(range.from, range.to, linkMark)\n if (value && isSafeHref(value)) tr.addMark(range.from, range.to, linkMark.create({ href: value }))\n current.dispatch(tr)\n closeLink()\n}\n\nfunction removeLink() {\n href.value = ''\n applyLink()\n}\n\nfunction closeLink() {\n const current = view.value\n const range = linking.value\n linking.value = null\n if (!current) return\n if (range)\n current.dispatch(\n current.state.tr.setSelection(TextSelection.create(current.state.doc, range.from, range.to)),\n )\n current.focus()\n}\n\nfunction onLinkKey(event: KeyboardEvent) {\n if (isComposing(event)) return\n if (event.key === 'Enter') {\n event.preventDefault()\n applyLink()\n } else if (event.key === 'Escape') {\n event.preventDefault()\n event.stopPropagation()\n closeLink()\n }\n}\n\nfunction onLinkBlur(event: FocusEvent) {\n // leaving for anywhere but the toolbar ends editing without saving\n if (!panel.value?.contains(event.relatedTarget as Node)) linking.value = null\n}\n\ndefineExpose({ editLink })\n</script>\n\n<template>\n <Transition name=\"dou-floating\">\n <div\n v-if=\"open\"\n ref=\"panel\"\n class=\"dou-floating dou-editor-bubble\"\n :style=\"[style, { zIndex }]\"\n :data-placement=\"placement\"\n @mousedown=\"!linking && $event.preventDefault()\"\n @focusin=\"panelFocused = true\"\n @focusout=\"onPanelFocusOut\"\n @keydown=\"onPanelKeyDown\"\n >\n <div v-if=\"linking\" class=\"dou-editor-bubble__link\">\n <DouInput\n ref=\"linkInput\"\n v-model=\"href\"\n size=\"sm\"\n :placeholder=\"messages.linkPlaceholder\"\n :aria-label=\"messages.link\"\n @keydown=\"onLinkKey\"\n @blur=\"onLinkBlur\"\n >\n <template #prefix><LinkIcon /></template>\n </DouInput>\n <DouButton\n size=\"sm\"\n variant=\"ghost\"\n :aria-label=\"messages.applyLink\"\n @mousedown.prevent\n @click=\"applyLink\"\n >\n <CheckIcon />\n </DouButton>\n <DouButton\n v-if=\"currentHref\"\n size=\"sm\"\n variant=\"ghost\"\n :aria-label=\"messages.removeLink\"\n @mousedown.prevent\n @click=\"removeLink\"\n >\n <UnlinkIcon />\n </DouButton>\n </div>\n <DouToolbar v-else size=\"sm\" :aria-label=\"messages.formatting\">\n <DouDropdown\n v-if=\"turnable\"\n v-model:open=\"menuOpen\"\n placement=\"bottom-start\"\n class=\"dou-editor-menu\"\n >\n <DouButton size=\"sm\" variant=\"ghost\" class=\"dou-editor-bubble__type\">\n {{ currentOption ? labelOf(currentOption.label, messages) : messages.turnInto }}\n <template #trailing><ChevronDownIcon /></template>\n </DouButton>\n <template #menu>\n <DouDropdownItem\n v-for=\"option in turnOptions\"\n :key=\"option.key\"\n @click=\"turnInto(option)\"\n >\n <template #prefix><component :is=\"option.icon\" /></template>\n {{ labelOf(option.label, messages) }}\n <template v-if=\"option === currentOption\" #suffix><CheckIcon /></template>\n </DouDropdownItem>\n </template>\n </DouDropdown>\n <DouToolbarSeparator v-if=\"turnable && marks.length\" />\n <DouToggleGroup\n v-if=\"marks.length\"\n type=\"multiple\"\n :model-value=\"active\"\n :aria-label=\"messages.formatting\"\n @update:model-value=\"onMarks\"\n >\n <DouTooltip\n v-for=\"mark in marks\"\n :key=\"mark.mark\"\n :content=\"labelOf(mark.label, messages)\"\n :shortcut=\"mark.hotkey\"\n placement=\"bottom\"\n >\n <DouToggle :value=\"mark.mark\" :aria-label=\"labelOf(mark.label, messages)\">\n <component :is=\"mark.icon\" />\n </DouToggle>\n </DouTooltip>\n </DouToggleGroup>\n <template v-if=\"kit.toolbar.length\">\n <DouToolbarSeparator v-if=\"marks.length\" />\n <component :is=\"control\" v-for=\"(control, index) in kit.toolbar\" :key=\"index\" />\n </template>\n <DouToolbarSeparator v-if=\"linkMark\" />\n <DouTooltip v-if=\"linkMark\" :content=\"messages.link\" shortcut=\"mod+k\" placement=\"bottom\">\n <DouToggle\n :pressed=\"!!currentHref\"\n :aria-label=\"messages.link\"\n @update:pressed=\"editLink\"\n >\n <LinkIcon />\n </DouToggle>\n </DouTooltip>\n <template v-if=\"comments.writable.value\">\n <DouToolbarSeparator />\n <DouTooltip :content=\"messages.comment\" shortcut=\"mod+alt+m\" placement=\"bottom\">\n <DouButton\n size=\"sm\"\n variant=\"ghost\"\n :aria-label=\"messages.comment\"\n @click=\"comments.start()\"\n >\n <MessagePlusIcon />\n </DouButton>\n </DouTooltip>\n </template>\n </DouToolbar>\n </div>\n </Transition>\n</template>\n"],"mappings":""}
|