diff --git a/packages/components/src/__internal__/components/icon.tsx b/packages/components/src/__internal__/components/icon.tsx index 5b0a82e4ab8..a5c0145a332 100644 --- a/packages/components/src/__internal__/components/icon.tsx +++ b/packages/components/src/__internal__/components/icon.tsx @@ -6,12 +6,14 @@ h type IconProps = { icon?: string | null class?: string + onClick?: (event: PointerEvent) => void } -export function Icon({ icon, class: className }: IconProps) { +export function Icon({ icon, class: className, onClick }: IconProps) { return ( { if (el && icon) { ;(el as HTMLElement).innerHTML = icon.trim() @@ -30,4 +32,8 @@ Icon.props = { type: String, required: false, }, + onClick: { + type: Function, + required: false, + }, } diff --git a/packages/components/src/code-block/view/node-view.ts b/packages/components/src/code-block/view/node-view.ts index 0a544e60f03..42567d601c6 100644 --- a/packages/components/src/code-block/view/node-view.ts +++ b/packages/components/src/code-block/view/node-view.ts @@ -11,7 +11,7 @@ import { Compartment, EditorState } from '@codemirror/state' import type { Line, SelectionRange } from '@codemirror/state' import { exitCode } from '@milkdown/prose/commands' import { TextSelection } from '@milkdown/prose/state' -import { createApp, ref, watchEffect, type WatchHandle } from 'vue' +import { createApp, ref, watchEffect, type App, type WatchHandle } from 'vue' import type { CodeBlockConfig } from '../config' import type { LanguageLoader } from './loader' @@ -20,6 +20,7 @@ import { CodeBlock } from './components/code-block' export class CodeMirrorBlock implements NodeView { dom: HTMLElement cm: CodeMirror + app: App readonly = ref(false) selected = ref(false) @@ -55,7 +56,9 @@ export class CodeMirrorBlock implements NodeView { ], }) - this.dom = this.createDom() + this.app = this.createApp() + + this.dom = this.createDom(this.app) this.disposeSelectedWatcher = watchEffect(() => { const isSelected = this.selected.value @@ -93,11 +96,8 @@ export class CodeMirrorBlock implements NodeView { } } - private createDom() { - const dom = document.createElement('div') - dom.className = 'milkdown-code-block' - this.text.value = this.node.textContent - const app = createApp(CodeBlock, { + private createApp = () => { + return createApp(CodeBlock, { text: this.text, selected: this.selected, readonly: this.readonly, @@ -107,6 +107,12 @@ export class CodeMirrorBlock implements NodeView { setLanguage: this.setLanguage, config: this.config, }) + } + + private createDom(app: App) { + const dom = document.createElement('div') + dom.className = 'milkdown-code-block' + this.text.value = this.node.textContent app.mount(dom) return dom } @@ -248,6 +254,7 @@ export class CodeMirrorBlock implements NodeView { } destroy() { + this.app.unmount() this.cm.destroy() this.disposeSelectedWatcher() } diff --git a/packages/components/src/link-tooltip/edit/component.tsx b/packages/components/src/link-tooltip/edit/component.tsx new file mode 100644 index 00000000000..e38986db30b --- /dev/null +++ b/packages/components/src/link-tooltip/edit/component.tsx @@ -0,0 +1,79 @@ +import { defineComponent, ref, watch, type Ref, h } from 'vue' +import type { LinkTooltipConfig } from '../slices' +import { Icon } from '../../__internal__/components/icon' + +h + +type EditLinkProps = { + config: Ref + src: Ref + onConfirm: (href: string) => void + onCancel: () => void +} + +export const EditLink = defineComponent({ + props: { + config: { + type: Object, + required: true, + }, + src: { + type: Object, + required: true, + }, + onConfirm: { + type: Function, + required: true, + }, + onCancel: { + type: Function, + required: true, + }, + }, + setup({ config, src, onConfirm, onCancel }) { + const link = ref(src) + + watch(src, (value) => { + link.value = value + }) + + const onConfirmEdit = () => { + onConfirm(link.value) + } + + const onKeydown = (e: KeyboardEvent) => { + e.stopPropagation() + if (e.key === 'Enter') { + e.preventDefault() + onConfirmEdit() + } + if (e.key === 'Escape') { + e.preventDefault() + onCancel() + } + } + + return () => { + return ( + + ) + } + }, +}) diff --git a/packages/components/src/link-tooltip/edit/edit-component.ts b/packages/components/src/link-tooltip/edit/edit-component.ts deleted file mode 100644 index 5e96590a979..00000000000 --- a/packages/components/src/link-tooltip/edit/edit-component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { Component } from 'atomico' -import { c, html, useEffect, useRef, useState } from 'atomico' -import clsx from 'clsx' -import type { LinkTooltipConfig } from '../slices' - -export interface LinkEditProps { - config: LinkTooltipConfig - src: string - onConfirm: (href: string) => void - onCancel: () => void -} - -export const linkEditComponent: Component = ({ - src, - onConfirm, - onCancel, - config, -}) => { - const linkInput = useRef() - const [link, setLink] = useState(src) - - useEffect(() => { - setLink(src ?? '') - }, [src]) - - const onConfirmEdit = () => { - onConfirm?.(linkInput.current?.value ?? '') - } - - const onKeydown = (e: KeyboardEvent) => { - e.stopPropagation() - if (e.key === 'Enter') { - onConfirm?.(linkInput.current?.value ?? '') - e.preventDefault() - } - if (e.key === 'Escape') { - onCancel?.() - e.preventDefault() - } - } - - return html` - - - - ` -} - -linkEditComponent.props = { - config: Object, - src: String, - onConfirm: Function, - onCancel: Function, -} - -export const LinkEditElement = c(linkEditComponent) diff --git a/packages/components/src/link-tooltip/edit/edit-configure.ts b/packages/components/src/link-tooltip/edit/edit-configure.ts index de43c456c59..18761e34e9a 100644 --- a/packages/components/src/link-tooltip/edit/edit-configure.ts +++ b/packages/components/src/link-tooltip/edit/edit-configure.ts @@ -1,11 +1,8 @@ import type { Ctx } from '@milkdown/ctx' import { linkTooltipAPI } from '../slices' import { linkEditTooltip } from '../tooltips' -import { defIfNotExists } from '../../__internal__/helper' -import { LinkEditElement } from './edit-component' import { LinkEditTooltip } from './edit-view' -defIfNotExists('milkdown-link-edit', LinkEditElement) export function configureLinkEditTooltip(ctx: Ctx) { let linkEditTooltipView: LinkEditTooltip | null diff --git a/packages/components/src/link-tooltip/edit/edit-view.ts b/packages/components/src/link-tooltip/edit/edit-view.ts index 2e717700fc1..fa940210c1c 100644 --- a/packages/components/src/link-tooltip/edit/edit-view.ts +++ b/packages/components/src/link-tooltip/edit/edit-view.ts @@ -7,8 +7,13 @@ import { TooltipProvider } from '@milkdown/plugin-tooltip' import { editorViewCtx } from '@milkdown/core' import { linkSchema } from '@milkdown/preset-commonmark' import { posToDOMRect } from '@milkdown/prose' -import { linkTooltipConfig, linkTooltipState } from '../slices' -import { LinkEditElement } from './edit-component' +import { + linkTooltipConfig, + linkTooltipState, + type LinkTooltipConfig, +} from '../slices' +import { createApp, ref, type App, type Ref } from 'vue' +import { EditLink } from './component' interface Data { from: number @@ -23,28 +28,43 @@ const defaultData: Data = { } export class LinkEditTooltip implements PluginView { - #content = new LinkEditElement() + #content: HTMLElement #provider: TooltipProvider #data: Data = { ...defaultData } + #app: App + #config: Ref + #src = ref('') constructor( readonly ctx: Ctx, view: EditorView ) { + this.#config = ref(this.ctx.get(linkTooltipConfig.key)) + + const content = document.createElement('div') + content.className = 'milkdown-link-edit' + + const app = createApp(EditLink, { + config: this.#config, + src: this.#src, + onConfirm: this.#confirmEdit, + onCancel: this.#reset, + }) + app.mount(content) + this.#app = app + + this.#content = content this.#provider = new TooltipProvider({ - content: this.#content, + content, debounce: 0, shouldShow: () => false, }) this.#provider.onHide = () => { - this.#content.update().catch((e) => { - throw e + requestAnimationFrame(() => { + view.dom.focus({ preventScroll: true }) }) - view.dom.focus({ preventScroll: true }) } this.#provider.update(view) - this.#content.onConfirm = this.#confirmEdit - this.#content.onCancel = this.#reset } #reset = () => { @@ -76,14 +96,13 @@ export class LinkEditTooltip implements PluginView { #enterEditMode = (value: string, from: number, to: number) => { const config = this.ctx.get(linkTooltipConfig.key) - this.#content.config = config - this.#content.src = value + this.#config.value = config + this.#src.value = value this.ctx.update(linkTooltipState.key, (state) => ({ ...state, mode: 'edit' as const, })) const view = this.ctx.get(editorViewCtx) - // this.#setRect(posToDOMRect(view, from, to)) view.dispatch( view.state.tr.setSelection(TextSelection.create(view.state.doc, from, to)) ) @@ -106,6 +125,7 @@ export class LinkEditTooltip implements PluginView { } destroy = () => { + this.#app.unmount() this.#provider.destroy() this.#content.remove() } diff --git a/packages/components/src/link-tooltip/preview/component.tsx b/packages/components/src/link-tooltip/preview/component.tsx new file mode 100644 index 00000000000..3bdde225f89 --- /dev/null +++ b/packages/components/src/link-tooltip/preview/component.tsx @@ -0,0 +1,80 @@ +import { defineComponent, type Ref, h } from 'vue' +import type { LinkTooltipConfig } from '../slices' +import { Icon } from '../../__internal__/components/icon' + +type PreviewLinkProps = { + config: Ref + src: Ref + onEdit: Ref<() => void> + onRemove: Ref<() => void> +} + +h + +export const PreviewLink = defineComponent({ + props: { + config: { + type: Object, + required: true, + }, + src: { + type: Object, + required: true, + }, + onEdit: { + type: Object, + required: true, + }, + onRemove: { + type: Object, + required: true, + }, + }, + setup({ config, src, onEdit, onRemove }) { + const onClickEditButton = (e: Event) => { + e.preventDefault() + e.stopPropagation() + onEdit.value() + } + + const onClickRemoveButton = (e: Event) => { + e.preventDefault() + e.stopPropagation() + onRemove.value() + } + + const onClickPreview = (e: Event) => { + e.preventDefault() + const link = src.value + if (navigator.clipboard && link) { + navigator.clipboard + .writeText(link) + .then(() => { + config.value.onCopyLink(link) + }) + .catch((e) => console.error(e)) + } + } + + return () => { + return ( + + ) + } + }, +}) diff --git a/packages/components/src/link-tooltip/preview/preview-component.ts b/packages/components/src/link-tooltip/preview/preview-component.ts deleted file mode 100644 index f502f45639d..00000000000 --- a/packages/components/src/link-tooltip/preview/preview-component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { Component } from 'atomico' -import { c, html } from 'atomico' -import type { LinkTooltipConfig } from '../slices' - -export interface LinkPreviewProps { - config: LinkTooltipConfig - src: string - onEdit: () => void - onRemove: () => void -} - -export const linkPreviewComponent: Component = ({ - config, - src, - onEdit, - onRemove, -}) => { - const onClickEditButton = (e: MouseEvent) => { - e.stopPropagation() - e.preventDefault() - onEdit?.() - } - - const onClickRemoveButton = (e: MouseEvent) => { - e.stopPropagation() - e.preventDefault() - onRemove?.() - } - - const onClickPreview = (e: MouseEvent) => { - e.preventDefault() - if (navigator.clipboard && src) { - navigator.clipboard - .writeText(src) - .then(() => { - config?.onCopyLink(src) - }) - .catch((e) => { - throw e - }) - } - } - - return html` - - - - ` -} - -linkPreviewComponent.props = { - config: Object, - src: String, - onEdit: Function, - onRemove: Function, -} - -export const LinkPreviewElement = c(linkPreviewComponent) diff --git a/packages/components/src/link-tooltip/preview/preview-configure.ts b/packages/components/src/link-tooltip/preview/preview-configure.ts index 786a37ae273..5f89e79f326 100644 --- a/packages/components/src/link-tooltip/preview/preview-configure.ts +++ b/packages/components/src/link-tooltip/preview/preview-configure.ts @@ -5,11 +5,8 @@ import { posToDOMRect } from '@milkdown/prose' import { linkTooltipState } from '../slices' import { findMarkPosition, shouldShowPreviewWhenHover } from '../utils' import { linkPreviewTooltip } from '../tooltips' -import { defIfNotExists } from '../../__internal__/helper' import { LinkPreviewTooltip } from './preview-view' -import { LinkPreviewElement } from './preview-component' -defIfNotExists('milkdown-link-preview', LinkPreviewElement) export function configureLinkPreviewTooltip(ctx: Ctx) { let linkPreviewTooltipView: LinkPreviewTooltip | null diff --git a/packages/components/src/link-tooltip/preview/preview-view.ts b/packages/components/src/link-tooltip/preview/preview-view.ts index d2f3f548c7c..0bbc6dd6f31 100644 --- a/packages/components/src/link-tooltip/preview/preview-view.ts +++ b/packages/components/src/link-tooltip/preview/preview-view.ts @@ -3,14 +3,20 @@ import type { EditorView } from '@milkdown/prose/view' import type { Mark } from '@milkdown/prose/model' import { TooltipProvider } from '@milkdown/plugin-tooltip' import type { Ctx, Slice } from '@milkdown/ctx' -import type { LinkToolTipState } from '../slices' +import type { LinkTooltipConfig, LinkToolTipState } from '../slices' import { linkTooltipAPI, linkTooltipConfig, linkTooltipState } from '../slices' -import { LinkPreviewElement } from './preview-component' +import { createApp, ref, type App, type Ref } from 'vue' +import { PreviewLink } from './component' export class LinkPreviewTooltip implements PluginView { - #content = new LinkPreviewElement() + #content: HTMLElement #provider: TooltipProvider #slice: Slice = this.ctx.use(linkTooltipState.key) + #config: Ref + #src = ref('') + #onEdit = ref(() => {}) + #onRemove = ref(() => {}) + #app: App #hovering = false @@ -18,6 +24,17 @@ export class LinkPreviewTooltip implements PluginView { readonly ctx: Ctx, view: EditorView ) { + this.#config = ref(this.ctx.get(linkTooltipConfig.key)) + this.#app = createApp(PreviewLink, { + config: this.#config, + src: this.#src, + onEdit: this.#onEdit, + onRemove: this.#onRemove, + }) + this.#content = document.createElement('div') + this.#content.className = 'milkdown-link-preview' + this.#app.mount(this.#content) + this.#provider = new TooltipProvider({ debounce: 0, content: this.#content, @@ -47,12 +64,12 @@ export class LinkPreviewTooltip implements PluginView { } show = (mark: Mark, from: number, to: number, rect: DOMRect) => { - this.#content.config = this.ctx.get(linkTooltipConfig.key) - this.#content.src = mark.attrs.href - this.#content.onEdit = () => { + this.#config.value = this.ctx.get(linkTooltipConfig.key) + this.#src.value = mark.attrs.href + this.#onEdit.value = () => { this.ctx.get(linkTooltipAPI.key).editLink(mark, from, to) } - this.#content.onRemove = () => { + this.#onRemove.value = () => { this.ctx.get(linkTooltipAPI.key).removeLink(from, to) this.#hide() } @@ -73,6 +90,7 @@ export class LinkPreviewTooltip implements PluginView { update = () => {} destroy = () => { + this.#app.unmount() this.#slice.off(this.#onStateChange) this.#provider.destroy() this.#content.remove() diff --git a/packages/components/src/link-tooltip/slices.ts b/packages/components/src/link-tooltip/slices.ts index d4e71b69802..a751d1e4de7 100644 --- a/packages/components/src/link-tooltip/slices.ts +++ b/packages/components/src/link-tooltip/slices.ts @@ -1,6 +1,5 @@ import { $ctx } from '@milkdown/utils' import type { Mark } from '@milkdown/prose/model' -import { html } from 'atomico' import { withMeta } from '../__internal__/meta' export interface LinkToolTipState { @@ -38,10 +37,10 @@ withMeta(linkTooltipState, { }) export interface LinkTooltipConfig { - linkIcon: () => ReturnType - editButton: () => ReturnType - confirmButton: () => ReturnType - removeButton: () => ReturnType + linkIcon: () => string + editButton: () => string + confirmButton: () => string + removeButton: () => string onCopyLink: (link: string) => void inputPlaceholder: string } @@ -50,7 +49,7 @@ const defaultConfig: LinkTooltipConfig = { linkIcon: () => '🔗', editButton: () => '✎', removeButton: () => '⌫', - confirmButton: () => html`Confirm ⏎`, + confirmButton: () => 'Confirm ⏎', onCopyLink: () => {}, inputPlaceholder: 'Paste link...', } diff --git a/packages/crepe/src/feature/link-tooltip/index.ts b/packages/crepe/src/feature/link-tooltip/index.ts index 49627d04674..0190f54970d 100644 --- a/packages/crepe/src/feature/link-tooltip/index.ts +++ b/packages/crepe/src/feature/link-tooltip/index.ts @@ -4,12 +4,7 @@ import { linkTooltipPlugin, } from '@milkdown/kit/component/link-tooltip' import type { DefineFeature, Icon } from '../shared' -import { - legacyConfirmIcon, - copyIcon, - removeIcon, - legacyEditIcon, -} from '../../icons' +import { copyIcon, editIcon, removeIcon, confirmIcon } from '../../icons' interface LinkTooltipConfig { linkIcon: Icon @@ -32,9 +27,9 @@ export const defineFeature: DefineFeature = ( ctx.update(linkTooltipConfig.key, (prev) => ({ ...prev, linkIcon: config?.linkIcon ?? (() => copyIcon), - editButton: config?.editButton ?? (() => legacyEditIcon), + editButton: config?.editButton ?? (() => editIcon), removeButton: config?.removeButton ?? (() => removeIcon), - confirmButton: config?.confirmButton ?? (() => legacyConfirmIcon), + confirmButton: config?.confirmButton ?? (() => confirmIcon), inputPlaceholder: config?.inputPlaceholder ?? 'Paste link...', onCopyLink: config?.onCopyLink ?? (() => {}), })) diff --git a/packages/crepe/src/feature/shared.ts b/packages/crepe/src/feature/shared.ts index 9df7ef1364d..4308a6dc1af 100644 --- a/packages/crepe/src/feature/shared.ts +++ b/packages/crepe/src/feature/shared.ts @@ -5,4 +5,4 @@ export type DefineFeature = ( config?: Config ) => void | Promise -export type Icon = () => string | undefined +export type Icon = () => string diff --git a/packages/crepe/src/feature/table/index.ts b/packages/crepe/src/feature/table/index.ts index 757e2dbca40..41fb0475038 100644 --- a/packages/crepe/src/feature/table/index.ts +++ b/packages/crepe/src/feature/table/index.ts @@ -9,7 +9,7 @@ import { alignRightIcon, dragHandleIcon, plusIcon, - removeIcon, + legacyRemoveIcon, } from '../../icons' interface TableConfig { @@ -41,9 +41,9 @@ export const defineFeature: DefineFeature = ( case 'add_col': return config?.addColIcon?.() ?? plusIcon case 'delete_row': - return config?.deleteRowIcon?.() ?? removeIcon + return config?.deleteRowIcon?.() ?? legacyRemoveIcon case 'delete_col': - return config?.deleteColIcon?.() ?? removeIcon + return config?.deleteColIcon?.() ?? legacyRemoveIcon case 'align_col_left': return config?.alignLeftIcon?.() ?? alignLeftIcon case 'align_col_center': diff --git a/packages/crepe/src/icons/copy.ts b/packages/crepe/src/icons/copy.ts index e1f1bc4f561..d11b4b2d88a 100644 --- a/packages/crepe/src/icons/copy.ts +++ b/packages/crepe/src/icons/copy.ts @@ -1,6 +1,4 @@ -import { html } from 'atomico' - -export const copyIcon = html` +export const copyIcon = ` ` - -export const legacyEditIcon = html` - - - - - - - - - - -` diff --git a/packages/crepe/src/icons/remove.ts b/packages/crepe/src/icons/remove.ts index edd0db07d84..63c4db6ca7c 100644 --- a/packages/crepe/src/icons/remove.ts +++ b/packages/crepe/src/icons/remove.ts @@ -1,6 +1,19 @@ import { html } from 'atomico' -export const removeIcon = html` +export const removeIcon = ` + + + +` + +export const legacyRemoveIcon = html`