UNPKG

nuxt-tiptap-editor

Version:

Essentials to Quickly Integrate TipTap Editor into your Nuxt App

235 lines (234 loc) 6.9 kB
import { Fragment, Slice } from "prosemirror-model"; import "prosemirror-replaceattrs"; import { Plugin } from "prosemirror-state"; let plugin = null; const fileCache = {}; export function imageUploader(options) { plugin = new ImageUploaderPlugin(options); const dummy = {}; return new Plugin({ props: { handleDOMEvents: { keydown(view) { return !plugin?.setView(view); }, drop(view) { return !plugin?.setView(view); }, focus(view) { return !plugin?.setView(view); } }, handlePaste(view, event) { return plugin?.setView(view).handlePaste(event) || false; }, transformPasted(slice) { return plugin?.transformPasted(slice) || slice; }, handleDrop(view, event) { return plugin?.setView(view).handleDrop(event) || false; } }, state: { init() { return dummy; }, apply(tr, _value, _oldState, newState) { const filesOrUrls = tr.getMeta("uploadImages"); if (filesOrUrls) { const arr = typeof filesOrUrls === "string" || filesOrUrls instanceof File ? [filesOrUrls] : Array.from(filesOrUrls); setTimeout(() => { arr.forEach( (item, i) => plugin?.beforeUpload(item, newState.selection.from + i) ); tr.setMeta("uploadImages", void 0); }, 10); } return dummy; } } }); } export class ImageUploaderPlugin { constructor(config) { this.config = config; } view; handleDrop(event) { if (!event.dataTransfer?.files.length) return; const coordinates = this.view.posAtCoords({ left: event.clientX, top: event.clientY }); if (!coordinates) return; const imageFiles = Array.from(event.dataTransfer.files).filter( (file) => this.config.acceptMimes.includes(file.type) ); if (!imageFiles.length) return; imageFiles.forEach((file, i) => { this.beforeUpload(file, coordinates.pos + i); }); return true; } transformPasted(slice) { const imageNodes = []; const children = []; slice.content.forEach((child) => { let newChild = child; if (child.type.name === "image" && !this.isOurOwnPic(child.attrs)) { newChild = this.newUploadingImageNode(child.attrs); imageNodes.push({ id: newChild.attrs.uploadId, url: child.attrs.src || child.attrs["data-src"] }); } else { child.descendants((node, pos) => { if (node.type.name === "image" && !this.isOurOwnPic(node.attrs)) { const imageNode = this.newUploadingImageNode(node.attrs); newChild = newChild.replace( pos, pos + 1, new Slice(Fragment.from(imageNode), 0, 0) ); imageNodes.push({ id: imageNode.attrs.uploadId, url: node.attrs.src || node.attrs["data-src"] }); } }); } children.push(newChild); }); imageNodes.forEach(({ url, id }) => this.uploadImageForId(url, id)); return new Slice( Fragment.fromArray(children), slice.openStart, slice.openEnd ); } handlePaste(event) { const items = Array.from(event.clipboardData?.items || []); if (items.some((x) => x.type === "text/html")) { return false; } const image = items.find( (item) => this.config.acceptMimes.includes(item.type) ); if (image) { this.beforeUpload(image.getAsFile(), this.view.state.selection.from); return true; } return false; } beforeUpload(fileOrUrl, at) { const tr = this.view.state.tr; if (!tr.selection.empty) { tr.deleteSelection(); } if (at < 0) { at = this.view.state.selection.from; } const node = this.newUploadingImageNode({ src: fileOrUrl }); tr.replaceWith(at, at, node); this.view.dispatch(tr); this.uploadImageForId(fileOrUrl, node.attrs.uploadId); } // eslint-disable-next-line @typescript-eslint/no-explicit-any newUploadingImageNode(attrs) { const uploadId = this.config.id(); fileCache[uploadId] = attrs.src || attrs["data-src"]; return this.view.state.schema.nodes.imagePlaceholder.create({ ...attrs, src: "", // attrs.src, uploadId }); } async uploadImageForId(fileOrUrl, id) { const getImagePositions = () => { const positions = []; this.view.state.doc.descendants( (node, pos) => { if (node.type.name === "imagePlaceholder" && node.attrs.uploadId === id) { positions.push({ node, pos }); } } ); return positions; }; let file = fileOrUrl; if (typeof file === "string") { file = await webImg2File(file); } const url = file && await this.config.upload(file, id).catch(console.warn); const imageNodes = getImagePositions(); if (!imageNodes.length) { return; } const tr = this.view.state.tr; imageNodes.forEach(({ node, pos }) => { if (url) { const newNode = this.view.state.schema.nodes.image.create({ ...node.attrs, width: node.attrs.width, src: url }); tr.replaceWith(pos, pos + 1, newNode); } else { tr.delete(pos, pos + 1); } }); this.view.dispatch(tr); fileCache[id] = ""; } setView(view) { this.view = view; return this; } isOurOwnPic(attrs) { const src = attrs.src || attrs["data-src"] || ""; return (this.config.ignoreDomains || []).some( (domain) => src.includes(domain) ); } } async function webImg2File(imgUrl) { function imgToBase64(url) { let canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"), img = new Image(); img.crossOrigin = "Anonymous"; img.setAttribute("referrerpolicy", "no-referrer"); img.src = url; return new Promise((resolve, reject) => { img.onload = function() { canvas.height = img.height; canvas.width = img.width; ctx?.drawImage(img, 0, 0); const dataURL = canvas.toDataURL("image/png"); resolve(dataURL); canvas = null; }; img.onerror = reject; }); } function base64toFile(base, filename) { const arr = base.split(","); const mime = arr[0].match(/:(.*?);/)[1]; const suffix = mime.split("/")[1]; const bstr = atob(arr[1]); let n = bstr.length; const u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new File([u8arr], `${filename}.${suffix}`, { type: mime }); } return imgToBase64(imgUrl).then((base) => { return base64toFile(base, "Web Image"); }).catch(() => { return null; }); } export function getFileCache(key) { return fileCache[key]; }