wysiwyg4all
Version:
Free opensource minimal WYSIWYG editor for web developers
1,306 lines (1,137 loc) • 139 kB
JavaScript
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) {