@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.4 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const Quill = require("quill");
const base64Image = require("../config/base64-image.cjs.js");
const editor_config = require("../config/editor.config.cjs.js");
const editor_utils = require("../config/editor.utils.cjs.js");
const is = require("../utils/is.cjs.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 (is.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 (editor_utils.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 (editor_utils.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 > editor_config.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 = editor_utils.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 = editor_utils.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 editor_utils.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" ? base64Image.ERROR_IMAGE_PLACEHOLDER_EN : base64Image.ERROR_IMAGE_PLACEHOLDER_CN;
file = await editor_utils.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 = editor_utils.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,
editor_utils.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 ? editor_utils.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 = editor_utils.splitWithBreak(op.insert);
let insertWithWhiteSpace = "";
lines.forEach((text) => {
insertWithWhiteSpace += editor_utils.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;
}
exports.CustomClipboard = CustomClipboard;
//# sourceMappingURL=custom-clipboard.cjs.js.map