UNPKG

@opentiny/fluent-editor

Version:

A rich text editor based on Quill 2.0, which extends rich modules and formats on the basis of Quill. It's powerful and out-of-the-box.

470 lines (469 loc) 16.8 kB
import Quill from "quill"; import { ERROR_IMAGE_PLACEHOLDER_EN, ERROR_IMAGE_PLACEHOLDER_CN } from "../config/base64-image.es.js"; import { BIG_DELTA_LIMIT } from "../config/editor.config.es.js"; import { isNullOrUndefined, insideTable, replaceDeltaImage, imageFileToUrl, imageUrlToFile, splitWithBreak, omit, hexToRgbA } from "../config/editor.utils.es.js"; const Clipboard = Quill.import("modules/clipboard"); const Delta = Quill.import("delta"); class CustomClipboard extends Clipboard { prepareMatching(container, nodeMatches) { const elementMatchers = []; const textMatchers = []; this.matchers.forEach((pair) => { const [selector, matcher] = pair; switch (selector) { case Node.TEXT_NODE: textMatchers.push(matcher); break; case Node.ELEMENT_NODE: elementMatchers.push(matcher); break; default: { const vRegex = /v:(.+)/; const nodeList = vRegex.test(selector) ? Array.from(container.getElementsByTagName(selector)) : Array.from(container.querySelectorAll(selector)); nodeList.forEach((node) => { if (nodeMatches.has(node)) { const matches = nodeMatches.get(node); matches.push(matcher); } else { nodeMatches.set(node, [matcher]); } }); break; } } }); return [elementMatchers, textMatchers]; } onCaptureCopy(e, isCut = false) { if (e.defaultPrevented) { return; } e.preventDefault(); const [range] = this.quill.selection.getRange(); if (isNullOrUndefined(range)) { return; } const { html, text } = this.onCopy(range, isCut); if (!e.clipboardData) { e.clipboardData = { types: "text/plain", setData: (_type, value) => { return window.clipboardData.setData("Text", value); } }; } let plainText = text; if (html.startsWith("<pre>")) { plainText = text.replace(/\u00A0/g, " "); } e.clipboardData.setData("text/html", html); e.clipboardData.setData("text/plain", plainText); if (isCut) { this.quill.deleteText(range, Quill.sources.USER); } } onCapturePaste(e) { if (e.defaultPrevented || !this.quill.isEnabled()) { return; } e.preventDefault(); const range = this.quill.getSelection(true); if (isNullOrUndefined(range)) { return; } if (!e.clipboardData) { e.clipboardData = { types: "text/plain", getData: () => { return window.clipboardData.getData("Text"); } }; } const html = e.clipboardData.getData("text/html"); const text = e.clipboardData.getData("text/plain"); const files = Array.from(e.clipboardData.files || []); const msExcelCheck = /<meta.*?Microsoft Excel\s[\d].*?>/; if (html.search(msExcelCheck) === -1 && files.length > 0) { this.quill.uploader.upload(range, files); } else { const msWordCheck1 = /<meta\s*name="?generator"?\s*content="?microsoft\s*word\s*\d+"?\/?>/i; const msWordCheck2 = /xmlns:o="urn:schemas-microsoft-com/i; const result = { html, text, files, rtf: null }; if (html.search(msExcelCheck) !== -1) { result.html = renderStyles(html); } if (msWordCheck1.test(html) || msWordCheck2.test(html)) { result.rtf = e.clipboardData.getData("text/rtf"); } this.onPaste(range, result); } } onPaste(range, { html, text, files: clipboardFiles, rtf }) { const hexImages = this.extractImageDataFromRtf(rtf); const rootBgColor = getComputedStyle(this.quill.root).backgroundColor; const formats = this.quill.getFormat(range.index); let pastedDelta = this.convert({ text, html }, formats); pastedDelta = replaceDeltaWhiteSpace(pastedDelta, rootBgColor); const deltaLength = pastedDelta.ops.length; let loadingTipsContainer; if (deltaLength > BIG_DELTA_LIMIT) { loadingTipsContainer = this.quill.addContainer("ql-loading-tips"); loadingTipsContainer.innerHTML = this.quill.getLangText("pasting"); } const linePos = { index: range.index, length: range.length, fix: 0 }; const [line, offset] = this.quill.getLine(range.index); const isInsideTable = insideTable.call(this); const handlePasteContent = (content) => { let pastedContent = content; const tableBreaker = pastedContent.ops.find((op) => { return op.attributes && (op.attributes.blockquote || op.attributes["code-block"]); }); if (isInsideTable) { const table = line.domNode.closest("table.quill-better-table"); const tableBlot = Quill.find(table); const tableIndex = this.quill.getIndex(tableBlot); const tableLength = tableBlot.length(); const tableEndPos = tableIndex + tableLength; const anchorNode = getSelection().anchorNode; if (tableBreaker) { return; } if (formats["table-col"]) { linePos.index = tableIndex - 1; linePos.length = 0; } else if (range.index === tableEndPos - 1 && anchorNode instanceof HTMLDivElement && anchorNode.classList.contains("quill-better-table-wrapper")) { const list = pastedContent.filter( (op) => op.attributes && op.attributes.list ); if (list && list.length) { return; } linePos.index = tableEndPos; linePos.length = 0; } else { if (!formats["table-cell-line"]) { return; } pastedContent = { ops: pastedContent.filter((op, index) => { const regexp = /^[\n\r]+$/; const isString = op.insert && typeof op.insert === "string"; const isLine = isString && regexp.test(op.insert); const isCellLine = isLine && op.attributes && op.attributes["table-cell-line"]; const isList = isLine && op.attributes && op.attributes.list; const isPureLine = isLine && !isCellLine && !isList; const isTableCol = isLine && op.attributes && op.attributes["table-col"]; const isLastCellLine = isCellLine && index === deltaLength - 1; return !isPureLine && !isTableCol && !isLastCellLine; }) }; pastedContent = rebuildDelta( new Delta(pastedContent.ops), formats["table-cell-line"] ); } } const lastChild = pastedContent.ops[pastedContent.ops.length - 1]; const hasList = lastChild && lastChild.attributes && lastChild.attributes.list; if (hasList && offset === 0 && line && line.cache.length === 1 && (line.statics.blotName === "block" || line.statics.blotName === "table-cell-line") && (!line.next || line.next.statics.blotName !== "table-view")) { linePos.index = this.quill.getIndex(line); linePos.length = line.length(); linePos.fix = 1; } const oldDelta = new Delta().retain(linePos.index).delete(linePos.length); const delta = oldDelta.concat(pastedContent); setTimeout(() => { this.quill.updateContents(delta, Quill.sources.USER); this.quill.setSelection( delta.length() - linePos.length - linePos.fix, Quill.sources.SILENT ); this.quill.scrollIntoView(); if (loadingTipsContainer) { loadingTipsContainer.remove(); } }); }; (async () => { try { const [files, placeholders, originalUrls, imageIndexs] = this.flipFilesArray( await this.extractFilesFromDelta( pastedDelta, clipboardFiles, hexImages ) ); if (files.length === 0) { handlePasteContent(pastedDelta); } else { if (this.quill.options.editorPaste && this.quill.options.editorPaste.observers.length !== 0) { this.quill.options.editorPaste.emit({ files, callback: ({ code, message, data }) => { if (code === 0) { const { imageUrls } = data; pastedDelta = replaceDeltaImage( pastedDelta, imageUrls, placeholders ); handlePasteContent(pastedDelta); } else { console.error("error message:", message); } } }); } else { if (files[0] !== void 0 || originalUrls.length === 0) { const imageUrls = await this.files2urls( files, placeholders, originalUrls, pastedDelta, imageIndexs ); pastedDelta = replaceDeltaImage( pastedDelta, imageUrls, placeholders ); } handlePasteContent(pastedDelta); } } } catch (_e) { throw new Error("Paste failed."); } })(); } files2urls(files, placeholders, originalUrls, pastedDelta, imageIndexs) { return Promise.all( files.map(async (imageFile, index) => { const netImgExp = /^((http|https)\:)?\/\/([\s\S]+)$/; if (!placeholders[index] && originalUrls[index] && netImgExp.test(originalUrls[index])) { return new Promise((resolve) => { resolve(originalUrls[index]); }); } else if (this.quill.options.uploadOption.imageUploadToServer) { const range = this.getImgSelection(pastedDelta, imageIndexs[index]); this.quill.uploader.upload(range, [imageFile]); } else { return imageFileToUrl(imageFile); } }) ); } flipFilesArray(filesArr) { const files = []; const placeholders = []; const originalUrls = []; const imageIndexs = []; filesArr.forEach((item) => { if (item) { const [file, placeholder, originalUrl, imageIndex] = item; files.push(file); placeholders.push(placeholder); originalUrls.push(originalUrl); if (imageIndex === 0 || imageIndex) { imageIndexs.push(imageIndex); } } }); return [files, placeholders, originalUrls, imageIndexs]; } // 将图片从hex转为base64 convertHexToBase64(hexString) { return btoa( hexString.match(/\w{2}/g).map((char) => { return String.fromCharCode(Number.parseInt(char, 16)); }).join("") ); } // 匹配rtf中的图片,存储为{hex, type}对象数组 extractImageDataFromRtf(rtfData) { if (!rtfData) { return []; } const regexPictureHeader = /{\\pict[\s\S]+?\\bliptag-?\d+(\\blipupi-?\d+)?({\\\*\\blipuid\s?[\da-fA-F]+)?[\s}]*?/; const regexPicture = new RegExp( `(?:(${regexPictureHeader.source}))([\\da-fA-F\\s]+)\\}`, "g" ); const images = rtfData.match(regexPicture); const result = []; if (images) { for (const image of images) { let imageType = ""; if (image.includes("\\pngblip")) { imageType = "image/png"; } else if (image.includes("\\jpegblip")) { imageType = "image/jpeg"; } if (imageType) { result.push({ hex: image.replace(regexPictureHeader, "").replace(/[^\da-fA-F]/g, ""), type: imageType }); } } } return result; } extractFilesFromDelta(delta, clipboardFiles, hexImages) { let index = -1; return Promise.all( delta.map(async (op) => { var _a; index++; const image = op.insert.image; if (!image || image.hasExisted) { return; } let file; let isPlaceholderImage = false; let imageIndex; try { const hexImage = hexImages.length && hexImages.shift(); const newImage = hexImage && `data:${hexImage.type};base64,${this.convertHexToBase64( hexImage.hex )}`; imageIndex = index; file = await imageUrlToFile(newImage || image.src || image); } catch (_err) { if (clipboardFiles.length !== 0) { const clipboardFile = clipboardFiles[0]; const imageType = ((_a = clipboardFile.type) == null ? void 0 : _a.indexOf("image")) === -1 ? "image/png" : clipboardFile.type; const blob = clipboardFile.slice(0, clipboardFile.size, imageType); file = new File([blob], `image-CORS-${(/* @__PURE__ */ new Date()).getTime()}.png`, { type: imageType }); } else if (image.src.startsWith("http")) { } else { const errorImagePlaceholderJpg = this.quill.getLangText("img-error") === "Image Copy Error" ? ERROR_IMAGE_PLACEHOLDER_EN : ERROR_IMAGE_PLACEHOLDER_CN; file = await imageUrlToFile(errorImagePlaceholderJpg, true); isPlaceholderImage = true; } } return [file, isPlaceholderImage, image, imageIndex]; }) ); } getImgSelection(delta, imageIndex) { let length = 0; delta.ops.every((op, index) => { if (index === imageIndex) { return false; } if (typeof op.insert === "string") { length += op.insert.length; } return true; }); const range = { index: length, length: 0 }; return range; } } function rebuildDelta(delta, cellLine) { const { cell: cellId, colspan, row: rowId, rowspan } = cellLine; const buildedDelta = delta.reduce((newDelta, op) => { if (op.insert && typeof op.insert === "string") { const lines = splitWithBreak(op.insert); lines.forEach((text) => { if (text === "\n") { newDelta.insert("\n", { ...op.attributes, "table-cell-line": { row: rowId, cell: cellId, rowspan, colspan } }); } else { text = text.endsWith("\r") ? text.slice(0, -1) : text; newDelta.insert( text, omit(op.attributes, ["table", "table-cell-line"]) ); } }); } else { newDelta.insert(op.insert, op.attributes); } return newDelta; }, new Delta()); return buildedDelta; } function replaceStrWhiteSpace(str) { const isWhiteSpace = (value) => /^(\u3000|\u0020){1}$/.test(value); let textWithWhiteSpace = ""; let beginHasChar = false; for (const char of str) { if (isWhiteSpace(char) && !beginHasChar) { textWithWhiteSpace += " "; } else { textWithWhiteSpace += char; beginHasChar = true; } } return textWithWhiteSpace; } function replaceDeltaWhiteSpace(delta, rootBgColor) { return delta.reduce((newDelta, op) => { if (rootBgColor && op.attributes && op.attributes.color && !op.attributes.background) { const originColor = op.attributes.color; const fontColor = originColor.indexOf("#") === 0 ? hexToRgbA(originColor) : originColor; if (fontColor === rootBgColor || fontColor === "rgba(255,255,255,1)" && rootBgColor === "rgba(0, 0, 0, 0)") { delete op.attributes.color; } } if (op.insert && typeof op.insert === "string") { const lines = splitWithBreak(op.insert); let insertWithWhiteSpace = ""; lines.forEach((text) => { insertWithWhiteSpace += replaceStrWhiteSpace(text); }); newDelta.insert(insertWithWhiteSpace, op.attributes); } else { newDelta.insert(op.insert, op.attributes); } return newDelta; }, new Delta()); } function renderStyles(html) { let htmlString = html; htmlString = htmlString.substring( htmlString.indexOf("<html "), htmlString.length ); htmlString = htmlString.substring( 0, htmlString.lastIndexOf("</html>") + "</html>".length ); const iframe = document.createElement("iframe"); iframe.style.display = "none"; document.body.appendChild(iframe); const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; iframeDoc.open(); iframeDoc.write(htmlString); iframeDoc.close(); let collection; let pointer; const rules = iframeDoc.styleSheets[iframeDoc.styleSheets.length - 1].cssRules; for (let idx = 0; idx < rules.length; idx++) { if (rules[idx].selectorText === "") { continue; } collection = iframeDoc.body.querySelectorAll( rules[idx].selectorText ); for (pointer = 0; pointer < collection.length; pointer++) { collection[pointer].style.cssText += rules[idx].style.cssText; } } const convertedString = iframeDoc.firstChild.outerHTML; iframe.parentNode.removeChild(iframe); return convertedString; } export { CustomClipboard as default }; //# sourceMappingURL=custom-clipboard.es.js.map