@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
JavaScript
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