From a66b1bbb383311b647c466cbcb17d56f41c0378e Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Tue, 29 Aug 2023 16:55:08 +0800 Subject: [PATCH 01/11] chore: optimize the behavior of the bubble menu --- .../src/components/EditorBubbleMenu.vue | 7 +++--- .../src/components/bubble/BubbleItem.vue | 22 ++++++++++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/components/EditorBubbleMenu.vue b/packages/editor/src/components/EditorBubbleMenu.vue index 6e9aaae..c39b0d2 100644 --- a/packages/editor/src/components/EditorBubbleMenu.vue +++ b/packages/editor/src/components/EditorBubbleMenu.vue @@ -1,6 +1,4 @@ - + diff --git a/packages/editor/src/components/bubble/TextBubbleMenu.vue b/packages/editor/src/components/bubble/TextBubbleMenu.vue new file mode 100644 index 0000000..69861b5 --- /dev/null +++ b/packages/editor/src/components/bubble/TextBubbleMenu.vue @@ -0,0 +1,14 @@ + + + + diff --git a/packages/editor/src/extensions/text/index.ts b/packages/editor/src/extensions/text/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/editor/src/types/index.ts b/packages/editor/src/types/index.ts index 4b64b2e..32f5e3b 100644 --- a/packages/editor/src/types/index.ts +++ b/packages/editor/src/types/index.ts @@ -1,6 +1,7 @@ -import type { Editor, Range } from "@tiptap/vue-3"; +import type { Editor, Range } from "@tiptap/core"; +import type { EditorState } from "prosemirror-state"; +import type { EditorView } from "prosemirror-view"; import type { Component } from "vue"; - export interface ToolbarItem { priority: number; component: Component; @@ -15,17 +16,22 @@ export interface ToolbarItem { children?: ToolbarItem[]; } -export interface BubbleItem { - priority: number; - component: Component; - props: { +interface BubbleItemProps { + pluginKey?: string; + editor: Editor; + updateDelay: number; + shouldShow: (props: { editor: Editor; - isActive: boolean; - visible?: boolean; - icon?: Component; - title?: string; - action?: () => void; - }; + view: EditorView; + state: EditorState; + oldState?: EditorState | undefined; + from: number; + to: number; + }) => boolean; +} + +export interface NodeBubbleMenu extends BubbleItemProps { + component: Component; } export interface ToolboxItem { @@ -49,11 +55,7 @@ export interface ExtensionOptions { getCommandMenuItems?: () => CommandMenuItem | CommandMenuItem[]; - getBubbleItems?: ({ - editor, - }: { - editor: Editor; - }) => BubbleItem | BubbleItem[]; + getBubbleMenu?: ({ editor }: { editor: Editor }) => NodeBubbleMenu; getToolboxItems?: ({ editor, From 48bc7f0882670719da9f79d57de1ad179f75ceaa Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Wed, 30 Aug 2023 18:41:10 +0800 Subject: [PATCH 03/11] wip: update bubble menu --- .../src/components/EditorBubbleMenu.vue | 35 ++- .../src/components/bubble/BubbleItem.vue | 82 +++++-- .../src/components/bubble/TextBubbleMenu.ts | 228 ++++++++++++++++++ .../src/components/bubble/TextBubbleMenu.vue | 14 -- .../editor/src/extensions/audio/AudioView.vue | 147 ++--------- .../extensions/audio/BubbleItemEditorLink.vue | 37 +++ packages/editor/src/extensions/audio/index.ts | 111 ++++++++- .../editor/src/extensions/blockquote/index.ts | 16 -- packages/editor/src/extensions/bold/index.ts | 14 -- .../code-block/CodeBlockViewRenderer.vue | 44 ++-- .../src/extensions/code-block/code-block.ts | 36 +-- packages/editor/src/extensions/code/index.ts | 14 -- .../src/extensions/color/ColorBubbleItem.vue | 32 +-- packages/editor/src/extensions/color/index.ts | 13 - .../highlight/HighlightBubbleItem.vue | 30 +-- .../editor/src/extensions/highlight/index.ts | 12 - .../editor/src/extensions/italic/index.ts | 14 -- .../src/extensions/link/LinkBubbleButton.vue | 31 +-- packages/editor/src/extensions/link/index.ts | 56 +---- .../editor/src/extensions/strike/index.ts | 14 -- .../editor/src/extensions/subscript/index.ts | 14 -- .../src/extensions/superscript/index.ts | 14 -- .../editor/src/extensions/text-align/index.ts | 50 ---- .../editor/src/extensions/underline/index.ts | 14 -- .../editor/src/extensions/video/VideoView.vue | 47 ---- packages/editor/src/extensions/video/index.ts | 103 ++++++++ packages/editor/src/types/index.ts | 24 +- packages/editor/src/utils/delete-node.ts | 47 ++++ packages/editor/src/utils/index.ts | 1 + 29 files changed, 720 insertions(+), 574 deletions(-) create mode 100644 packages/editor/src/components/bubble/TextBubbleMenu.ts delete mode 100644 packages/editor/src/components/bubble/TextBubbleMenu.vue create mode 100644 packages/editor/src/extensions/audio/BubbleItemEditorLink.vue create mode 100644 packages/editor/src/utils/delete-node.ts create mode 100644 packages/editor/src/utils/index.ts diff --git a/packages/editor/src/components/EditorBubbleMenu.vue b/packages/editor/src/components/EditorBubbleMenu.vue index 76e29fe..28c6930 100644 --- a/packages/editor/src/components/EditorBubbleMenu.vue +++ b/packages/editor/src/components/EditorBubbleMenu.vue @@ -1,8 +1,10 @@ - + + + + + + + + + + + diff --git a/packages/editor/src/components/bubble/BubbleItem.vue b/packages/editor/src/components/bubble/BubbleItem.vue index c00e9fb..ede50f0 100644 --- a/packages/editor/src/components/bubble/BubbleItem.vue +++ b/packages/editor/src/components/bubble/BubbleItem.vue @@ -1,42 +1,76 @@ - - - + + + + + + + + + + + diff --git a/packages/editor/src/extensions/video/VideoView.vue b/packages/editor/src/extensions/video/VideoView.vue index ff577c4..2a879f4 100644 --- a/packages/editor/src/extensions/video/VideoView.vue +++ b/packages/editor/src/extensions/video/VideoView.vue @@ -3,26 +3,6 @@ import type { Node as ProseMirrorNode } from "prosemirror-model"; import type { Decoration } from "prosemirror-view"; import { Editor, NodeViewWrapper, Node } from "@tiptap/vue-3"; import { computed, onMounted, ref } from "vue"; -import { Dropdown as VDropdown } from "floating-vue"; -import BlockCard from "@/components/block/BlockCard.vue"; -import BlockActionButton from "@/components/block/BlockActionButton.vue"; -import BlockActionInput from "@/components/block/BlockActionInput.vue"; -import BlockActionSeparator from "@/components/block/BlockActionSeparator.vue"; -import MdiLinkVariant from "~icons/mdi/link-variant"; -import MdiShare from "~icons/mdi/share"; -import MdiImageSizeSelectActual from "~icons/mdi/image-size-select-actual"; -import MdiImageSizeSelectSmall from "~icons/mdi/image-size-select-small"; -import MdiImageSizeSelectLarge from "~icons/mdi/image-size-select-large"; -import MdiFormatAlignLeft from "~icons/mdi/format-align-left"; -import MdiFormatAlignCenter from "~icons/mdi/format-align-center"; -import MdiFormatAlignRight from "~icons/mdi/format-align-right"; -import MdiFormatAlignJustify from "~icons/mdi/format-align-justify"; -import MdiCogPlay from "~icons/mdi/cog-play"; -import MdiCogPlayOutline from "~icons/mdi/cog-play-outline"; -import MdiPlayCircle from "~icons/mdi/play-circle"; -import MdiPlayCircleOutline from "~icons/mdi/play-circle-outline"; -import MdiMotionPlayOutline from "~icons/mdi/motion-play-outline"; -import MdiMotionPlay from "~icons/mdi/motion-play"; import { i18n } from "@/locales"; const props = defineProps<{ @@ -45,24 +25,6 @@ const src = computed({ }, }); -const width = computed({ - get: () => { - return props.node.attrs.width; - }, - set: (value: string) => { - handleSetSize(value, height.value); - }, -}); - -const height = computed({ - get: () => { - return props.node.attrs.height; - }, - set: (value: string) => { - handleSetSize(width.value, value); - }, -}); - const controls = computed(() => { return props.node.attrs.controls; }); @@ -79,36 +41,6 @@ function handleSetFocus() { props.editor.commands.setNodeSelection(props.getPos()); } -function handleSetSize(width: string, height: string) { - props.updateAttributes({ width, height }); - props.editor.chain().focus().setNodeSelection(props.getPos()).run(); -} - -function handleToggleControls() { - props.updateAttributes({ - controls: props.node.attrs.controls ? null : true, - }); - props.editor.chain().focus().setNodeSelection(props.getPos()).run(); -} - -function handleToggleAutoplay() { - props.updateAttributes({ - autoplay: props.node.attrs.autoplay ? null : true, - }); - props.editor.chain().focus().setNodeSelection(props.getPos()).run(); -} - -function handleToggleLoop() { - props.updateAttributes({ - loop: props.node.attrs.loop ? null : true, - }); - props.editor.chain().focus().setNodeSelection(props.getPos()).run(); -} - -function handleOpenLink() { - window.open(src.value, "_blank"); -} - const inputRef = ref(); onMounted(() => { @@ -120,159 +52,35 @@ onMounted(() => { - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/packages/editor/src/extensions/video/index.ts b/packages/editor/src/extensions/video/index.ts index 78d60c3..175523d 100644 --- a/packages/editor/src/extensions/video/index.ts +++ b/packages/editor/src/extensions/video/index.ts @@ -1,6 +1,7 @@ import type { ExtensionOptions } from "@/types"; import { Editor, + isActive, mergeAttributes, Node, nodeInputRule, @@ -12,13 +13,27 @@ import VideoView from "./VideoView.vue"; import MdiVideo from "~icons/mdi/video"; import ToolboxItem from "@/components/toolbox/ToolboxItem.vue"; import { i18n } from "@/locales"; +import { BlockActionSeparator } from "@/components"; +import BubbleItemVideoSize from "./BubbleItemVideoSize.vue"; +import BubbleItemVideoLink from "./BubbleItemVideoLink.vue"; +import MdiImageSizeSelectActual from "~icons/mdi/image-size-select-actual"; +import MdiImageSizeSelectSmall from "~icons/mdi/image-size-select-small"; +import MdiImageSizeSelectLarge from "~icons/mdi/image-size-select-large"; +import MdiFormatAlignLeft from "~icons/mdi/format-align-left"; +import MdiFormatAlignCenter from "~icons/mdi/format-align-center"; +import MdiFormatAlignRight from "~icons/mdi/format-align-right"; +import MdiFormatAlignJustify from "~icons/mdi/format-align-justify"; +import MdiCogPlay from "~icons/mdi/cog-play"; +import MdiCogPlayOutline from "~icons/mdi/cog-play-outline"; import MdiPlayCircle from "~icons/mdi/play-circle"; import MdiPlayCircleOutline from "~icons/mdi/play-circle-outline"; import MdiMotionPlayOutline from "~icons/mdi/motion-play-outline"; import MdiMotionPlay from "~icons/mdi/motion-play"; -import MdiCogPlay from "~icons/mdi/cog-play"; -import MdiCogPlayOutline from "~icons/mdi/cog-play-outline"; -import { BlockActionSeparator } from "@/components"; +import MdiLinkVariant from "~icons/mdi/link-variant"; +import MdiShare from "~icons/mdi/share"; +import { deleteNode } from "@/utils"; +import MdiDeleteForeverOutline from "~icons/mdi/delete-forever-outline?color=red"; +import type { EditorState } from "prosemirror-state"; declare module "@tiptap/core" { interface Commands { @@ -103,6 +118,17 @@ const Video = Node.create({ }; }, }, + textAlign: { + default: null, + parseHTML: (element) => { + return element.getAttribute("text-align"); + }, + renderHTML: (attributes) => { + return { + "text-align": attributes.textAlign, + }; + }, + }, }; }, @@ -192,8 +218,8 @@ const Video = Node.create({ getBubbleMenu({ editor }: { editor: Editor }) { return { pluginKey: "videoBubbleMenu", - shouldShow: () => { - return editor.isActive(Video.name); + shouldShow: ({ state }: { state: EditorState }) => { + return isActive(state, Video.name); }, items: [ { @@ -282,6 +308,115 @@ const Video = Node.create({ priority: 40, component: markRaw(BlockActionSeparator), }, + { + priority: 50, + component: markRaw(BubbleItemVideoSize), + }, + { + priority: 60, + component: markRaw(BlockActionSeparator), + }, + { + priority: 70, + props: { + isActive: () => + editor.getAttributes(Video.name).width === "25%", + icon: markRaw(MdiImageSizeSelectSmall), + action: () => handleSetSize(editor, "25%", "auto"), + title: i18n.global.t("editor.extensions.video.small_size"), + }, + }, + { + priority: 80, + props: { + isActive: () => + editor.getAttributes(Video.name).width === "50%", + icon: markRaw(MdiImageSizeSelectLarge), + action: () => handleSetSize(editor, "50%", "auto"), + title: i18n.global.t("editor.extensions.video.medium_size"), + }, + }, + { + priority: 90, + props: { + isActive: () => + editor.getAttributes(Video.name).width === "100%", + icon: markRaw(MdiImageSizeSelectActual), + action: () => handleSetSize(editor, "100%", "auto"), + title: i18n.global.t("editor.extensions.video.large_size"), + }, + }, + { + priority: 100, + component: markRaw(BlockActionSeparator), + }, + { + priority: 110, + props: { + isActive: () => editor.isActive({ textAlign: "left" }), + icon: markRaw(MdiFormatAlignLeft), + action: () => handleSetTextAlign(editor, "left"), + }, + }, + { + priority: 120, + props: { + isActive: () => editor.isActive({ textAlign: "center" }), + icon: markRaw(MdiFormatAlignCenter), + action: () => handleSetTextAlign(editor, "center"), + }, + }, + { + priority: 130, + props: { + isActive: () => editor.isActive({ textAlign: "right" }), + icon: markRaw(MdiFormatAlignRight), + action: () => handleSetTextAlign(editor, "right"), + }, + }, + { + priority: 140, + props: { + isActive: () => editor.isActive({ textAlign: "justify" }), + icon: markRaw(MdiFormatAlignJustify), + action: () => handleSetTextAlign(editor, "justify"), + }, + }, + { + priority: 150, + component: markRaw(BlockActionSeparator), + }, + { + priority: 160, + props: { + icon: markRaw(MdiLinkVariant), + title: i18n.global.t("editor.common.button.edit_link"), + action: () => { + return markRaw(BubbleItemVideoLink); + }, + }, + }, + { + priority: 170, + props: { + icon: markRaw(MdiShare), + title: i18n.global.t("editor.common.tooltip.open_link"), + action: () => + window.open(editor.getAttributes(Video.name).src, "_blank"), + }, + }, + { + priority: 180, + component: markRaw(BlockActionSeparator), + }, + { + priority: 190, + props: { + icon: markRaw(MdiDeleteForeverOutline), + title: i18n.global.t("editor.common.button.delete"), + action: ({ editor }) => deleteNode(Video.name, editor), + }, + }, ], }; }, @@ -289,4 +424,20 @@ const Video = Node.create({ }, }); +const handleSetSize = (editor: Editor, width: string, height: string) => { + editor + .chain() + .updateAttributes(Video.name, { width, height }) + .setNodeSelection(editor.state.selection.from) + .focus() + .run(); +}; + +const handleSetTextAlign = ( + editor: Editor, + align: "left" | "center" | "right" | "justify" +) => { + editor.chain().focus().setTextAlign(align).run(); +}; + export default Video; diff --git a/packages/editor/src/types/index.ts b/packages/editor/src/types/index.ts index 441e6af..10df984 100644 --- a/packages/editor/src/types/index.ts +++ b/packages/editor/src/types/index.ts @@ -2,6 +2,7 @@ import type { Editor, Range } from "@tiptap/core"; import type { EditorState } from "prosemirror-state"; import type { EditorView } from "prosemirror-view"; import type { Component } from "vue"; +import default from '../../vite.config'; export interface ToolbarItem { priority: number; component: Component; @@ -19,17 +20,18 @@ export interface ToolbarItem { interface BubbleMenuProps { pluginKey?: string; editor?: Editor; - updateDelay?: number; shouldShow: (props: { editor: Editor; - view: EditorView; - state: EditorState; - oldState?: EditorState | undefined; - from: number; - to: number; + node?: HTMLElement; + view?: EditorView; + state?: EditorState; + oldState?: EditorState; + from?: number; + to?: number; }) => boolean; tippyOptions?: Record; - isDeleteItem?: boolean; + getRenderContainer?: (node: HTMLElement) => HTMLElement; + defaultAnimation?: boolean; } export interface NodeBubbleMenu extends BubbleMenuProps { @@ -44,6 +46,7 @@ export interface BubbleItem { isActive: ({ editor }: { editor: Editor }) => boolean; visible?: ({ editor }: { editor: Editor }) => boolean; icon?: Component; + iconStyle?: string; title?: string; action?: ({ editor }: { editor: Editor }) => any; }; From f7d4c08511a2f42f12620d4e4783f95beb4bdebb Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Thu, 31 Aug 2023 18:28:06 +0800 Subject: [PATCH 05/11] wip: refactor the bubble menu. --- .../src/components/bubble/BubbleMenuPlugin.ts | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/components/bubble/BubbleMenuPlugin.ts b/packages/editor/src/components/bubble/BubbleMenuPlugin.ts index 7c09a49..93737ce 100644 --- a/packages/editor/src/components/bubble/BubbleMenuPlugin.ts +++ b/packages/editor/src/components/bubble/BubbleMenuPlugin.ts @@ -14,6 +14,7 @@ export interface BubbleMenuPluginProps { editor: Editor; element: HTMLElement; tippyOptions?: Partial; + updateDelay?: number; shouldShow?: | ((props: { editor: Editor; @@ -46,6 +47,10 @@ export class BubbleMenuView { public tippy: Instance | undefined; + private updateDebounceTimer: number | undefined; + + public updateDelay: number; + public tippyOptions?: Partial; public getRenderContainer?: BubbleMenuPluginProps["getRenderContainer"]; @@ -79,6 +84,7 @@ export class BubbleMenuView { element, view, tippyOptions = {}, + updateDelay = 250, shouldShow, getRenderContainer, defaultAnimation = true, @@ -86,6 +92,7 @@ export class BubbleMenuView { this.editor = editor; this.element = element; this.view = view; + this.updateDelay = updateDelay; this.getRenderContainer = getRenderContainer; this.defaultAnimation = defaultAnimation; @@ -185,7 +192,26 @@ export class BubbleMenuView { } } - update(view: EditorView, oldState?: EditorState) { + update = (view: EditorView, oldState?: EditorState) => { + const { state, composing } = view; + const { doc, selection } = state; + const isSame = + oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); + + if (composing || isSame) { + return; + } + + if (this.updateDebounceTimer) { + clearTimeout(this.updateDebounceTimer); + } + + this.updateDebounceTimer = window.setTimeout(() => { + this.updateHandler(view, oldState); + }, this.updateDelay); + }; + + updateHandler(view: EditorView, oldState?: EditorState) { const { state, composing } = view; const { doc, selection } = state; const isSame = From e8c4429a4f7fe6faa0495f031ef9fb10d3a9dba6 Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Fri, 1 Sep 2023 16:54:07 +0800 Subject: [PATCH 06/11] wip: refactor the bubble menu. --- .../src/components/EditorBubbleMenu.vue | 2 - .../src/components/bubble/BubbleMenu.vue | 2 +- .../src/components/bubble/BubbleMenuPlugin.ts | 48 +-- .../src/components/bubble/TextBubbleMenu.ts | 22 +- .../code-block/CodeBlockViewRenderer.vue | 2 +- .../src/extensions/code-block/code-block.ts | 2 +- .../extensions/image/BubbleItemImageAlt.vue | 37 +++ .../extensions/image/BubbleItemImageLink.vue | 37 +++ .../extensions/image/BubbleItemImageSize.vue | 170 ++++++++++ .../editor/src/extensions/image/ImageView.vue | 308 ++---------------- packages/editor/src/extensions/image/index.ts | 114 ++++++- 11 files changed, 417 insertions(+), 327 deletions(-) create mode 100644 packages/editor/src/extensions/image/BubbleItemImageAlt.vue create mode 100644 packages/editor/src/extensions/image/BubbleItemImageLink.vue create mode 100644 packages/editor/src/extensions/image/BubbleItemImageSize.vue diff --git a/packages/editor/src/components/EditorBubbleMenu.vue b/packages/editor/src/components/EditorBubbleMenu.vue index 8b6a37e..731e5bf 100644 --- a/packages/editor/src/components/EditorBubbleMenu.vue +++ b/packages/editor/src/components/EditorBubbleMenu.vue @@ -7,7 +7,6 @@ import BubbleItem from "@/components/bubble/BubbleItem.vue"; import { defaultTextBubbleMenu } from "@/components/bubble/TextBubbleMenu"; import type { EditorView } from "prosemirror-view"; import type { EditorState } from "prosemirror-state"; -import type { BubbleMenuPluginProps } from "./bubble/BubbleMenuPlugin"; const props = defineProps({ editor: { @@ -69,7 +68,6 @@ const shouldShow = ( :editor="editor" :tippy-options="{ maxWidth: '100%', - moveTransition: 'transform 0.2s ease-out', ...bubbleMenu.tippyOptions, }" :get-render-container="bubbleMenu.getRenderContainer" diff --git a/packages/editor/src/components/bubble/BubbleMenu.vue b/packages/editor/src/components/bubble/BubbleMenu.vue index f8cdb88..dd5e3b6 100644 --- a/packages/editor/src/components/bubble/BubbleMenu.vue +++ b/packages/editor/src/components/bubble/BubbleMenu.vue @@ -37,7 +37,7 @@ const props = defineProps({ defaultAnimation: { type: Boolean as PropType, - default: false, + default: true, }, }); diff --git a/packages/editor/src/components/bubble/BubbleMenuPlugin.ts b/packages/editor/src/components/bubble/BubbleMenuPlugin.ts index 93737ce..13b0acf 100644 --- a/packages/editor/src/components/bubble/BubbleMenuPlugin.ts +++ b/packages/editor/src/components/bubble/BubbleMenuPlugin.ts @@ -47,10 +47,6 @@ export class BubbleMenuView { public tippy: Instance | undefined; - private updateDebounceTimer: number | undefined; - - public updateDelay: number; - public tippyOptions?: Partial; public getRenderContainer?: BubbleMenuPluginProps["getRenderContainer"]; @@ -63,16 +59,16 @@ export class BubbleMenuView { from, to, }) => { - const { doc, selection } = state; + const { doc, selection } = state as EditorState; const { empty } = selection; // Sometime check for `empty` is not enough. // Doubleclick an empty paragraph returns a node size of 2. // So we check also for an empty text size. const isEmptyTextBlock = - !doc.textBetween(from, to).length && isTextSelection(state.selection); + !doc.textBetween(from || 0, to || 0).length && isTextSelection(selection); - if (!view.hasFocus() || empty || isEmptyTextBlock) { + if (!(view as EditorView).hasFocus() || empty || isEmptyTextBlock) { return false; } @@ -84,7 +80,6 @@ export class BubbleMenuView { element, view, tippyOptions = {}, - updateDelay = 250, shouldShow, getRenderContainer, defaultAnimation = true, @@ -92,7 +87,6 @@ export class BubbleMenuView { this.editor = editor; this.element = element; this.view = view; - this.updateDelay = updateDelay; this.getRenderContainer = getRenderContainer; this.defaultAnimation = defaultAnimation; @@ -169,7 +163,6 @@ export class BubbleMenuView { ...Object.assign( { zIndex: 999, - duration: 200, ...(this.defaultAnimation ? { animation: "shift-toward-subtle", @@ -192,26 +185,7 @@ export class BubbleMenuView { } } - update = (view: EditorView, oldState?: EditorState) => { - const { state, composing } = view; - const { doc, selection } = state; - const isSame = - oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); - - if (composing || isSame) { - return; - } - - if (this.updateDebounceTimer) { - clearTimeout(this.updateDebounceTimer); - } - - this.updateDebounceTimer = window.setTimeout(() => { - this.updateHandler(view, oldState); - }, this.updateDelay); - }; - - updateHandler(view: EditorView, oldState?: EditorState) { + update(view: EditorView, oldState?: EditorState) { const { state, composing } = view; const { doc, selection } = state; const isSame = @@ -228,7 +202,9 @@ export class BubbleMenuView { const cursorAt = selection.$anchor.pos; const from = Math.min(...ranges.map((range) => range.$from.pos)); const to = Math.max(...ranges.map((range) => range.$to.pos)); - const placement = isNodeSelection(selection) + const placement = this.tippyOptions?.placement + ? this.tippyOptions?.placement + : isNodeSelection(selection) ? "top" : Math.abs(cursorAt - to) <= Math.abs(cursorAt - from) ? "bottom-start" @@ -237,8 +213,6 @@ export class BubbleMenuView { const nodeDOM = view.nodeDOM(from) as HTMLElement; const node = nodeDOM || domAtPos; - console.log(selection); - const shouldShow = this.editor.isEditable && this.shouldShow?.({ @@ -262,7 +236,8 @@ export class BubbleMenuView { instance.popperInstance && instance.popperInstance.state ); - const offsetX = this.tippyOptions?.offset?.[0] ?? 0; + const offset = this.tippyOptions?.offset as [number, number]; + const offsetX = offset?.[0] ?? 0; const offsetY = otherBubbleMenus.length ? otherBubbleMenus.reduce((prev, instance, currentIndex, array) => { const prevY = array[currentIndex - 1] @@ -281,8 +256,7 @@ export class BubbleMenuView { return prev; }, 0) - : this.tippyOptions?.offset?.[1] ?? 10; - + : offset?.[1] ?? 10; this.tippy?.setProps({ offset: [offsetX, offsetY], placement, @@ -319,7 +293,7 @@ export class BubbleMenuView { (instance) => instance?.id === this.tippy?.id ); if (idx < 0) { - ACTIVE_BUBBLE_MENUS.push(this.tippy); + ACTIVE_BUBBLE_MENUS.push(this.tippy as Instance); } }; diff --git a/packages/editor/src/components/bubble/TextBubbleMenu.ts b/packages/editor/src/components/bubble/TextBubbleMenu.ts index 291f8df..53f4a93 100644 --- a/packages/editor/src/components/bubble/TextBubbleMenu.ts +++ b/packages/editor/src/components/bubble/TextBubbleMenu.ts @@ -20,37 +20,43 @@ import MdiFormatAlignCenter from "~icons/mdi/format-align-center"; import MdiFormatAlignRight from "~icons/mdi/format-align-right"; import MdiFormatAlignJustify from "~icons/mdi/format-align-justify"; import MdiFormatUnderline from "~icons/mdi/format-underline"; -import { isTextSelection } from "@tiptap/core"; +import { isActive, isTextSelection } from "@tiptap/core"; +import type { EditorState } from "prosemirror-state"; +import type { EditorView } from "prosemirror-view"; const OTHER_BUBBLE_MENU_TYPES = ["audio", "video", "image", "iframe"]; export const defaultTextBubbleMenu: NodeBubbleMenu = { pluginKey: "textBubbleMenu", - shouldShow: ({ editor, view, state, from, to }) => { - const { empty } = editor.state.selection; + shouldShow: ({ view, state, from, to }) => { + const { doc, selection } = state as EditorState; + const { empty } = selection; if (empty) { return false; } - if (OTHER_BUBBLE_MENU_TYPES.some((type) => editor.isActive(type))) { + if ( + OTHER_BUBBLE_MENU_TYPES.some((type) => + isActive(state as EditorState, type) + ) + ) { return false; } - const { doc } = editor.state; - const isEmptyTextBlock = - !doc.textBetween(from, to).length && isTextSelection(state.selection); + !doc.textBetween(from || 0, to || 0).length && isTextSelection(selection); if (isEmptyTextBlock) { return false; } - const hasEditorFocus = view.hasFocus(); + const hasEditorFocus = (view as EditorView).hasFocus(); if (!hasEditorFocus) { return false; } return true; }, + defaultAnimation: false, items: [ { priority: 20, diff --git a/packages/editor/src/extensions/code-block/CodeBlockViewRenderer.vue b/packages/editor/src/extensions/code-block/CodeBlockViewRenderer.vue index eb536ee..8fcfb94 100644 --- a/packages/editor/src/extensions/code-block/CodeBlockViewRenderer.vue +++ b/packages/editor/src/extensions/code-block/CodeBlockViewRenderer.vue @@ -30,7 +30,7 @@ const selectedLanguage = computed({ }); - + +import { i18n } from "@/locales"; +import type { Editor } from "@tiptap/vue-3"; +import { computed, type Component } from "vue"; +import Image from "./index"; + +const props = defineProps<{ + editor: Editor; + isActive: ({ editor }: { editor: Editor }) => boolean; + visible?: ({ editor }: { editor: Editor }) => boolean; + icon?: Component; + title?: string; + action?: ({ editor }: { editor: Editor }) => void; +}>(); + +const alt = computed({ + get: () => { + return props.editor.getAttributes(Image.name).alt; + }, + set: (alt: string) => { + props.editor + .chain() + .updateAttributes(Image.name, { alt: alt }) + .setNodeSelection(props.editor.state.selection.from) + .focus() + .run(); + }, +}); + + + + + diff --git a/packages/editor/src/extensions/image/BubbleItemImageLink.vue b/packages/editor/src/extensions/image/BubbleItemImageLink.vue new file mode 100644 index 0000000..bd71386 --- /dev/null +++ b/packages/editor/src/extensions/image/BubbleItemImageLink.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/packages/editor/src/extensions/image/BubbleItemImageSize.vue b/packages/editor/src/extensions/image/BubbleItemImageSize.vue new file mode 100644 index 0000000..bf732c3 --- /dev/null +++ b/packages/editor/src/extensions/image/BubbleItemImageSize.vue @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/editor/src/extensions/image/ImageView.vue b/packages/editor/src/extensions/image/ImageView.vue index 047033a..468cfd0 100644 --- a/packages/editor/src/extensions/image/ImageView.vue +++ b/packages/editor/src/extensions/image/ImageView.vue @@ -1,27 +1,10 @@