UNPKG

quill-react-pro

Version:

A Quill component for React and more maturely.

1,362 lines (1,291 loc) 110 kB
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(" &times; "); 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