UNPKG

@milkdown/plugin-block

Version:

The block plugin of [milkdown](https://milkdown.dev/).

194 lines (166 loc) 5.36 kB
import type { ComputePositionConfig, Middleware, OffsetOptions, Placement, VirtualElement, } from '@floating-ui/dom' import type { Ctx } from '@milkdown/ctx' import type { EditorState } from '@milkdown/prose/state' import type { EditorView } from '@milkdown/prose/view' import { computePosition, flip, offset } from '@floating-ui/dom' import { editorViewCtx } from '@milkdown/core' import type { BlockService } from './block-service' import type { ActiveNode } from './types' import { blockServiceInstance } from './block-plugin' /// The context of the block provider. export interface DeriveContext { ctx: Ctx active: ActiveNode editorDom: HTMLElement blockDom: HTMLElement } /// Options for creating block provider. export interface BlockProviderOptions { /// The context of the editor. ctx: Ctx /// The content of the block. content: HTMLElement /// The function to determine whether the tooltip should be shown. shouldShow?: (view: EditorView, prevState?: EditorState) => boolean /// The offset to get the block. Default is 0. getOffset?: (deriveContext: DeriveContext) => OffsetOptions /// The function to get the position of the block. Default is the position of the active node. getPosition?: (deriveContext: DeriveContext) => Omit<DOMRect, 'toJSON'> /// The function to get the placement of the block. Default is 'left'. getPlacement?: (deriveContext: DeriveContext) => Placement /// Other middlewares for floating ui. This will be added after the internal middlewares. middleware?: Middleware[] /// Options for floating ui. If you pass `middleware` or `placement`, it will override the internal settings. floatingUIOptions?: Partial<ComputePositionConfig> /// The root element that the block will be appended to. root?: HTMLElement } /// A provider for creating block. export class BlockProvider { /// @internal readonly #element: HTMLElement /// @internal readonly #ctx: Ctx /// @internal #service?: BlockService /// @internal #activeNode: ActiveNode | null = null /// @internal readonly #root?: HTMLElement /// @internal #initialized = false /// @internal readonly #middleware: Middleware[] /// @internal readonly #floatingUIOptions: Partial<ComputePositionConfig> /// @internal readonly #getOffset?: (deriveContext: DeriveContext) => OffsetOptions /// @internal readonly #getPosition?: ( deriveContext: DeriveContext ) => Omit<DOMRect, 'toJSON'> /// @internal readonly #getPlacement?: (deriveContext: DeriveContext) => Placement /// The context of current active node. get active() { return this.#activeNode } constructor(options: BlockProviderOptions) { this.#ctx = options.ctx this.#element = options.content this.#getOffset = options.getOffset this.#getPosition = options.getPosition this.#getPlacement = options.getPlacement this.#middleware = options.middleware ?? [] this.#floatingUIOptions = options.floatingUIOptions ?? {} this.#root = options.root this.hide() } /// @internal #init() { const view = this.#ctx.get(editorViewCtx) const root = this.#root ?? view.dom.parentElement ?? document.body root.appendChild(this.#element) const service = this.#ctx.get(blockServiceInstance.key) service.bind(this.#ctx, (message) => { if (message.type === 'hide') { this.hide() this.#activeNode = null } else if (message.type === 'show') { this.show(message.active) this.#activeNode = message.active } }) this.#service = service this.#service.addEvent(this.#element) this.#element.draggable = true } /// Update provider state by editor view. update = (): void => { requestAnimationFrame(() => { if (!this.#initialized) { try { this.#init() this.#initialized = true } catch { // ignore } } }) } /// Destroy the block. destroy = () => { this.#service?.unBind() this.#service?.removeEvent(this.#element) this.#element.remove() } /// Show the block. show = (active: ActiveNode) => { const dom = active.el const editorDom = this.#ctx.get(editorViewCtx).dom const deriveContext: DeriveContext = { ctx: this.#ctx, active, editorDom, blockDom: this.#element, } const virtualEl: VirtualElement = { contextElement: dom, getBoundingClientRect: () => { if (this.#getPosition) return this.#getPosition(deriveContext) return dom.getBoundingClientRect() }, } const middleware = [flip()] if (this.#getOffset) { const offsetOption = this.#getOffset(deriveContext) const offsetExt = offset(offsetOption) middleware.push(offsetExt) } computePosition(virtualEl, this.#element, { placement: this.#getPlacement ? this.#getPlacement(deriveContext) : 'left', middleware: [...middleware, ...this.#middleware], ...this.#floatingUIOptions, }) .then(({ x, y }) => { Object.assign(this.#element.style, { left: `${x}px`, top: `${y}px`, }) this.#element.dataset.show = 'true' }) .catch(console.error) } /// Hide the block. hide = () => { this.#element.dataset.show = 'false' } }