import { imageBlockConfig } from '@milkdown/kit/component/image-block' import { type DefaultValue, defaultValueCtx, Editor, EditorStatus, editorViewCtx, editorViewOptionsCtx, rootCtx, } from '@milkdown/kit/core' import { clipboard } from '@milkdown/kit/plugin/clipboard' import { history } from '@milkdown/kit/plugin/history' import { indent, indentConfig } from '@milkdown/kit/plugin/indent' import { listener, listenerCtx, type ListenerManager, } from '@milkdown/kit/plugin/listener' import { trailing } from '@milkdown/kit/plugin/trailing' import { upload, uploadConfig } from '@milkdown/kit/plugin/upload' import { commonmark } from '@milkdown/kit/preset/commonmark' import { gfm } from '@milkdown/kit/preset/gfm' import { getMarkdown } from '@milkdown/kit/utils' import type { CrepeFeatureConfig } from '../feature' import type { DefineFeature } from '../feature/shared' import { CrepeFeature } from '../feature' import { CrepeCtx, FeaturesCtx, useCrepeFeatures } from './slice' /// The crepe builder configuration. export interface CrepeBuilderConfig { /// The root element for the editor. /// Supports both DOM nodes and CSS selectors, /// If not provided, the editor will be appended to the body. root?: Node | string | null /// The default value for the editor. defaultValue?: DefaultValue } /// The crepe builder class. /// This class allows users to manually add features to the editor. export class CrepeBuilder { /// @internal readonly #editor: Editor /// @internal readonly #rootElement: Node /// @internal #editable = true /// The constructor of the crepe builder. /// You can pass configs to the builder to configure the editor. constructor({ root, defaultValue = '' }: CrepeBuilderConfig = {}) { this.#rootElement = (typeof root === 'string' ? document.querySelector(root) : root) ?? document.body this.#editor = Editor.make() .config((ctx) => { ctx.inject(CrepeCtx, this) ctx.inject(FeaturesCtx, []) }) .config((ctx) => { ctx.set(rootCtx, this.#rootElement) ctx.set(defaultValueCtx, defaultValue) ctx.set(editorViewOptionsCtx, { editable: () => this.#editable, }) ctx.update(indentConfig.key, (value) => ({ ...value, size: 4, })) ctx.update(uploadConfig.key, (prev) => ({ ...prev, uploader: async (files, schema, ctx) => { const features = useCrepeFeatures(ctx).get() const hasImageBlock = features.includes(CrepeFeature.ImageBlock) const nodeType = hasImageBlock ? schema.nodes['image-block'] : schema.nodes['image'] if (!nodeType) return [] const onUpload = hasImageBlock ? ctx.get(imageBlockConfig.key).onUpload : undefined const images: File[] = [] for (let i = 0; i < files.length; i++) { const file = files.item(i) if (file && file.type.includes('image')) images.push(file) } const nodes = await Promise.all( images.map(async (file) => { const src = onUpload ? await onUpload(file) : URL.createObjectURL(file) return nodeType.createAndFill({ src })! }) ) return nodes }, })) }) .use(commonmark) .use(listener) .use(history) .use(indent) .use(trailing) .use(clipboard) .use(upload) .use(gfm) } /// Add a feature to the editor. addFeature: { ( feature: DefineFeature, config?: CrepeFeatureConfig[T] ): CrepeBuilder (feature: DefineFeature, config?: C): CrepeBuilder } = (feature: DefineFeature, config?: never) => { feature(this.#editor, config) return this } /// Create the editor. create = () => { return this.#editor.create() } /// Destroy the editor. destroy = () => { return this.#editor.destroy() } /// Get the milkdown editor instance. get editor(): Editor { return this.#editor } /// Get the readonly state of the editor. get readonly() { return !this.#editable } /// Set the readonly mode of the editor. setReadonly = (value: boolean) => { this.#editable = !value this.#editor.action((ctx) => { if (this.#editor.status === EditorStatus.Created) { const view = ctx.get(editorViewCtx) view.setProps({ editable: () => !value, }) } }) return this } /// Get the markdown content of the editor. getMarkdown = () => { return this.#editor.action(getMarkdown()) } /// Register event listeners. on = (fn: (api: ListenerManager) => void) => { if (this.#editor.status !== EditorStatus.Created) { this.#editor.config((ctx) => { const listener = ctx.get(listenerCtx) fn(listener) }) return this } this.#editor.action((ctx) => { const listener = ctx.get(listenerCtx) fn(listener) }) return this } }