2022-09-09 11:53:35 +00:00
|
|
|
import { Plugin } from 'prosemirror-state'
|
|
|
|
import type { Node, Schema } from 'prosemirror-model'
|
|
|
|
import type { EditorView } from 'prosemirror-view'
|
2022-10-08 16:40:58 +00:00
|
|
|
import type { ProseMirrorExtension } from '../../store/state'
|
2022-09-09 11:53:35 +00:00
|
|
|
|
|
|
|
const REGEX = /^!\[([^[\]]*)]\((.+?)\)\s+/
|
|
|
|
const MAX_MATCH = 500
|
|
|
|
|
|
|
|
const isUrl = (str: string) => {
|
|
|
|
try {
|
|
|
|
const url = new URL(str)
|
|
|
|
|
|
|
|
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
|
|
} catch {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const isBlank = (text: string) => text === ' ' || text === '\u00A0'
|
|
|
|
/*
|
|
|
|
export const getImagePath = async (src: string, path?: string) => {
|
|
|
|
let paths = [src]
|
|
|
|
|
|
|
|
if (path) paths = [await dirname(path), src]
|
|
|
|
|
|
|
|
const absolutePath = await resolvePath(paths)
|
|
|
|
|
|
|
|
return convertFileSrc(absolutePath)
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
const imageInput = (schema: Schema, _path?: string) =>
|
|
|
|
new Plugin({
|
|
|
|
props: {
|
|
|
|
handleTextInput(view, from, to, text) {
|
|
|
|
if (view.composing || !isBlank(text)) return false
|
|
|
|
|
|
|
|
const $from = view.state.doc.resolve(from)
|
|
|
|
|
|
|
|
if ($from.parent.type.spec.code) return false
|
|
|
|
|
|
|
|
const textBefore =
|
|
|
|
$from.parent.textBetween(
|
|
|
|
Math.max(0, $from.parentOffset - MAX_MATCH),
|
|
|
|
$from.parentOffset,
|
|
|
|
undefined,
|
|
|
|
'\uFFFC'
|
|
|
|
) + text
|
|
|
|
|
|
|
|
const match = REGEX.exec(textBefore)
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
const [, title, src] = match
|
|
|
|
|
|
|
|
if (isUrl(src)) {
|
|
|
|
const node = schema.node('image', { src, title })
|
|
|
|
const start = from - (match[0].length - text.length)
|
|
|
|
const tr = view.state.tr
|
|
|
|
|
|
|
|
tr.delete(start, to)
|
|
|
|
tr.insert(start, node)
|
|
|
|
view.dispatch(tr)
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// if (!isTauri) return false
|
|
|
|
/*
|
|
|
|
getImagePath(src, path).then((p) => {
|
|
|
|
const node = schema.node('image', { src: p, title, path: src })
|
|
|
|
const start = from - (match[0].length - text.length)
|
|
|
|
const tr = view.state.tr
|
|
|
|
|
|
|
|
tr.delete(start, to)
|
|
|
|
tr.insert(start, node)
|
|
|
|
view.dispatch(tr)
|
|
|
|
})
|
|
|
|
*/
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const imageSchema = {
|
|
|
|
inline: true,
|
|
|
|
attrs: {
|
|
|
|
src: {},
|
|
|
|
alt: { default: null },
|
|
|
|
title: { default: null },
|
|
|
|
path: { default: null },
|
|
|
|
width: { default: null }
|
|
|
|
},
|
|
|
|
group: 'inline',
|
|
|
|
draggable: true,
|
|
|
|
parseDOM: [
|
|
|
|
{
|
|
|
|
tag: 'img[src]',
|
|
|
|
getAttrs: (dom: HTMLElement) => ({
|
|
|
|
src: dom.getAttribute('src'),
|
|
|
|
title: dom.getAttribute('title'),
|
|
|
|
alt: dom.getAttribute('alt'),
|
|
|
|
path: dom.dataset.path
|
|
|
|
})
|
|
|
|
}
|
|
|
|
],
|
|
|
|
toDOM: (node: Node) => [
|
|
|
|
'img',
|
|
|
|
{
|
|
|
|
src: node.attrs.src,
|
|
|
|
title: node.attrs.title,
|
|
|
|
alt: node.attrs.alt,
|
|
|
|
'data-path': node.attrs.path
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
export const insertImage = (view: EditorView, src: string, left: number, top: number) => {
|
|
|
|
const state = view.state
|
|
|
|
const tr = state.tr
|
|
|
|
const node = state.schema.nodes.image.create({ src })
|
|
|
|
|
|
|
|
if (view) {
|
|
|
|
const pos = view.posAtCoords({ left, top }).pos
|
|
|
|
|
|
|
|
tr.insert(pos, node)
|
|
|
|
view.dispatch(tr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class ImageView {
|
|
|
|
node: Node
|
|
|
|
view: EditorView
|
|
|
|
getPos: () => number
|
|
|
|
schema: Schema
|
|
|
|
dom: Element
|
|
|
|
contentDOM: Element
|
|
|
|
container: HTMLElement
|
|
|
|
handle: HTMLElement
|
2022-10-08 16:40:58 +00:00
|
|
|
onResizeFn: (e: Event) => void
|
|
|
|
onResizeEndFn: (e: Event) => void
|
2022-09-09 11:53:35 +00:00
|
|
|
width: number
|
|
|
|
updating: number
|
|
|
|
|
|
|
|
constructor(node: Node, view: EditorView, getPos: () => number, schema: Schema, _path: string) {
|
|
|
|
this.node = node
|
|
|
|
this.view = view
|
|
|
|
this.getPos = getPos
|
|
|
|
this.schema = schema
|
|
|
|
this.onResizeFn = this.onResize.bind(this)
|
|
|
|
this.onResizeEndFn = this.onResizeEnd.bind(this)
|
|
|
|
|
|
|
|
this.container = document.createElement('span')
|
|
|
|
this.container.className = 'image-container'
|
|
|
|
|
|
|
|
if (node.attrs.width) this.setWidth(node.attrs.width)
|
|
|
|
|
|
|
|
const image = document.createElement('img')
|
|
|
|
|
|
|
|
image.setAttribute('title', node.attrs.title ?? '')
|
|
|
|
|
|
|
|
if (
|
|
|
|
// isTauri &&
|
|
|
|
!node.attrs.src.startsWith('asset:') &&
|
|
|
|
!node.attrs.src.startsWith('data:') &&
|
|
|
|
!isUrl(node.attrs.src)
|
|
|
|
) {
|
|
|
|
// getImagePath(node.attrs.src, path).then((p) => image.setAttribute('src', p))
|
|
|
|
} else {
|
|
|
|
image.setAttribute('src', node.attrs.src)
|
|
|
|
}
|
|
|
|
|
|
|
|
this.handle = document.createElement('span')
|
|
|
|
this.handle.className = 'resize-handle'
|
|
|
|
this.handle.addEventListener('mousedown', (e) => {
|
|
|
|
e.preventDefault()
|
|
|
|
window.addEventListener('mousemove', this.onResizeFn)
|
|
|
|
window.addEventListener('mouseup', this.onResizeEndFn)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.container.append(image)
|
|
|
|
this.container.append(this.handle)
|
|
|
|
this.dom = this.container
|
|
|
|
}
|
|
|
|
|
|
|
|
onResize(e: MouseEvent) {
|
|
|
|
this.width = e.pageX - this.container.getBoundingClientRect().left
|
|
|
|
this.setWidth(this.width)
|
|
|
|
}
|
|
|
|
|
|
|
|
onResizeEnd() {
|
|
|
|
window.removeEventListener('mousemove', this.onResizeFn)
|
|
|
|
|
|
|
|
if (this.updating === this.width) return
|
|
|
|
|
|
|
|
this.updating = this.width
|
|
|
|
const tr = this.view.state.tr
|
|
|
|
|
|
|
|
tr.setNodeMarkup(this.getPos(), undefined, {
|
|
|
|
...this.node.attrs,
|
|
|
|
width: this.width
|
|
|
|
})
|
|
|
|
|
|
|
|
this.view.dispatch(tr)
|
|
|
|
}
|
|
|
|
|
|
|
|
setWidth(width: number) {
|
|
|
|
this.container.style.width = `${width}px`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default (path?: string): ProseMirrorExtension => ({
|
|
|
|
schema: (prev) => ({
|
|
|
|
...prev,
|
|
|
|
nodes: (prev.nodes as any).update('image', imageSchema)
|
|
|
|
}),
|
|
|
|
plugins: (prev, schema) => [...prev, imageInput(schema, path)],
|
|
|
|
nodeViews: {
|
|
|
|
// FIXME something is not right
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
// @ts-ignore
|
|
|
|
image: (node, view, getPos) => {
|
|
|
|
return new ImageView(node, view, getPos, view.state.schema, path)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|