@milkdown/plugin-clipboard
Version:
The clipboard plugin of [milkdown](https://milkdown.dev/).
131 lines (110 loc) • 3.95 kB
text/typescript
import type { Node } from '@milkdown/prose/model'
import {
editorViewOptionsCtx,
parserCtx,
schemaCtx,
serializerCtx,
} from '@milkdown/core'
import { getNodeFromSchema, isTextOnlySlice } from '@milkdown/prose'
import { DOMParser, DOMSerializer } from '@milkdown/prose/model'
import { Plugin, PluginKey, TextSelection } from '@milkdown/prose/state'
import { $prose } from '@milkdown/utils'
type UnknownRecord = Record<string, unknown>
function isPureText(
content: UnknownRecord | UnknownRecord[] | undefined | null
): boolean {
if (!content) return false
if (Array.isArray(content)) {
if (content.length > 1) return false
return isPureText(content[0])
}
const child = content.content
if (child) return isPureText(child as UnknownRecord[])
return content.type === 'text'
}
/// The prosemirror plugin for clipboard.
export const clipboard = $prose((ctx) => {
const schema = ctx.get(schemaCtx)
// Set editable props for https://github.com/Milkdown/milkdown/issues/190
ctx.update(editorViewOptionsCtx, (prev) => ({
...prev,
editable: prev.editable ?? (() => true),
}))
const key = new PluginKey('MILKDOWN_CLIPBOARD')
const plugin = new Plugin({
key,
props: {
handlePaste: (view, event) => {
const parser = ctx.get(parserCtx)
const editable = view.props.editable?.(view.state)
const { clipboardData } = event
if (!editable || !clipboardData) return false
const currentNode = view.state.selection.$from.node()
if (currentNode.type.spec.code) return false
const text = clipboardData.getData('text/plain')
// if is copied from vscode, try to create a code block
const vscodeData = clipboardData.getData('vscode-editor-data')
if (vscodeData) {
const data = JSON.parse(vscodeData)
const language = data?.mode
if (text && language) {
const { tr } = view.state
const codeBlock = getNodeFromSchema('code_block', schema)
tr.replaceSelectionWith(codeBlock.create({ language }))
.setSelection(
TextSelection.near(
tr.doc.resolve(Math.max(0, tr.selection.from - 2))
)
)
.insertText(text.replace(/\r\n?/g, '\n'))
view.dispatch(tr)
return true
}
}
const html = clipboardData.getData('text/html')
if (html.length === 0 && text.length === 0) return false
const domParser = DOMParser.fromSchema(schema)
let dom
if (html.length === 0) {
const slice = parser(text)
if (!slice || typeof slice === 'string') return false
dom = DOMSerializer.fromSchema(schema).serializeFragment(
slice.content
)
} else {
const template = document.createElement('template')
template.innerHTML = html
dom = template.content.cloneNode(true)
template.remove()
}
const slice = domParser.parseSlice(dom)
const node = isTextOnlySlice(slice)
if (node) {
view.dispatch(view.state.tr.replaceSelectionWith(node, true))
return true
}
view.dispatch(view.state.tr.replaceSelection(slice))
return true
},
clipboardTextSerializer: (slice) => {
const serializer = ctx.get(serializerCtx)
const isText = isPureText(slice.content.toJSON())
if (isText)
return (slice.content as unknown as Node).textBetween(
0,
slice.content.size,
'\n\n'
)
const doc = schema.topNodeType.createAndFill(undefined, slice.content)
if (!doc) return ''
const value = serializer(doc)
return value
},
},
})
return plugin
})
clipboard.meta = {
displayName: 'Prose<clipboard>',
package: '@milkdown/plugin-clipboard',
}