quill-react-pro
Version:
A Quill component for React and more maturely.
1,362 lines (1,291 loc) • 110 kB
JavaScript
import React, { useRef, useEffect } from 'react';
import Quill from 'quill';
import Delta from 'quill-delta';
import highlight from 'highlight.js/lib/core';
import 'highlight.js/styles/xcode.css';
import javascript from 'highlight.js/lib/languages/javascript';
import css from 'highlight.js/lib/languages/css';
import json from 'highlight.js/lib/languages/json';
import bash from 'highlight.js/lib/languages/bash';
import python from 'highlight.js/lib/languages/python';
import java from 'highlight.js/lib/languages/java';
import cpp from 'highlight.js/lib/languages/cpp';
import sql from 'highlight.js/lib/languages/sql';
import xml from 'highlight.js/lib/languages/xml';
import QuillBetterTable from 'quill-better-table';
const i18nConfig = {
en: {
toolbarHeader: 'Body',
toolbarFont: 'System',
fontYahei: 'MS Yahei',
fontSong: 'SimSun',
fontKai: 'KaiTi',
tableDialogLabel: 'Slide & Click',
imageDialogLocal: 'Select Local Image',
imageDialogUrlLabel: 'Or input image url',
iamgeDialogInsert: 'Insert',
imageDialogTypeErr: 'File type is error, please upload again!',
imageDialogSizeErr: 'Image size cannot exceed $M',
dividerDialogColorLabel: 'Primary Color:',
placeholder: 'Start Note(Support input markdown)...',
alignLeft: 'Left align',
alignRight: 'Right align',
alignCenter: 'Center align',
tableBackground: 'Background Colors',
linkWords: 'Text',
linkUrl: 'Link',
linkSave: 'Save',
linkTarget: 'Link To Url',
linkClear: 'Remove Link',
linkUrlErr: 'Please input correct Url!',
imgStatusUploading: ' Image uploading... ',
imgStatusFail: ' Upload fail & click to upload again ',
imgRemarkPre: 'Fig. ',
imgRemarkLabel: 'Add Remark',
deleteImg: 'Delete Image'
},
zh: {
toolbarHeader: '正文',
toolbarFont: '系统字体',
fontYahei: '微软雅黑',
fontSong: '宋体',
fontKai: '楷体',
tableDialogLabel: '滑动点击生成',
imageDialogLocal: '选择本地图片',
imageDialogUrlLabel: '或输入网络图片URL',
iamgeDialogInsert: '插入',
imageDialogTypeErr: '图片格式错误,请重新上传!',
imageDialogSizeErr: '图片大小不能超过$M',
dividerDialogColorLabel: '主色:',
placeholder: '开始笔记(支持直接Markdown输入)...',
alignLeft: '居左',
alignRight: '居右',
alignCenter: '居中',
tableBackground: '背景色',
linkWords: '文本',
linkUrl: '链接',
linkSave: '保存',
linkTarget: '跳转',
linkClear: '取消链接',
linkUrlErr: '请输入正确Url!',
imgStatusUploading: ' 图片上传中... ',
imgStatusFail: ' 上传失败,点击重新上传 ',
imgRemarkPre: '图:',
imgRemarkLabel: '添加备注',
deleteImg: '删除图片'
},
es: {
toolbarHeader: 'Texto',
toolbarFont: 'Sistema',
fontYahei: 'MS Yahei',
fontSong: 'SimSun',
fontKai: 'KaiTi',
tableDialogLabel: 'Desliza y haz clic',
imageDialogLocal: 'Subir imagen',
imageDialogUrlLabel: 'O introduce la URL de la imagen',
iamgeDialogInsert: 'Insertar',
imageDialogTypeErr: '¡El tipo de archivo es incorrecto, por favor súbelo de nuevo!',
imageDialogSizeErr: 'El tamaño de la imagen no puede exceder $M',
dividerDialogColorLabel: 'Color principal:',
placeholder: 'Empieza la nota (admite Markdown)...',
alignLeft: 'Alinear a la izquierda',
alignRight: 'Alinear a la derecha',
alignCenter: 'Alinear al centro',
tableBackground: 'Colores de fondo',
linkWords: 'Texto',
linkUrl: 'Enlace',
linkSave: 'Guardar',
linkTarget: 'Enlazar a URL',
linkClear: 'Eliminar enlace',
linkUrlErr: '¡Introduce una URL correcta!',
imgStatusUploading: ' Subiendo imagen... ',
imgStatusFail: ' Error al subir. Haz clic para volver a intentarlo ',
imgRemarkPre: 'Fig. ',
imgRemarkLabel: 'Agregar comentario',
deleteImg: 'Eliminar imagen'
}
};
const getI18nText = function (keys) {
let i18n = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'en';
if (Array.isArray(keys)) return keys.map(key => i18nConfig[i18n][key]);
return i18nConfig[i18n][keys];
};
function isUrl(url) {
return /(?:(https?|ftp|file):\/\/)?[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/.test(url);
}
function isEmail(url) {
return /^\S+@\S+\.\S+$/.test(url);
}
function isMobile() {
if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4))) {
return true;
}
return false;
}
function setContent(content, quill) {
quill.getModule("imageResize")?.hide();
if (content) {
if (typeof content === "object") {
quill.setContents(content);
} else {
quill.clipboard.dangerouslyPasteHTML(content);
}
} else {
quill.setText("");
}
}
const optionDisableToggle = (quill, blockList, disable) => {
const toolbar = quill.getModule("toolbar");
blockList.forEach(item => {
const btns = toolbar.container.querySelectorAll(`.ql-${item}`);
btns.forEach(btn => {
if (btn.className.indexOf("ql-picker") >= 0) {
const picker = btn.querySelector(".ql-picker-options");
if (disable) {
picker.setAttribute("style", "display: none");
btn.classList.add("picker-disable");
} else {
picker.setAttribute("style", "");
btn.classList.remove("picker-disable");
}
} else {
btn.disabled = disable;
}
});
});
};
const throttle = function (fn) {
let delay = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 200;
let timer = null;
return () => {
if (timer) return;
timer = setTimeout(() => {
fn();
timer = null;
}, delay);
};
};
function htmlDecode(str) {
var div = document.createElement("div");
div.innerHTML = str;
return div.innerText;
}
const isColor = value => {
const isRgb = /^rgb\((\s*\d{1,3}\s*,\s*){2}\d{1,3}\s*\)$|^rgba\((\s*\d{1,3}\s*,\s*){2}\d{1,3}\s*,\s*\d*\.\d+\s*\)$/i;
const isHex = /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/;
return isRgb.test(value) || isHex.test(value);
};
class ImageDrop {
constructor(quill) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.quill = quill;
this.options = options;
this.editorContainer = quill.root.parentNode;
this.imageHandler = options.imageHandler;
this.uploadedImgsList = options.uploadedImgsList || [];
this.quill.on("text-change", (delta, oldDelta, source) => {
throttle(() => {
const imgs = this.quill.container.querySelectorAll('img[src^="data:"]:not(img[data-status=uploading]):not(img[data-status=fail])');
if (imgs && imgs.length > 0) {
imgs.forEach(img => {
this.uploadBase64Img(img);
});
}
})();
this.onDelete();
});
}
uploadBase64Img(img) {
console.log("upload img");
const base64Str = img.getAttribute("src");
if (typeof base64Str === "string" && /data:image\/.*;base64,/.test(base64Str)) {
const words = getI18nText(["imgStatusUploading", "imgStatusFail"], this.options.i18n);
img.setAttribute("data-status", "uploading");
img.parentNode.classList.add("img-container");
img.parentNode.setAttribute("data-after", words[0]);
const {
uploadSuccCB,
uploadFailCB
} = this.imageHandler || {};
this.b64ToUrl(base64Str).then(url => {
img.setAttribute("src", url);
img.setAttribute("data-status", "success");
img.parentNode.setAttribute("data-after", "");
this.uploadedImgsList.push(url);
if (uploadSuccCB) uploadSuccCB(url);
}).catch(error => {
console.log(error);
img.setAttribute("data-status", "fail");
img.parentNode.setAttribute("data-after", words[1]);
if (uploadFailCB) uploadFailCB(error);
img.parentNode.onclick = () => this.uploadBase64Img(img);
});
}
}
onDelete() {
const allContainers = this.quill.container.querySelectorAll(".img-container[data-after]");
if (allContainers && allContainers.length > 0) {
allContainers.forEach(container => {
const imgs = container.querySelectorAll('img[src^="data:"]');
if (!imgs || imgs.length === 0) {
container.removeAttribute("data-after");
container.removeAttribute("class");
}
});
}
}
b64ToUrl(base64) {
return new Promise((resolve, reject) => {
const block = base64.split(";");
const contentType = block[0].split(":")[1];
const realData = block[1].split(",")[1];
const blob = ImageDrop.b64toBlob(realData, contentType);
const {
imgUploadApi
} = this.imageHandler || {};
if (imgUploadApi) {
ImageDrop.uploadImg(blob, imgUploadApi, url => {
resolve(url);
}, error => {
reject(error);
});
} else {
reject(new Error("Image upload API is not defined."));
}
});
}
static getImgUrls(delta) {
return delta.ops.filter(op => op.insert && typeof op.insert === "object" && "image" in op.insert).map(op => op.insert.image);
}
static b64toBlob(b64Data) {
let contentType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
let sliceSize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 512;
const byteCharacters = atob(b64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, {
type: contentType
});
}
}
ImageDrop.uploadImg = (blob, imgUploadApi, uploadSuccCB, uploadFailCB) => {
try {
const formData = new FormData();
formData.append("file", blob, blob.name || `default.${blob.type.split("/")[1]}`);
imgUploadApi(formData).then(url => {
uploadSuccCB(url);
}).catch(error => {
console.log("upload img error: ", error);
uploadFailCB(error);
});
} catch (e) {
console.log("uploadImg: ", e);
uploadFailCB(e);
}
};
var IconAlignLeft = "<svg viewbox=\"0 0 18 18\">\n <line class=\"ql-stroke\" x1=\"3\" x2=\"15\" y1=\"9\" y2=\"9\"></line>\n <line class=\"ql-stroke\" x1=\"3\" x2=\"13\" y1=\"14\" y2=\"14\"></line>\n <line class=\"ql-stroke\" x1=\"3\" x2=\"9\" y1=\"4\" y2=\"4\"></line>\n</svg>";
var IconAlignCenter = "<svg viewbox=\"0 0 18 18\">\n <line class=\"ql-stroke\" x1=\"15\" x2=\"3\" y1=\"9\" y2=\"9\"></line>\n <line class=\"ql-stroke\" x1=\"14\" x2=\"4\" y1=\"14\" y2=\"14\"></line>\n <line class=\"ql-stroke\" x1=\"12\" x2=\"6\" y1=\"4\" y2=\"4\"></line>\n</svg>";
var IconAlignRight = "<svg viewbox=\"0 0 18 18\">\n <line class=\"ql-stroke\" x1=\"15\" x2=\"3\" y1=\"9\" y2=\"9\"></line>\n <line class=\"ql-stroke\" x1=\"15\" x2=\"5\" y1=\"14\" y2=\"14\"></line>\n <line class=\"ql-stroke\" x1=\"15\" x2=\"9\" y1=\"4\" y2=\"4\"></line>\n</svg>";
var Delete = "<svg width=\"18px\" height=\"18px\" viewBox=\"64 64 896 896\" focusable=\"false\" width=\"1em\" height=\"1em\" aria-hidden=\"true\"><path class=\"ql-fill\" d=\"M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72zm504 72H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z\"></path></svg>";
var Words = "<svg class=\"icon\" viewBox=\"0 0 1024 1024\" p-id=\"1605\" width=\"22\" height=\"22\"><path class=\"ql-fill\" d=\"M640 272H192a32 32 0 0 1 0-64h448a32 32 0 0 1 0 64zM832 480H576a32 32 0 0 1 0-64h256a32 32 0 0 1 0 64z\" fill=\"#444444\"></path><path class=\"ql-fill\" d=\"M416 816a32 32 0 0 1-32-32v-544a32 32 0 0 1 64 0v544a32 32 0 0 1-32 32zM704 816a32 32 0 0 1-32-32V448a32 32 0 0 1 64 0v336a32 32 0 0 1-32 32z\" fill=\"#444444\"></path></svg>";
var unlinkIcon = "<svg viewBox=\"0 0 1024 1024\" p-id=\"7520\" width=\"20\" height=\"20\"><path class=\"ql-fill\" d=\"M384 128v128h64V128H384zM523.328 613.824l-147.072 147.072a80 80 0 0 1-113.152-113.152l147.072-147.072-45.248-45.248-147.072 147.072a144 144 0 0 0 203.648 203.648l147.072-147.072-45.248-45.248zM613.76 523.328l45.312 45.248 147.072-147.072a144 144 0 1 0-203.648-203.648L455.424 364.928l45.248 45.248 147.072-147.072a80 80 0 0 1 113.152 113.152L613.824 523.328zM768 576h128v64h-128V576zM128 448h128V384H128v64zM640 768v128H576v-128h64zM750.848 705.6l103.808 103.744-45.312 45.248-103.68-103.68 45.184-45.312zM169.344 214.592l103.808 103.808 45.248-45.248-103.744-103.808-45.312 45.248z\"></path></svg>";
var jumpIcon = "<svg viewBox=\"0 0 1024 1024\" width=\"18\" height=\"18\"><path class=\"ql-fill\" d=\"M385.088 85.333333a42.666667 42.666667 0 1 1 0 85.333334H213.333333a42.666667 42.666667 0 0 0-42.666666 42.666666v597.333334a42.666667 42.666667 0 0 0 42.666666 42.666666h597.333334a42.666667 42.666667 0 0 0 42.666666-42.666666V640a42.666667 42.666667 0 1 1 85.333334 0v170.666667c0 70.688-57.312 128-128 128H213.333333c-70.688 0-128-57.312-128-128V213.333333c0-70.688 57.312-128 128-128z m517.44 0.501334l0.64 0.106666c0.501333 0.074667 0.992 0.170667 1.482667 0.266667l0.170666 0.042667c1.344 0.277333 2.666667 0.618667 3.946667 1.024l0.469333 0.149333a38.186667 38.186667 0 0 1 5.248 2.112c1.568 0.746667 3.114667 1.610667 4.608 2.570667l0.138667 0.106666 1.653333 1.12a42.730667 42.730667 0 0 1 5.28 4.501334l-1.098666-1.066667 0.682666 0.650667 0.426667 0.416a43.52 43.52 0 0 1 3.104 3.456l1.386667 1.834666a42.762667 42.762667 0 0 1 5.909333 11.637334l0.138667 0.458666c0.576 1.813333 1.024 3.690667 1.354666 5.6l0.106667 0.64 0.032 0.234667c0.298667 2.058667 0.458667 4.16 0.458667 6.304v277.333333a42.666667 42.666667 0 1 1-85.333334 0V230.997333L542.165333 542.165333a42.666667 42.666667 0 0 1-58.634666 1.610667l-1.706667-1.6a42.666667 42.666667 0 0 1 0-60.341333L792.992 170.666667H618.666667a42.666667 42.666667 0 0 1-42.613334-40.533334L576 128a42.666667 42.666667 0 0 1 42.666667-42.666667h277.333333a43.178667 43.178667 0 0 1 6.304 0.458667l0.224 0.042667z\" p-id=\"9528\"></path></svg>";
const iconsConfig = {
unlinkIcon,
jumpIcon
};
const genIconDom = (icon, title, className) => `<span class="${className || 'flex flex-center'}" onmouseenter="showTitle(this, '${title}')" style="width:100%;height:100%;">${icon}</span>`;
const DefaultOptions = {
modules: ["DisplaySize", "Toolbar", "Resize"],
overlayStyles: {
position: "absolute",
boxSizing: "border-box",
border: "1px dashed #444"
},
handleStyles: {
position: "absolute",
height: "12px",
width: "12px",
backgroundColor: "white",
border: "1px solid #777",
boxSizing: "border-box",
opacity: "0.80"
},
displayStyles: {
position: "absolute",
font: "12px/1.0 Arial, Helvetica, sans-serif",
padding: "4px 8px",
textAlign: "center",
backgroundColor: "white",
color: "rgb(68, 68, 68)",
border: "1px solid #777",
boxSizing: "border-box",
opacity: "0.80",
cursor: "default"
},
toolbarStyles: {
position: "absolute",
top: "-12px",
right: "0",
left: "0",
height: "0",
minWidth: "100px",
font: "12px/1.0 Arial, Helvetica, sans-serif",
textAlign: "center",
color: "#333",
boxSizing: "border-box",
cursor: "default"
},
toolbarButtonStyles: {
display: "inline-block",
width: "24px",
height: "24px",
background: "white",
border: "1px solid #999",
verticalAlign: "middle"
},
toolbarButtonSvgStyles: {
fill: "#444",
stroke: "#444",
strokeWidth: "2"
}
};
class BaseModule {
constructor(resizer) {
this.overlay = resizer.overlay;
this.img = resizer.img;
this.options = resizer.options;
this.requestUpdate = resizer.onUpdate;
this.onCreate();
}
onCreate() {}
onDestroy() {}
onUpdate() {}
}
class DisplaySize extends BaseModule {
constructor() {
super(...arguments);
this.onCreate = () => {
this.display = document.createElement("div");
Object.assign(this.display.style, this.options.displayStyles);
this.overlay.appendChild(this.display);
};
this.onUpdate = () => {
if (!this.display || !this.img) {
return;
}
const size = this.getCurrentSize();
this.display.innerHTML = size.join(" × ");
if (size[0] > 120 && size[1] > 30) {
Object.assign(this.display.style, {
right: "4px",
bottom: "4px",
left: "auto"
});
} else if (this.img.style.float === "right") {
const dispRect = this.display.getBoundingClientRect();
Object.assign(this.display.style, {
right: "auto",
bottom: `-${dispRect.height + 4}px`,
left: `-${dispRect.width + 4}px`
});
} else {
const dispRect = this.display.getBoundingClientRect();
Object.assign(this.display.style, {
right: `-${dispRect.width + 4}px`,
bottom: `-${dispRect.height + 4}px`,
left: "auto"
});
}
};
this.getCurrentSize = () => [this.img.width, Math.round(this.img.width / this.img.naturalWidth * this.img.naturalHeight)];
}
onDestroy() {
if (this.display && this.display.parentNode) {
this.display.parentNode.removeChild(this.display);
}
}
}
class Resize extends BaseModule {
constructor() {
super(...arguments);
this.isMobile = isMobile();
this.boxes = [];
this.dragStartX = 0;
this.preDragWidth = 0;
this.onCreate = () => {
this.addBox("nwse-resize");
this.addBox("nesw-resize");
this.addBox("nwse-resize");
this.addBox("nesw-resize");
this.positionBoxes();
};
this.onDestroy = () => {
this.setCursor("");
this.boxes.forEach(box => {
if (box.parentNode) {
box.parentNode.removeChild(box);
}
});
};
this.onUpdate = () => {
this.positionBoxes();
};
this.positionBoxes = () => {
const handleXOffset = `-6px`;
const handleYOffset = `-6px`;
this.boxes.forEach((box, index) => {
Object.assign(box.style, {
cursor: box.style.cursor,
width: "12px",
height: "12px",
left: index % 2 === 0 ? index < 2 ? handleXOffset : `${this.img.naturalWidth - parseInt(handleXOffset, 10)}px` : index < 2 ? `${this.img.naturalWidth - parseInt(handleXOffset, 10)}px` : handleXOffset,
top: index < 2 ? handleYOffset : `${this.img.naturalHeight - parseInt(handleYOffset, 10)}px`
});
});
};
this.addBox = cursor => {
const box = document.createElement("div");
Object.assign(box.style, this.options.handleStyles);
Object.assign(box.style, {
cursor,
width: "12px",
height: "12px"
});
const action = this.isMobile ? "touchstart" : "mousedown";
this.setCursor("");
box.addEventListener(action, this.handleMousedown, false);
this.overlay.appendChild(box);
this.boxes.push(box);
};
this.handleMousedown = evt => {
this.dragBox = evt.target;
this.dragStartX = this.isMobile ? evt.touches[0].clientX : evt.clientX;
this.preDragWidth = this.img.width || this.img.naturalWidth;
this.setCursor(this.dragBox.style.cursor);
const moveAction = this.isMobile ? "touchmove" : "mousemove";
const upAction = this.isMobile ? "touchend" : "mouseup";
document.addEventListener(moveAction, this.handleDrag, false);
document.addEventListener(upAction, this.handleMouseup, false);
evt.preventDefault();
};
this.handleMouseup = () => {
this.setCursor("");
const moveAction = this.isMobile ? "touchmove" : "mousemove";
const upAction = this.isMobile ? "touchend" : "mouseup";
document.removeEventListener(moveAction, this.handleDrag, false);
document.removeEventListener(upAction, this.handleMouseup, false);
};
this.handleDrag = evt => {
if (!this.dragBox) {
return;
}
const deltaX = this.isMobile ? evt.touches[0].clientX - this.dragStartX : evt.clientX - this.dragStartX;
const newWidth = Math.round(this.preDragWidth + deltaX);
this.img.width = newWidth > 0 ? newWidth : 0;
this.requestUpdate();
};
this.setCursor = value => {
document.body.style.cursor = value;
};
}
}
let FloatStyle, MarginStyle, DisplayStyle;
const initializeParchmentStyles = () => {
if (typeof Quill !== 'undefined' && Quill.imports && Quill.imports.parchment) {
const Parchment = Quill.imports.parchment;
FloatStyle = new Parchment.Attributor.Style("float", "float");
MarginStyle = new Parchment.Attributor.Style("margin", "margin");
DisplayStyle = new Parchment.Attributor.Style("display", "display");
} else {
setTimeout(initializeParchmentStyles, 100);
}
};
class Toolbar extends BaseModule {
constructor(resizer) {
super(resizer);
this.onCreate = () => {
this.toolbar = document.createElement("div");
Object.assign(this.toolbar.style, this.options.toolbarStyles);
this.overlay.appendChild(this.toolbar);
this._defineAlignments();
this._addToolbarButtons();
};
this._defineAlignments = () => {
if (!FloatStyle || !MarginStyle || !DisplayStyle) {
initializeParchmentStyles();
if (!FloatStyle || !MarginStyle || !DisplayStyle) {
setTimeout(() => this._defineAlignments(), 50);
return;
}
}
const blot = Quill.find(this.img);
if (!blot) {
return;
}
const index = this.quill.getIndex(blot);
this.alignments = [{
icon: IconAlignLeft,
apply: () => {
DisplayStyle.add(this.img, "inline");
FloatStyle.add(this.img, "left");
MarginStyle.add(this.img, "0 1em 1em 0");
this.quill.formatLine(index + 2, 1, "align", false);
},
isApplied: () => FloatStyle.value(this.img) === "left"
}, {
icon: IconAlignCenter,
apply: () => {
DisplayStyle.add(this.img, "block");
FloatStyle.remove(this.img);
MarginStyle.add(this.img, "auto");
this.quill.formatLine(index + 2, 1, "align", "center");
this.img.parentNode.classList.add("img-center");
},
isApplied: () => MarginStyle.value(this.img) === "auto"
}, {
icon: IconAlignRight,
apply: () => {
DisplayStyle.add(this.img, "inline");
FloatStyle.add(this.img, "right");
MarginStyle.add(this.img, "0 0 1em 1em");
this.quill.formatLine(index + 2, 1, "align", "right");
this.img.parentNode.classList.add("float-right");
},
isApplied: () => FloatStyle.value(this.img) === "right"
}, {
icon: Words,
apply: () => {
let align = false;
if (MarginStyle.value(this.img) === "auto") {
align = "center";
} else if (FloatStyle.value(this.img)) {
align = FloatStyle.value(this.img);
}
const imgRemarkPre = this.options.imgRemarkPre || getI18nText("imgRemarkPre", this.options.i18n);
this.quill.insertText(index + 1, `\n${imgRemarkPre}`, {
color: "#999999",
size: "12px"
});
this.quill.insertText(index + 2 + imgRemarkPre.length, "\n", {
align
});
this.quill.setSelection(index + 2 + imgRemarkPre.length, Quill.sources.SILENT);
this.img.setAttribute("data-remark", "1");
},
isApplied: () => this.img.getAttribute("data-remark") === "1"
}, {
icon: Delete,
apply: () => {
const imageBlot = Quill.find(this.img);
if (imageBlot) {
imageBlot?.deleteAt(0);
}
this.hide();
},
isApplied: () => false
}];
};
this._addToolbarButtons = () => {
if (!this.toolbar) return;
const buttons = [];
const words = getI18nText(["alignLeft", "alignCenter", "alignRight", "imgRemarkLabel", "deleteImg"], this.options.i18n);
this.alignments.forEach((alignment, idx) => {
const button = document.createElement("span");
buttons.push(button);
button.innerHTML = genIconDom(alignment.icon, words[idx]);
button.addEventListener("click", () => {
buttons.forEach((bt, index) => {
if (index !== 3) bt.style.filter = "";
});
if (alignment.isApplied()) {
FloatStyle.remove(this.img);
MarginStyle.remove(this.img);
DisplayStyle.remove(this.img);
} else {
this._selectButton(button);
alignment.apply();
}
this.requestUpdate();
});
Object.assign(button.style, this.options.toolbarButtonStyles);
if (idx > 0) {
button.style.borderLeftWidth = "0";
}
if (alignment.isApplied()) {
this._selectButton(button);
}
this.toolbar.appendChild(button);
});
};
this.quill = resizer.quill;
this.hide = resizer.hide;
this.options = resizer.options;
this.alignments = [];
if (!FloatStyle || !MarginStyle || !DisplayStyle) {
initializeParchmentStyles();
}
}
onDestroy() {
if (this.toolbar && this.toolbar.parentNode) {
this.toolbar.parentNode.removeChild(this.toolbar);
}
}
onUpdate() {}
_selectButton(button) {
button.style.filter = "invert(20%)";
}
}
const knownModules = {
DisplaySize,
Toolbar,
Resize
};
class ImageResize {
constructor(quill) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.modules = [];
this.initializeModules = () => {
this.removeModules();
this.modules = this.moduleClasses.map(ModuleClass => {
if (typeof ModuleClass === "string") {
return new knownModules[ModuleClass](this);
} else {
return new ModuleClass(this);
}
});
this.modules.forEach(module => {
module.onCreate();
});
this.onUpdate();
};
this.onUpdate = () => {
this.repositionElements();
this.modules.forEach(module => {
module.onUpdate();
});
};
this.removeModules = () => {
this.modules.forEach(module => {
module.onDestroy();
});
this.modules = [];
};
this.handleClick = evt => {
if (evt.target && evt.target.tagName && evt.target.tagName.toUpperCase() === "IMG") {
if (this.img === evt.target) {
return;
}
if (this.img) {
this.hide();
}
this.show(evt.target);
} else if (this.img) {
this.hide();
}
};
this.show = img => {
this.img = img;
this.showOverlay();
this.initializeModules();
};
this.showOverlay = () => {
if (this.overlay) {
this.hideOverlay();
}
this.setUserSelect("none");
document.addEventListener("keyup", this.checkImage, true);
this.quill.root.addEventListener("input", this.checkImage, true);
this.overlay = document.createElement("div");
Object.assign(this.overlay.style, this.options.overlayStyles);
this.quill.root.parentNode.appendChild(this.overlay);
this.scrollThrottle = throttle(() => {
if (this.img && this.overlay) {
this.hide();
}
});
this.quill.root.addEventListener("scroll", this.scrollThrottle);
this.repositionElements();
};
this.hideOverlay = () => {
if (!this.overlay) {
return;
}
this.quill.root.parentNode.removeChild(this.overlay);
this.overlay = undefined;
document.removeEventListener("keyup", this.checkImage);
this.quill.root.removeEventListener("input", this.checkImage);
if (this.scrollThrottle) {
this.quill.root.removeEventListener("scroll", this.scrollThrottle);
}
this.setUserSelect("");
};
this.repositionElements = () => {
if (!this.overlay || !this.img) {
return;
}
const parent = this.quill.root.parentNode;
const imgRect = this.img.getBoundingClientRect();
const containerRect = parent.getBoundingClientRect();
Object.assign(this.overlay.style, {
left: `${imgRect.left - containerRect.left - 1 + parent.scrollLeft}px`,
top: `${imgRect.top - containerRect.top + parent.scrollTop}px`,
width: `${imgRect.width}px`,
height: `${imgRect.height}px`
});
};
this.hide = () => {
this.hideOverlay();
this.removeModules();
this.img = undefined;
};
this.setUserSelect = value => {
["userSelect", "mozUserSelect", "webkitUserSelect", "msUserSelect"].forEach(prop => {
this.quill.root.style[prop] = value;
document.documentElement.style[prop] = value;
});
};
this.checkImage = evt => {
if (this.img) {
if (evt.keyCode === 46 || evt.keyCode === 8) {
const blot = Quill.find(this.img);
if (blot) {
const range = this.quill.getSelection();
if (range) {
if (range.length === 0 && range.index === this.quill.getIndex(blot)) {
blot.deleteAt(0);
} else if (range.length > 0) {
this.quill.deleteText(range.index, range.length);
} else {
blot.deleteAt(0);
}
} else {
blot.deleteAt(0);
}
}
}
this.hide();
}
};
this.quill = quill;
let moduleClasses = [];
if (options.modules) {
moduleClasses = options.modules.slice();
}
this.options = {
...DefaultOptions,
...options
};
if (moduleClasses.length > 0) {
this.options.modules = moduleClasses;
}
document.execCommand("enableObjectResizing", false, "false");
this.quill.root.addEventListener("click", this.handleClick, false);
this.quill.root.parentNode.style.position = this.quill.root.parentNode.style.position || "relative";
this.moduleClasses = this.options.modules || [];
this.modules = [];
}
}
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
const DATA_URL_DEFAULT_MIME_TYPE = 'text/plain';
const DATA_URL_DEFAULT_CHARSET = 'us-ascii';
const testParameter = (name, filters) => filters.some(filter => filter instanceof RegExp ? filter.test(name) : filter === name);
const supportedProtocols = new Set([
'https:',
'http:',
'file:',
]);
const hasCustomProtocol = urlString => {
try {
const {protocol} = new URL(urlString);
return protocol.endsWith(':')
&& !protocol.includes('.')
&& !supportedProtocols.has(protocol);
} catch {
return false;
}
};
const normalizeDataURL = (urlString, {stripHash}) => {
const match = /^data:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.exec(urlString);
if (!match) {
throw new Error(`Invalid URL: ${urlString}`);
}
let {type, data, hash} = match.groups;
const mediaType = type.split(';');
hash = stripHash ? '' : hash;
let isBase64 = false;
if (mediaType[mediaType.length - 1] === 'base64') {
mediaType.pop();
isBase64 = true;
}
// Lowercase MIME type
const mimeType = mediaType.shift()?.toLowerCase() ?? '';
const attributes = mediaType
.map(attribute => {
let [key, value = ''] = attribute.split('=').map(string => string.trim());
// Lowercase `charset`
if (key === 'charset') {
value = value.toLowerCase();
if (value === DATA_URL_DEFAULT_CHARSET) {
return '';
}
}
return `${key}${value ? `=${value}` : ''}`;
})
.filter(Boolean);
const normalizedMediaType = [
...attributes,
];
if (isBase64) {
normalizedMediaType.push('base64');
}
if (normalizedMediaType.length > 0 || (mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE)) {
normalizedMediaType.unshift(mimeType);
}
return `data:${normalizedMediaType.join(';')},${isBase64 ? data.trim() : data}${hash ? `#${hash}` : ''}`;
};
function normalizeUrl(urlString, options) {
options = {
defaultProtocol: 'http',
normalizeProtocol: true,
forceHttp: false,
forceHttps: false,
stripAuthentication: true,
stripHash: false,
stripTextFragment: true,
stripWWW: true,
removeQueryParameters: [/^utm_\w+/i],
removeTrailingSlash: true,
removeSingleSlash: true,
removeDirectoryIndex: false,
removeExplicitPort: false,
sortQueryParameters: true,
...options,
};
// Legacy: Append `:` to the protocol if missing.
if (typeof options.defaultProtocol === 'string' && !options.defaultProtocol.endsWith(':')) {
options.defaultProtocol = `${options.defaultProtocol}:`;
}
urlString = urlString.trim();
// Data URL
if (/^data:/i.test(urlString)) {
return normalizeDataURL(urlString, options);
}
if (hasCustomProtocol(urlString)) {
return urlString;
}
const hasRelativeProtocol = urlString.startsWith('//');
const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString);
// Prepend protocol
if (!isRelativeUrl) {
urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol);
}
const urlObject = new URL(urlString);
if (options.forceHttp && options.forceHttps) {
throw new Error('The `forceHttp` and `forceHttps` options cannot be used together');
}
if (options.forceHttp && urlObject.protocol === 'https:') {
urlObject.protocol = 'http:';
}
if (options.forceHttps && urlObject.protocol === 'http:') {
urlObject.protocol = 'https:';
}
// Remove auth
if (options.stripAuthentication) {
urlObject.username = '';
urlObject.password = '';
}
// Remove hash
if (options.stripHash) {
urlObject.hash = '';
} else if (options.stripTextFragment) {
urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, '');
}
// Remove duplicate slashes if not preceded by a protocol
// NOTE: This could be implemented using a single negative lookbehind
// regex, but we avoid that to maintain compatibility with older js engines
// which do not have support for that feature.
if (urlObject.pathname) {
// TODO: Replace everything below with `urlObject.pathname = urlObject.pathname.replace(/(?<!\b[a-z][a-z\d+\-.]{1,50}:)\/{2,}/g, '/');` when Safari supports negative lookbehind.
// Split the string by occurrences of this protocol regex, and perform
// duplicate-slash replacement on the strings between those occurrences
// (if any).
const protocolRegex = /\b[a-z][a-z\d+\-.]{1,50}:\/\//g;
let lastIndex = 0;
let result = '';
for (;;) {
const match = protocolRegex.exec(urlObject.pathname);
if (!match) {
break;
}
const protocol = match[0];
const protocolAtIndex = match.index;
const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex);
result += intermediate.replace(/\/{2,}/g, '/');
result += protocol;
lastIndex = protocolAtIndex + protocol.length;
}
const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length);
result += remnant.replace(/\/{2,}/g, '/');
urlObject.pathname = result;
}
// Decode URI octets
if (urlObject.pathname) {
try {
urlObject.pathname = decodeURI(urlObject.pathname);
} catch {}
}
// Remove directory index
if (options.removeDirectoryIndex === true) {
options.removeDirectoryIndex = [/^index\.[a-z]+$/];
}
if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) {
let pathComponents = urlObject.pathname.split('/');
const lastComponent = pathComponents[pathComponents.length - 1];
if (testParameter(lastComponent, options.removeDirectoryIndex)) {
pathComponents = pathComponents.slice(0, -1);
urlObject.pathname = pathComponents.slice(1).join('/') + '/';
}
}
if (urlObject.hostname) {
// Remove trailing dot
urlObject.hostname = urlObject.hostname.replace(/\.$/, '');
// Remove `www.`
if (options.stripWWW && /^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(urlObject.hostname)) {
// Each label should be max 63 at length (min: 1).
// Source: https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// Each TLD should be up to 63 characters long (min: 2).
// It is technically possible to have a single character TLD, but none currently exist.
urlObject.hostname = urlObject.hostname.replace(/^www\./, '');
}
}
// Remove query unwanted parameters
if (Array.isArray(options.removeQueryParameters)) {
// eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.
for (const key of [...urlObject.searchParams.keys()]) {
if (testParameter(key, options.removeQueryParameters)) {
urlObject.searchParams.delete(key);
}
}
}
if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) {
urlObject.search = '';
}
// Keep wanted query parameters
if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) {
// eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.
for (const key of [...urlObject.searchParams.keys()]) {
if (!testParameter(key, options.keepQueryParameters)) {
urlObject.searchParams.delete(key);
}
}
}
// Sort query parameters
if (options.sortQueryParameters) {
urlObject.searchParams.sort();
// Calling `.sort()` encodes the search parameters, so we need to decode them again.
try {
urlObject.search = decodeURIComponent(urlObject.search);
} catch {}
}
if (options.removeTrailingSlash) {
urlObject.pathname = urlObject.pathname.replace(/\/$/, '');
}
// Remove an explicit port number, excluding a default port number, if applicable
if (options.removeExplicitPort && urlObject.port) {
urlObject.port = '';
}
const oldUrlString = urlString;
// Take advantage of many of the Node `url` normalizations
urlString = urlObject.toString();
if (!options.removeSingleSlash && urlObject.pathname === '/' && !oldUrlString.endsWith('/') && urlObject.hash === '') {
urlString = urlString.replace(/\/$/, '');
}
// Remove ending `/` unless removeSingleSlash is false
if ((options.removeTrailingSlash || urlObject.pathname === '/') && urlObject.hash === '' && options.removeSingleSlash) {
urlString = urlString.replace(/\/$/, '');
}
// Restore relative protocol, if applicable
if (hasRelativeProtocol && !options.normalizeProtocol) {
urlString = urlString.replace(/^http:\/\//, '//');
}
// Remove http/https
if (options.stripProtocol) {
urlString = urlString.replace(/^(?:https?:)?\/\//, '');
}
return urlString;
}
const defaults = {
globalRegularExpression: /(https?:\/\/|www\.)[\w-\.]+\.[\w-\.]+(\/([\S]+)?)?/gi,
urlRegularExpression: /(https?:\/\/|www\.)[\w-\.]+\.[\w-\.]+(\/([\S]+)?)?/gi,
globalMailRegularExpression: /([\w-\.]+@[\w-\.]+\.[\w-\.]+)/gi,
mailRegularExpression: /([\w-\.]+@[\w-\.]+\.[\w-\.]+)/gi,
normalizeRegularExpression: /(https?:\/\/|www\.)[\S]+/i,
normalizeUrlOptions: {
stripWWW: false
}
};
class MagicUrl {
constructor(quill, options) {
this.quill = quill;
options = options || {};
this.options = {
...defaults,
...options
};
this.urlNormalizer = url => this.normalize(url);
this.mailNormalizer = mail => `mailto:${mail}`;
this.registerTypeListener();
this.registerPasteListener();
this.registerBlurListener();
}
registerPasteListener() {
this.quill.clipboard.addMatcher("A", (node, delta) => {
const href = node.getAttribute("href");
const attributes = delta.ops[0]?.attributes;
if (attributes?.link != null) {
attributes.link = href;
}
return delta;
});
this.quill.clipboard.addMatcher(Node.TEXT_NODE, (node, delta) => {
if (typeof node.data !== "string") {
return undefined;
}
const urlRegExp = this.options.globalRegularExpression;
const mailRegExp = this.options.globalMailRegularExpression;
urlRegExp.lastIndex = 0;
mailRegExp.lastIndex = 0;
const newDelta = new Delta();
let index = 0;
let urlResult = urlRegExp.exec(node.data);
let mailResult = mailRegExp.exec(node.data);
const handleMatch = (result, regExp, normalizer) => {
const head = node.data.substring(index, result.index);
newDelta.insert(head);
const match = result[0];
newDelta.insert(match, {
link: normalizer(match)
});
index = regExp.lastIndex;
return regExp.exec(node.data);
};
while (urlResult !== null || mailResult !== null) {
if (urlResult === null) {
if (mailResult) mailResult = handleMatch(mailResult, mailRegExp, this.mailNormalizer);
} else if (mailResult === null) {
urlResult = handleMatch(urlResult, urlRegExp, this.urlNormalizer);
} else if (mailResult.index <= urlResult.index) {
while (urlResult !== null && urlResult.index < mailRegExp.lastIndex) {
urlResult = urlRegExp.exec(node.data);
}
mailResult = handleMatch(mailResult, mailRegExp, this.mailNormalizer);
} else {
while (mailResult !== null && mailResult.index < urlRegExp.lastIndex) {
mailResult = mailRegExp.exec(node.data);
}
urlResult = handleMatch(urlResult, urlRegExp, this.urlNormalizer);
}
}
if (index > 0) {
const tail = node.data.substring(index);
newDelta.insert(tail);
if (delta) delta.ops = newDelta.ops;
}
return delta;
});
}
registerTypeListener() {
this.quill.on("text-change", delta => {
const ops = delta.ops;
if (!ops || ops.length < 1 || ops.length > 2) {
return;
}
const lastOp = ops[ops.length - 1];
if (!lastOp.insert || typeof lastOp.insert !== "string" || !lastOp.insert.match(/\s/)) {
return;
}
this.checkTextForUrl(!!lastOp.insert.match(/ |\t/));
});
}
registerBlurListener() {
this.quill.root.addEventListener("blur", () => {
this.checkTextForUrl();
});
}
checkTextForUrl() {
let triggeredByInlineWhitespace = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
const sel = this.quill.getSelection();
if (!sel) {
return;
}
const [leaf] = this.quill.getLeaf(sel.index);
const leafIndex = this.quill.getIndex(leaf);
if (!leaf?.text) {
return;
}
const relevantLength = sel.index - leafIndex;
const text = leaf.text.slice(0, relevantLength);
if (!text || leaf.parent.domNode.localName === "a") {
return;
}
const nextLetter = leaf.text[relevantLength];
if (nextLetter != null && nextLetter.match(/\S/)) {
return;
}
const bailOutEndingRegex = triggeredByInlineWhitespace ? /\s\s$/ : /\s$/;
if (text.match(bailOutEndingRegex)) {
return;
}
const urlMatches = text.match(this.options.urlRegularExpression);
const mailMatches = text.match(this.options.mailRegularExpression);
if (urlMatches) {
this.handleMatches(leafIndex, text, urlMatches, this.urlNormalizer);
} else if (mailMatches) {
this.handleMatches(leafIndex, text, mailMatches, this.mailNormalizer);
}
}
handleMatches(leafIndex, text, matches, normalizer) {
const match = matches.pop();
if (match) {
const matchIndex = text.lastIndexOf(match);
const after = text.split(match).pop();
if (after && after.match(/\S/)) {
return;
}
this.updateText(leafIndex + matchIndex, match.trim(), normalizer);
}
}
updateText(index, string, normalizer) {
const ops = new Delta().retain(index).retain(string.length, {
link: normalizer(string)
});
this.quill.updateContents(ops);
}
normalize(url) {
if (this.options.normalizeRegularExpression.test(url)) {
try {
return normalizeUrl(url, this.options.normalizeUrlOptions);
} catch (error) {
console.error(error);
}
}
return url;
}
}
const BlockEmbed$1 = Quill.import("blots/block/embed");
const Block = Quill.import("blots/block");
class HorizontalRule extends BlockEmbed$1 {}
HorizontalRule.blotName = "hr";
HorizontalRule.tagName = "hr";
Quill.register("formats/horizontal", HorizontalRule);
class MarkdownShortcuts {
constructor(quill, options) {
this.quill = quill;
this.options = options || {};
this.ignoreElements = this.options.ignore || [];
this.ignoreTags = ["PRE"];
const elements = [{
name: "header",
pattern: /^(#){1,6}\s/g,
action: (text, selection, pattern) => {
const match = pattern.exec(text);
if (!match) return;
const size = match[0].length;
setTimeout(() => {
this.quill.formatLine(selection.index, 0, "header", size - 1);
this.quill.deleteText(selection.index - size, size);
}, 0);
}
}, {
name: "blockquote",
pattern: /^(>)\s/g,
action: (text, selection) => {
setTimeout(() => {
this.quill.formatLine(selection.index, 1, "blockquote", true);
this.quill.deleteText(selection.index - 2, 2);
}, 0);
}
}, {
name: "code-block",
pattern: /^`{3}(?:\s|\n)/g,
action: (text, selection) => {
setTimeout(() => {
this.quill.formatLine(selection.index, 1, "code-block", true);
this.quill.deleteText(selection.index - 4, 4);
}, 0);
}
}, {
name: "bolditalic",
pattern: /(?:\*|_){3}(.+?)(?:\*|_){3}/g,
action: (text, selection, pattern, lineStart) => {
const match = pattern.exec(text);
if (!match) return;
const annotatedText = match[0];
const matchedText = match[1];
const startIndex = lineStart + match.index;
if (text.match(/^([*_ \n]+)$/g)) return;
setTimeout(() => {
this.quill.deleteText(startIndex, annotatedText.length);
this.quill.insertText(startIndex, matchedText, {
bold: true,
italic: true
});
this.quill.format("bold", false);
}, 0);
}
}, {
name: "bold",
pattern: /(?:\*|_){2}(.+?)(?:\*|_){2}/g,
action: (text, selection, pattern, lineStart) => {
const match = pattern.exec(text);
if (!match) return;
const annotatedText = match[0];
const matchedText = match[1];
const startIndex = lineStart + match.index;
if (text.match(/^([*_ \n]+)$/g)) return;
setTimeout(() => {
this.quill.deleteText(startIndex, annotatedText.length);
this.quill.insertText(startIndex, matchedText, {
bold: true
});
this.quill.format("bold", false);
}, 0);
}
}, {
name: "italic",
pattern: /(?:\*|_){1}(.+?)(?:\*|_){1}/g,
action: (text, selection, pattern, lineStart) => {
const match = pattern.exec(text);
if (!match) return;
const annotatedText = match[0];
const matchedText = match[1];
const startIndex = lineStart + match.index;
if (text.match(/^([*_ \n]+)$/g)) return;
setTimeout(() => {
this.quill.deleteText(startIndex, annotatedText.length);
this.quill.insertText(startIndex, matchedText, {
italic: true
});
this.quill.format("italic", false);
}, 0);
}
}, {
name: "strikethrough",
pattern: /(?:~~)(.+?)(?:~~)/g,
action: (text, selection, pattern, lineStart) => {
const match = pattern.exec(text);
if (!match) return;
const annotatedText = match[0];
const matchedText = match[1];
const startIndex = lineStart + match.index;
if (text.match(/^([*_ \n]+)$/g)) return;
setTimeout