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.

411 lines (410 loc) 14.3 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, replaceDeltaImage, imageUrlToFile, splitWithBreak, omit, hexToRgbA, replaceStrWhiteSpace } from "../config/editor.utils.es.js"; import { isString } from "../utils/is.es.js"; const Clipboard = Quill.import("modules/clipboard"); const Delta = Quill.import("delta"); class CustomClipboard extends Clipboard { constructor(quill) { super(quill); this.quill = quill; this.quill.root.addEventListener("input", () => { if (this.quill.root.innerText !== "\\n" && this.quill.root.classList.contains("ql-blank")) { this.quill.root.classList.toggle("ql-blank", false); } else { this.quill.options.placeholder = this.quill.getLangText("editor-placeholder"); } }); } prepareMatching(container, nodeMatches) { const elementMatchers = []; const textMatchers = []; this.matchers.forEach((pair) => { const [selector, matcher] = pair; if (selector === Node.TEXT_NODE) { textMatchers.push(matcher); } else if (selector === Node.ELEMENT_NODE) { elementMatchers.push(matcher); } else if (isString(selector)) { const vRegex = /v:(.+)/; const nodeList = Array.from( vRegex.test(selector) ? container.getElementsByTagName(selector) : container.querySelectorAll(selector) ); nodeList.forEach((node) => { if (nodeMatches.has(node)) { const matches = nodeMatches.get(node); matches.push(matcher); } else { nodeMatches.set(node, [matcher]); } }); } }); 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 handlePasteContent = (content) => { const pastedContent = content; const oldDelta = new Delta().retain(linePos.index).delete(linePos.length); const delta = oldDelta.concat(pastedContent); setTimeout(() => { this.quill.updateContents(delta, Quill.sources.USER); const newSelectionIndex = linePos.index + (pastedContent.length ? pastedContent.length() : 0); this.quill.setSelection( newSelectionIndex, Quill.sources.SILENT ); this.quill.scrollSelectionIntoView(); 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 { const range = this.getImgSelection(pastedDelta, imageIndexs[index]); const urls = await this.quill.uploader.getFileUrls([imageFile], range); return urls[0] || void 0; } }) ); } 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; } else if (typeof op.insert === "object") { length += 1; } 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 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 }; //# sourceMappingURL=custom-clipboard.es.js.map