UNPKG

wysiwyg4all

Version:

Free opensource minimal WYSIWYG editor for web developers

1,306 lines (1,137 loc) 139 kB
import { ColorMangle } from "colormangle"; class Wysiwyg4All { /** * Wysiwyg4All is a simple framework for building a text editor for your website. * Focused on expandability and customizations. * Additional library ColorMangle is required for text colors. * @param {{}} option - Options * @param {string} option.elementId - ID of target <DIV>. * @param {boolean} [option.editable=true] - When set to false, Wysiwyg will not be editable. By doing this, it can be used as readonly. * @param {string} [option.placeholder=''] - Add placeholder string. * @param {boolean} [option.spellcheck=false] - Set spellcheck to true/false. * @param {string | object} [option.highlightColor='teal'] - Sets the highlight color of the wysiwyg (web color name | hex | rgb | hsl). Or can provide custom color scheme object (more details in api doc). * @param {string} [option.html=''] - HTML string to load on initialization. * @param {function} [option.callback=(cb)=>{return cb}] - Setup callback function. Callback argument contains array of information such as current text style, added images, hashtags, urllinks, selected range... etc. * @param {{} | number} [option.fontSize={desktop:18, tablet: 16, phone: 14}] - Set default font size of each screen size in px. If number is given all screen size will share the same give font size. * @param {boolean} [option.lastLineBlank=false] - When set to true, Blank line will always be added on the last line of wysiwyg. * @param {boolean} [option.hashtag=false] - When set to true, wysiwyg will auto detect hashtag strings. * @param {boolean} [option.urllink=false] - When set to true, wysiwyg will auto detect url strings. * @param {boolean} [option.logMutation=false] - When set to true, wysiwyg will output dom mutation data via callback function. */ constructor(option) { console.log("Wysiwyg4All 1.0.72"); this.hashtag_regex = /#[\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC\w-]+(?:\+[\w-]+)*/g; this.hashtag_stopper_regex = /[^\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC\w-]/g; this.urllink_regex = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi; let { elementId = "", editable = true, placeholder = "", spellcheck = false, highlightColor = "teal", html = "", callback, fontSize = { desktop: "18px", tablet: "16px", phone: "14px", h1: 4.2, h2: 3.56, h3: 2.92, h4: 2.28, h5: 1.64, h6: 1.15, small: 0.8, }, lastLineBlank = false, hashtag = false, urllink = false, logMutation = false, logExecution = false, } = option; this.hashtag = hashtag; this.urllink = urllink; this.logMutation = logMutation; this.logExecution = logExecution; this.fontSizeCssVariable = {}; if (typeof fontSize === "number") this.fontSizeCssVariable = { "--wysiwyg-font-desktop": `${fontSize}`, "--wysiwyg-font-tablet": `${fontSize}`, "--wysiwyg-font-phone": `${fontSize}`, }; else if (typeof fontSize === "object" && Object.keys(fontSize).length) { let hold; let keyArr = ["desktop", "tablet", "phone"]; for (let k of keyArr) { if (fontSize[k]) { hold = fontSize[k]; if (typeof hold === "number") hold = `${hold}px`; } this.fontSizeCssVariable[`--wysiwyg-font-${k}`] = `${hold}`; } } if (!elementId || typeof elementId !== "string") throw new Error("The wysiwyg element should have an ID"); elementId = elementId[0] === "#" ? elementId.substring(1) : elementId; this.html = html; this.elementId = elementId[0] === "#" ? elementId.substring(1) : elementId; this.placeholder = placeholder; this.spellcheck = spellcheck; this.lastLineBlank = lastLineBlank; if (typeof highlightColor === "string") highlightColor = new ColorMangle(highlightColor).color; this.colorScheme = highlightColor; this.callback = callback || null; this.image_array = []; this.hashtag_array = []; this.urllink_array = []; this.custom_array = []; this.blockElement_queryArray = [ "HR", "BLOCKQUOTE", "UL", "OL", // "LI", "._media_", "._custom_", ]; this.specialTextElement_queryArray = ["._hashtag_", "._urllink_"]; this.restrictedElement_queryArray = ["._media_"]; //, "._custom_" this.textAreaElement_queryArray = [ "BLOCKQUOTE", "LI", // "TD", // "TH" ]; this.textBlockElement_queryArray = [ "P", "LI", "TD", "TH", "TR" ]; //, "TD", "TH", '._color', '._small', '._h1`', '._h2', '._h3', '._h4', '._h5', '._h6', '._b', '._i', '._u', '._del' this.ceilingElement_queryArray = [ "UL", "OL", "LI", "BLOCKQUOTE", // "TD", // "TH", // "._media_", // "._custom_", `#${elementId}`, ]; this.unSelectable_queryArray = [ "._media_", // "._custom_", "._hashtag_", "._urllink_", "HR", ]; this.needSafeGuard = [ "._media_", "._custom_", "._hashtag_", "._urllink_", "HR", // "LI", "UL", "OL", "BLOCKQUOTE", ]; this.styleAllowedElement_queryArray = [ "._backgroundColor", "._color", "._hashtag_", "._urllink_", "TD", "TH", "TR", `#${elementId}`, ]; // ALLOWED ELEMENTS FOR STYLE ATTRIBUTE <... style="..."> this.alignClass = ["_alignCenter_", "_alignRight_"]; this.hashtag_flag = false; this.urllink_flag = false; this.range_backup = null; this.commandTracker = {}; this.range = null; this.isRangeDirectionForward = true; this.insertTabPending_tabString = ""; this.removeSandwichedLine_array = []; this.lastKey = null; // setup div this.element = document.getElementById(this.elementId); if (!this.element) throw `element #${this.elementId} is null`; this.element.innerHTML = ""; this.cssVariable = new ColorMangle().colorScheme(this.colorScheme); Object.assign(this.cssVariable, this.fontSizeCssVariable); for (const v in this.cssVariable) this.element.style.setProperty(v, this.cssVariable[v]); this.elementComputedStyle = window.getComputedStyle(this.element); this.defaultFontColor = new ColorMangle( this.cssVariable["--content-text"] ).hex(); this.defaultBackgroundColor = new ColorMangle( this.cssVariable["--content"] ).hex(); this.highlightColor = new ColorMangle( this.cssVariable["--content-focus"] ).hex(); if (!this.element.classList.contains("_wysiwyg4all")) this.element.classList.add("_wysiwyg4all"); this.setPlaceholder(this.placeholder); this.setSpellcheck(this.spellcheck); // re-adjust min-height depending on padding let paddingB = this.elementComputedStyle.paddingBottom; let paddingT = this.elementComputedStyle.paddingTop; let borderT = this.elementComputedStyle.borderTopWidth; let borderB = this.elementComputedStyle.borderBottomWidth; this.element.style.minHeight = `calc(${paddingB} + ${paddingT} + ${borderT} + ${borderB} + 1.6em)`; // command style tag const command = { // [<targetClassName>, <cssProperty>, [<string: counter tag | class name>]] h1: ["_h1", "fontSize", ["_small", "_h2", "_h3", "_h4", "_h5", "_h6"]], h2: ["_h2", "fontSize", ["_small", "_h1", "_h3", "_h4", "_h5", "_h6"]], h3: ["_h3", "fontSize", ["_small", "_h1", "_h2", "_h4", "_h5", "_h6"]], h4: ["_h4", "fontSize", ["_small", "_h1", "_h2", "_h3", "_h5", "_h6"]], h5: ["_h5", "fontSize", ["_small", "_h1", "_h2", "_h3", "_h4", "_h6"]], h6: ["_h6", "fontSize", ["_small", "_h1", "_h2", "_h3", "_h4", "_h5"]], italic: ["_i", "fontStyle"], small: [ "_small", "fontSize", ["_h1", "_h2", "_h3", "_h4", "_h5", "_h6", "_b"], ], bold: ["_b", "fontWeight", ["_small"]], underline: ["_u", "textDecoration", ["_del"]], strike: ["_del", "textDecoration", ["_u"]], color: ["_color", "color"], backgroundColor: ["_backgroundColor", "backgroundColor"], }; const fontSizeRatio = { // should always be the same em value as css // h1: 4.2, // h2: 3.56, // h3: 2.92, // h4: 2.28, // h5: 1.64, // h6: 1.15, // small: 0.8, h1: fontSize.h1 || 4.2, h2: fontSize.h2 || 3.56, h3: fontSize.h3 || 2.92, h4: fontSize.h4 || 2.28, h5: fontSize.h5 || 1.64, h6: fontSize.h6 || 1.15, small: fontSize.small || 0.8, }; // // font size variables // --wysiwyg-h1: calc(var(--wysiwyg-font) * 4.2); // --wysiwyg-h2: calc(var(--wysiwyg-font) * 3.56); // --wysiwyg-h3: calc(var(--wysiwyg-font) * 2.92); // --wysiwyg-h4: calc(var(--wysiwyg-font) * 2.28); // --wysiwyg-h5: calc(var(--wysiwyg-font) * 1.64); // --wysiwyg-h6: calc(var(--wysiwyg-font) * 1.15); // --wysiwyg-small: calc(var(--wysiwyg-font) * 0.8); for (const [tag, ratio] of Object.entries(fontSizeRatio)) { if (typeof ratio === "number") { this.element.style.setProperty( `--wysiwyg-${tag}`, `calc(var(--wysiwyg-font) * ${ratio})` ); } else if (typeof ratio === "string") { if (ratio.includes("px")) { this.element.style.setProperty(`--wysiwyg-${tag}`, ratio); } else if (ratio.includes("em") || ratio.includes("rem")) { let emSize = parseFloat(ratio); if (emSize > 0) { this.element.style.setProperty( `--wysiwyg-${tag}`, `calc(var(--wysiwyg-font) * ${emSize})` ); } } } } this.styleTagOfCommand = {}; this.counterTagOf = {}; this.cssPropertyOf = {}; this.cssPropertyChecker = { textDecoration: (v) => { // v = <string: value from computedStyle> if (v.includes("underline")) return "underline"; else if (v.includes("line-through")) return "strike"; return false; }, fontSize: (v) => { // v = <string: value from computedStyle> let documentFontSize = parseFloat(this.elementComputedStyle.fontSize); let nodeFontSize = parseFloat(v); for (let tag in fontSizeRatio) { let f_size = fontSizeRatio[tag]; if (typeof f_size === "number") { // precision let compare_size = documentFontSize * f_size; let f_size_high = compare_size + 0.01; let f_size_low = compare_size - 0.01; if (f_size_high > nodeFontSize && f_size_low < nodeFontSize) return tag; } else if (typeof f_size === "string") { if (f_size.includes("px")) { if (v === f_size) return tag; } else if (f_size.includes("em") || f_size.includes("rem")) { let emSize = parseFloat(f_size); if (emSize > 0) { let compare_size = documentFontSize * emSize; let f_size_high = compare_size + 0.01; let f_size_low = compare_size - 0.01; if (f_size_high > nodeFontSize && f_size_low < nodeFontSize) return tag; } } } } return false; }, fontStyle: (v) => { // v = <string: value from computedStyle> if (v.includes("italic")) return "italic"; return false; }, }; for (let c in command) { this.commandTracker[c] = false; this.styleTagOfCommand[c] = command[c][0]; this.cssPropertyOf[command[c][0]] = command[c][1]; if (!this.cssPropertyChecker.hasOwnProperty(command[c][1])) this.cssPropertyChecker[command[c][1]] = c; if (command[c][2]) this.counterTagOf[command[c][0]] = command[c][2]; } /** this.styleTagOfCommand = { [commandKey]: <targetClassName> }; this.cssPropertyChecker = { [cssPropertyKey]: <commandKey | function(<cssValue>)> }; this.cssPropertyOf = { [targetClassName]: <cssPropertyKey> }; this.counterTagOf = { [targetClassName]: [<counterClassName>] }; this.commandTracker = { [commandKey]: <boolean> }; console.log({ styleTagOfCommand: this.styleTagOfCommand, cssPropertyChecker: this.cssPropertyChecker, cssPropertyOf: this.cssPropertyOf, counterTagOf: this.counterTagOf, commandTracker: this.commandTracker }); */ this.loadHTML(this.html, editable).catch((err) => { throw err; }); } _adjustSelection( target, ceilingElement_query = this.ceilingElement_queryArray ) { if (this.logExecution) console.log("_adjustSelection()", { target, ceilingElement_query }); let toArray = (v, allowObject = false) => { if (Array.isArray(v)) return v; else if ( (typeof v === "string" && v) || typeof v === "number" || typeof v === "boolean" || (allowObject && typeof v === "object") ) return [v]; else return []; }; let setRange = !!target; let { node = null, position = true } = target || {}; let sel = window.getSelection(); if (!sel) return null; let range; try { range = sel.getRangeAt(0); } catch (err) { if (setRange) range = document.createRange(); } if (setRange) { node = toArray(node, true); position = toArray(position, true); for (let p of position) if (typeof p !== "number" && typeof p !== "boolean" && p !== null) throw "INVALID_POSITION"; for (let n of node) { if (!(n instanceof Node) && n !== null) { if (n === false) return; throw "INVALID_NODE"; } } const setTarget = (node, position) => { if (node instanceof Node) { if (node.nodeType === 1) { if (typeof position === "boolean") while (position === false ? node.lastChild : node.firstChild) node = position === false ? node.lastChild : node.firstChild; else if (typeof position === "number") { let textLength = 0; this._nodeCrawler( (n) => { if (n.nodeType === 3 && n.textContent.length) { let length = n.textContent.length; if ( n.parentNode.getAttribute("contenteditable") === "false" ) { if (position - (textLength + length) >= 0) textLength += length; else { position = length; return "BREAK"; } return n; } else { node = n; if (position - (textLength + length) >= 0) { textLength += length; } else { position -= textLength; return "BREAK"; } } } return n; }, { node, } ); if (node.nodeType === 1) { let text = document.createTextNode("\u200B"); node.insertBefore(text, node.childNodes[0]); node = text; position = 0; } } if (node.nodeName === "BR" && node.parentNode.childNodes.length > 1) node = node.previousSibling || node; } if (typeof position === "boolean") position = position ? 0 : node.textContent.length; else position = position > node.textContent.length ? node.textContent.length : position; return { node, position }; } }; let doCollapse = false, setEnd, setStart = (() => { node[0] = node[0] === null ? range.startContainer : node[0]; position[0] = position[0] === null ? range.startOffset : position[0]; return setTarget(node[0], position[0]); })(); range.setStart(setStart.node, setStart.position); if (position.length > 1) setEnd = setTarget( (node[1] === null ? range.endContainer : node[1]) || node[0], position[1] === null ? range.endOffset : position[1] ); else { setEnd = setStart; doCollapse = true; } range.setEnd(setEnd.node, setEnd.position); if (doCollapse) range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } if (ceilingElement_query && range) { let startLine, endLine; for (let q of ceilingElement_query) { if (startLine && endLine) break; let e = range.endContainer.nodeType === 3 ? range.endContainer.parentNode : range.endContainer; let s = range.startContainer.nodeType === 3 ? range.startContainer.parentNode : range.startContainer; if (!startLine && s.closest(q)) startLine = this._climbUpToEldestParent(s, s.closest(q)); if (!endLine && e.closest(q)) endLine = this._climbUpToEldestParent(e, e.closest(q)); } range.startLine = startLine; range.endLine = endLine; } return range; } _generateId(option) { if (this.logExecution) console.log("_generateId()", { option }); let limit = 12; let prefix = ""; if (typeof option === "number") limit = option; else if (typeof option === "string") prefix = `${option}_`; const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; let text = ""; for (let i = 0; i < limit - 6; i++) text += possible.charAt( Math.floor(Math.random() * (possible.length - 1)) ); const numb = new Date().getTime().toString().substring(7, 13); // SECOND, MILLISECOND // const shuffleArray = (array) => { // let currentIndex = array.length; // let temporaryValue, randomIndex; // while (0 !== currentIndex) { // randomIndex = Math.floor(Math.random() * currentIndex); // currentIndex -= 1; // temporaryValue = array[currentIndex]; // array[currentIndex] = array[randomIndex]; // array[randomIndex] = temporaryValue; // } // return array; // }; // const letter_array = shuffleArray((text + numb).split('')); // let output = ''; // for (let i = 0; i < limit; i++) output += letter_array[i]; return prefix + numb + text; } _nodeCrawler(run, option) { if (this.logExecution) console.log("_nodeCrawler()", { run, option }); const { parentNode, node, startFromEldestChild, startNode } = option; if (startFromEldestChild && !parentNode) throw new Error("Need parentNode to crawl up single child"); if (!node || !(node instanceof Node || node?.commonAncestorContainer)) throw new Error("No node to crawl"); let outputNodes = [], nodeIsRange = !!node.commonAncestorContainer, commonContainer = null, parentAnchor; if (parentNode && parentNode instanceof Node && parentNode?.nodeType === 1) parentAnchor = parentNode; if (nodeIsRange) { commonContainer = node.commonAncestorContainer; commonContainer = commonContainer.nodeType === 3 ? commonContainer.parentNode || commonContainer : commonContainer; } else commonContainer = node; if (startFromEldestChild) commonContainer = this._climbUpToEldestParent( commonContainer, parentNode, true ); if (parentAnchor) { while ( commonContainer.nodeType === 3 || (commonContainer !== parentAnchor && commonContainer.parentNode && commonContainer.parentNode !== parentAnchor) ) commonContainer = commonContainer.parentNode; } /** crawl order below (outputs node on the way) * If 'BREAK' is returned, the node is not saved in outputNode * * start -> [ end * | ^ (finish) * v | (outputNode) * outputNode -> outputNode * * NOTE: Will not crawl when node is textNode */ if (commonContainer.nodeType === 3) { outputNodes.push(run(commonContainer)); return { nodes: outputNodes, commonContainer }; } let id, uniqueId; if (commonContainer.nodeType === 1) { uniqueId = commonContainer.id || (() => { id = this._generateId("crawl"); commonContainer.id = id; return id; })(); } let crawl = (startNode instanceof Node ? startNode : null) || (nodeIsRange ? node.startContainer : commonContainer.childNodes[0]); let endNode = nodeIsRange ? node.endContainer : commonContainer.childNodes[ commonContainer.childNodes.length ? commonContainer.childNodes.length - 1 : 0 ]; let withInRange = (cwl) => { if (!cwl || !(cwl instanceof Node)) return false; if (cwl.nodeType === 1) return cwl.id !== uniqueId && cwl.parentNode?.closest("#" + uniqueId); else return true; }; while (withInRange(crawl)) { if (crawl.nodeType === 1 && crawl.childNodes.length) { // dive down to deepest child's first crawl crawl = crawl.childNodes[0]; } else if (crawl) { // entering the deepest elements first child. if (typeof run === "function") crawl = run(crawl); if (crawl === "BREAK" || !withInRange(crawl)) break; outputNodes.push(crawl); // break out if the crawl hits the end if (crawl === endNode) break; /** * Climb up the node if the node doesn't have any next siblings * Stop when it hits the commonContainer */ let breakOut = false; while ( !crawl.nextSibling && crawl.parentNode && withInRange(crawl.parentNode) ) { crawl = crawl.parentNode; if (crawl) { if (typeof run === "function") crawl = run(crawl); if (crawl === "BREAK" || !withInRange(crawl)) { breakOut = true; break; } if (crawl) outputNodes.push(crawl); } } if (breakOut) break; // move on to next crawl crawl = crawl.nextSibling; } } // let withInRange = (cwl) => { // if (!cwl || !(cwl instanceof Node)) return false; // if (cwl.nodeType === 1) // return cwl.id !== uniqueId && cwl.parentNode?.closest("#" + uniqueId); // else if (cwl.nodeType === 3) // return cwl.parentNode && cwl.parentNode?.closest("#" + uniqueId); // // else if(nextnext) { // // // crawl = nextnext; // // return true; // // } // else return false; // }; // let diving = false; // // let nextnext = null; // while (withInRange(crawl)) { // if (!diving && crawl.nodeType === 1 && crawl.childNodes.length) { // // dive down to deepest child's first crawl // crawl = crawl.childNodes[0]; // } else if (crawl) { // diving = true; // // entering the deepest elements first child. // if (crawl.nodeType === 3) { // crawl = crawl.nextSibling || crawl.parentNode; // continue; // } // if (typeof run === "function") crawl = run.bind(this)(crawl); // if (crawl === "BREAK") break; // if (withInRange(crawl)) // outputNodes.push(crawl); // // nextnext = crawl.nextSibling?.nextSibling || crawl.parentNode; // /** // * Climb up the node if the node doesn't have any next siblings // * Stop when it hits the commonContainer // */ // if ( // crawl.nextSibling // ) { // crawl = crawl.nextSibling; // } else if (crawl.parentNode) { // if (crawl.parentNode === commonContainer) { // crawl = crawl.nextSibling; // diving = false; // } // else { // crawl = crawl.parentNode; // } // } // else { // break; // } // } // } if (id) commonContainer.removeAttribute("id"); return { node: outputNodes, commonContainer }; } _wrapNode(node, wrapper, appendWhole = false) { if (this.logExecution) console.log("_wrapNode()", { node, wrapper, appendWhole }); if (!(node instanceof Node)) return; if (!node.parentNode) throw new Error("can't unwrap document fragment"); // save current range let sel = window.getSelection(); let range = sel.getRangeAt(0); let start = null; let startOffset = range.startOffset; let end = null; let endOffset = range.endOffset; const withinRange = (n) => { if (range.startContainer === n) { start = n; } if (range.endContainer === n) { end = n; } }; if (node.nodeType === 1) { this._nodeCrawler( (n) => { withinRange(n); return n; }, { node } ); } else withinRange(node); if (wrapper) { // place the wrapper node.parentNode.insertBefore(wrapper, node); } // append node if (node.nodeType === 3) { if (wrapper) wrapper.append(node); else throw new Error("no wrapper for text content"); } else if (appendWhole) wrapper.append(node); else while (node.childNodes[0]) { let child = node.childNodes[0]; if (wrapper) wrapper.append(child); else node.parentNode.insertBefore(child, node); } let stripped; if (node.nodeType === 1 && !appendWhole) { let n = wrapper || node; let p = n.parentNode; stripped = node.previousSibling; p.removeChild(node); } // restore range if ((stripped || node).textContent && (start || end)) { if (start && start === end && startOffset === endOffset) range = this._adjustSelection({ node: stripped || node, position: startOffset, }); else range = this._adjustSelection({ node: [start, end], position: [startOffset, endOffset], }); } this.range = range; return { node: stripped || node, range }; } _climbUpToEldestParent(node, wrapper, singleChildParent = false, callback) { if (this.logExecution) console.log("_climbUpToEldestParent()", { node, wrapper, singleChildParent, callback, }); callback = callback || ((n) => { return n; }); if (!(wrapper instanceof Node) || wrapper?.nodeType === 3) throw new Error("invalid wrapper node"); let id; let uniqueId = wrapper.id || (() => { id = this._generateId("eldest"); wrapper.id = id; return id; })(); // on single parent mode climb up if parent has only 1 child or 2 child with zero space text function _isSingleChildParent(n) { if (!n || n.nodeType === 3) return false; let childrenCount = n?.children?.length; return ( childrenCount === 0 || (() => { let sweep = n.childNodes.length; let divCount = 0; let iHaveText = false; while (sweep--) { let s = n.childNodes[sweep]; if ( s.nodeType === 3 && s.textContent.length > 0 && s.textContent !== "\u200B" ) iHaveText = true; else if (s.nodeType === 1 && s.nodeName !== "BR") divCount++; // if (divCount > 1 || divCount && iHaveText) if ((divCount > 1 && !iHaveText) || (divCount && iHaveText)) return false; } return true; })() ); } while ( node?.id !== uniqueId && node.parentNode && node.parentNode.closest("#" + uniqueId) && node.parentNode.id !== uniqueId && (singleChildParent ? _isSingleChildParent(node?.parentNode) : true) ) { let cb = callback(node.parentNode); if (!cb || cb === "BREAK") break; node = cb; } if (id) wrapper.removeAttribute("id"); return node; } _getStartEndLine( range = this.range, element = this.element, getInbetween = false ) { if (this.logExecution) console.log("_getStartEndLine()", { range, element }); if (!range) return [null, null, null]; let startLine = this._climbUpToEldestParent(range.startContainer, element); let endLine = this._climbUpToEldestParent(range.endContainer, element); let inBetween = []; if (getInbetween) { // collect all the lines in between startLine and endLine. line is a block element let currentLine = startLine.nextSibling; while (currentLine && currentLine !== endLine) { if ( currentLine.nodeType === 1 && this.blockElement_queryArray.some((q) => currentLine.matches(this._classNameToQuery(q)) ) ) { inBetween.push(currentLine); } currentLine = currentLine.nextSibling; } } if (this.logExecution) console.log("startLine | endLine", { startLine, endLine, inBetween }); return [startLine, endLine, inBetween]; } _isThereContentEditableOverMyHead(node, element = this.element) { if (node && node !== this.element) { let flyup = node; while (flyup && this.element !== flyup) { if (flyup.getAttribute("contenteditable") === "true") return true; if (flyup.getAttribute("contenteditable") === "false") return false; flyup = flyup.parentNode; } } return true; } _isSelectionWithinRestrictedRange( range = this.range, element = this.element ) { if (!range) { if (this.logExecution) console.log("_isSelectionWithinRestrictedRange():true", { range, element, }); return true; } let { commonAncestorContainer, startContainer, endContainer } = range; let restrict = this.restrictedElement_queryArray; // let startLine = this._climbUpToEldestParent(startContainer, element); // let endLine = this._climbUpToEldestParent(endContainer, element); // if (this.logExecution) console.log('startLine | endLine', {startLine, endLine}); let [startLine, endLine, inBetween] = this._getStartEndLine( range, element, true ); if (startLine === endLine) { commonAncestorContainer = commonAncestorContainer.nodeType === 3 ? commonAncestorContainer.parentNode : commonAncestorContainer; for (let r of restrict) { let cl = commonAncestorContainer.closest(this._classNameToQuery(r)); if (cl) { // if (cl.getAttribute('contenteditable') !== 'true') { // return r; // } let isThere = this._isThereContentEditableOverMyHead( commonAncestorContainer, element ); if (!isThere) { return true; } } // let cl = commonAncestorContainer.closest(this._classNameToQuery(r)); // if (cl) // return r; } } else if (inBetween?.length) { for (let i = 0; i < inBetween.length; i++) { for (let r of restrict) { if (inBetween[i].closest(this._classNameToQuery(r))) { let isThere = this._isThereContentEditableOverMyHead(inBetween[i]); if (!isThere) { return true; } } } } // while (startLine && startLine !== endLine) { // startLine = startLine.nextSibling; // if (startLine) { // if (startLine.nodeType === 1) // for (let r of restrict) { // if (startLine.classList.contains(r)) { // if (startLine.getAttribute('contenteditable') !== 'true') { // return r; // } // } // // if (startLine.classList.contains(r)) // // return r; // } // } else // break; // } } return false; } _classNameToQuery(q) { if (this.logExecution) console.log("_classNameToQuery()", { q }); if (q.includes("_stop") && q[0] !== ".") return "." + q; return q[0] === "_" ? "." + q : q; } _createEmptyParagraph(append) { if (this.logExecution) console.log("_createEmptyParagraph()", { append }); let p = document.createElement("p"); if (append && typeof append === "string") append = document.createTextNode(append); p.append(append || document.createTextNode("")); if (!append) p.append(document.createElement("br")); return p; } _trackStyle(n, cls) { if (this.logExecution) console.log("_trackStyle()", { n, cls }); let commandTracker = {}; let style = window.getComputedStyle(n); for (let c of this.alignClass) { if (n.closest("." + c)) commandTracker[c.substring(1, c.length - 1)] = true; } let checker = (sp) => { let key = this.cssPropertyChecker[sp]; if (typeof key === "function") { key = key(style[sp]); if (key) { if (cls) return key; commandTracker[key] = true; } } else if (sp === "color" && style[sp]) { let col = style[sp][0] === "#" ? style[sp] : new ColorMangle(style[sp]).hex(); if (col !== this.defaultFontColor) { if (cls) return col; commandTracker[key] = col; } } else if (sp === "backgroundColor" && style[sp]) { let col = null; if (style[sp][0] === "#") col = style[sp] else { let colSplit = style[sp].split(','); if (colSplit.length === 4) { let last = colSplit[colSplit.length - 1].trim(); if (last === '0)') { return false; } } col = new ColorMangle(style[sp]).hex(); } if (col && col !== this.defaultBackgroundColor) { if (cls) return col; commandTracker[key] = col; } } else if ( style[sp] !== this.elementComputedStyle[sp] && this._isTextElement(n) ) { if (cls) return true; commandTracker[key] = true; } return false; }; // if (cls) return checker(this.cssPropertyOf[cls.toLowerCase()]); if (cls) return checker(this.cssPropertyOf[cls]); for (let sp in this.cssPropertyChecker) { checker(sp); } return commandTracker; } _lastLineBlank(force) { if (this.logExecution) console.log("_lastLineBlank()", { force }); if (this.lastLineBlank || force) { let lastLine = this.element.lastChild; if ( lastLine.nodeName !== "P" || (lastLine.nodeName === "P" && lastLine.textContent && lastLine.textContent !== "\u200B") ) { // let br = document.createElement("br"); // this.element.append(this._createEmptyParagraph(br)); this.element.append(this._createEmptyParagraph()); } } } _setEventListener(listen) { if (this.logExecution) console.log("_setEventListener()", { listen }); /** * keydown -> observer(dom change) -> selection change -> click | keyup */ document.removeEventListener("selectionchange", this._selectionchange); this.imgInput = null; // if (this.element) { // this.element.removeEventListener("keydown", this._keydown); // this.element.removeEventListener("mousedown", this._normalize); // window.removeEventListener("mousedown", this._normalize); // this.element.removeEventListener("paste", this._paste); // this.element.removeEventListener("keyup", this._keyup); // } if (!listen) return; // image selector let imgInput = document.createElement("input"); for (const [key, value] of Object.entries({ id: this._generateId("imageInput"), type: "file", accept: "image/gif,image/png,image/jpeg,image/webp", hidden: true, multiple: true, })) { imgInput.setAttribute(key, value); } this.imgInput = imgInput; this.imgInput.addEventListener("change", (e) => { this._imageSelected(e).catch((err) => { console.error(err); }); }); this._keydown = function (e) { if (this._isSelectionWithinRestrictedRange()) { if (this.logExecution) console.log("_keydown(): restricted range", { e }); return; } this._modifySelection(() => { if (!this.range) return; let { startContainer, startOffset, collapsed, startLine, endLine } = this.range; let key = e.key.toUpperCase(); let shift = e.shiftKey; this.lastKey = key; if (key === "ENTER" && e.shiftKey) { // prevent shift+enter if (!this.range.endLine.closest("LI")) e.preventDefault(); return; } // delete key if (["BACKSPACE", "DELETE"].includes(key)) { this.isRangeDirectionForward = true; if (this.logExecution) console.log("_keydown(): delete key", { e }); // if ( // this.element.childNodes.length === 1 && // this._isTextBlockElement(this.element.childNodes[0]) && // this.element.childNodes[0].textContent.length === 0 // ) { // if(this.logExecution) console.log('dead end'); // e.preventDefault(); // // Optionally, reset to a blank paragraph // // this.element.childNodes[0].innerHTML = '<br>'; // // this.range = this._adjustSelection({ node: this.element.childNodes[0], position: 0 }); // this._lastLineBlank(true); // } if ( !this.element.textContent && this.element.childNodes.length <= 1 && this._isTextElement(this.element.childNodes[0]) && this.element.childNodes[0] === startLine ) { if (this.logExecution) console.log("_keydown(): delete key", "nothing to delete"); // there is nothing to delete e.preventDefault(); return; } // Prevent potential quirk where the browser removes the whole element if ( this.element.childNodes.length === 1 && this._isTextBlockElement(this.element.childNodes[0]) && this.element.childNodes[0].textContent.length === 0 ) { if (this.logExecution) console.log("_keydown(): delete key", "dead end"); e.preventDefault(); // Optionally, reset to a blank paragraph this.element.childNodes[0].innerHTML = "<p><br></p>"; this.range = this._adjustSelection({ node: this.element.childNodes[0], position: 0, }); this._lastLineBlank(true); return; } let stc = this.range.startContainer; if (this.range.collapsed) { let block = (stc.nodeType === 3 ? stc.parentNode : stc).closest( "blockquote" ); if ( block && block.childNodes[0] === this._climbUpToEldestParent(stc, block) && this.range.startOffset === 0 ) { // if the block is empty and the cursor is on the first offset position within the blockquote // cursor is on the first offset position within the blockquote if (this.logExecution) console.log("_keydown(): delete key", "block is empty and the cursor is on the first offset position within the blockquote" ); e.preventDefault(); this.command("quote"); return; } if (this.range.startOffset === 0) { if (this.logExecution) console.log("_keydown(): delete key", "this.range.startOffset === 0" ); let ceil = this._climbUpToEldestParent( stc, this.element ) let ceil_prev = ceil?.previousSibling; if(ceil_prev) {