diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eb647b..63f422f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file, starting fr The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.8] - 2024-02-21 + +### Fixed + +- Fix some keyboard shortcuts not working on some platforms. +- Fix unable to paste text with new line on Android. + ## [2.2.7] - 2024-02-21 ### Fixed diff --git a/dist/squire-raw.js b/dist/squire-raw.js index 82b63b7..caf607b 100644 --- a/dist/squire-raw.js +++ b/dist/squire-raw.js @@ -1909,6 +1909,10 @@ } let key = event.key; let modifiers = ""; + const code = event.code; + if (/^Digit\d$/.test(code)) { + key = code.slice(-1); + } if (key !== "Backspace" && key !== "Delete") { if (event.altKey) { modifiers += "Alt-"; @@ -2044,7 +2048,10 @@ event.preventDefault(); self.undo(); }; - keyHandlers[ctrlKey + "y"] = keyHandlers[ctrlKey + "Shift-z"] = (self, event) => { + keyHandlers[ctrlKey + "y"] = // Depending on platform, the Shift may cause the key to come through as + // upper case, but sometimes not. Just add both as shortcuts — the browser + // will only ever fire one or the other. + keyHandlers[ctrlKey + "Shift-z"] = keyHandlers[ctrlKey + "Shift-Z"] = (self, event) => { event.preventDefault(); self.redo(); }; @@ -2226,11 +2233,6 @@ } _beforeInput(event) { switch (event.inputType) { - case "insertText": - if (isAndroid && event.data && event.data.includes("\n")) { - event.preventDefault(); - } - break; case "insertLineBreak": event.preventDefault(); this.splitBlock(true); diff --git a/dist/squire-raw.mjs b/dist/squire-raw.mjs index 8ebb011..6af222b 100644 --- a/dist/squire-raw.mjs +++ b/dist/squire-raw.mjs @@ -1906,6 +1906,10 @@ var _onKey = function(event) { } let key = event.key; let modifiers = ""; + const code = event.code; + if (/^Digit\d$/.test(code)) { + key = code.slice(-1); + } if (key !== "Backspace" && key !== "Delete") { if (event.altKey) { modifiers += "Alt-"; @@ -2041,7 +2045,10 @@ keyHandlers[ctrlKey + "z"] = (self, event) => { event.preventDefault(); self.undo(); }; -keyHandlers[ctrlKey + "y"] = keyHandlers[ctrlKey + "Shift-z"] = (self, event) => { +keyHandlers[ctrlKey + "y"] = // Depending on platform, the Shift may cause the key to come through as +// upper case, but sometimes not. Just add both as shortcuts — the browser +// will only ever fire one or the other. +keyHandlers[ctrlKey + "Shift-z"] = keyHandlers[ctrlKey + "Shift-Z"] = (self, event) => { event.preventDefault(); self.redo(); }; @@ -2223,11 +2230,6 @@ var Squire = class { } _beforeInput(event) { switch (event.inputType) { - case "insertText": - if (isAndroid && event.data && event.data.includes("\n")) { - event.preventDefault(); - } - break; case "insertLineBreak": event.preventDefault(); this.splitBlock(true); diff --git a/dist/squire.js b/dist/squire.js index b318ff0..f449e41 100644 --- a/dist/squire.js +++ b/dist/squire.js @@ -1,11 +1,10 @@ -"use strict";(()=>{var st=()=>!0,T=class{constructor(t,e,n){this.root=t,this.currentNode=t,this.nodeType=e,this.filter=n||st}isAcceptableNode(t){let e=t.nodeType;return!!((e===Node.ELEMENT_NODE?1:e===Node.TEXT_NODE?4:0)&this.nodeType)&&this.filter(t)}nextNode(){let t=this.root,e=this.currentNode,n;for(;;){for(n=e.firstChild;!n&&e&&e!==t;)n=e.nextSibling,n||(e=e.parentNode);if(!n)return null;if(this.isAcceptableNode(n))return this.currentNode=n,n;e=n}}previousNode(){let t=this.root,e=this.currentNode,n;for(;;){if(e===t)return null;if(n=e.previousSibling,n)for(;e=n.lastChild;)n=e;else n=e.parentNode;if(!n)return null;if(this.isAcceptableNode(n))return this.currentNode=n,n;e=n}}previousPONode(){let t=this.root,e=this.currentNode,n;for(;;){for(n=e.lastChild;!n&&e&&e!==t;)n=e.previousSibling,n||(e=e.parentNode);if(!n)return null;if(this.isAcceptableNode(n))return this.currentNode=n,n;e=n}}};var y="\u200B",J=navigator.userAgent,de=/Mac OS X/.test(J),fe=/Windows NT/.test(J),_e=/iP(?:ad|hone|od)/.test(J)||de&&!!navigator.maxTouchPoints,Fe=/Android/.test(J),He=/Gecko\//.test(J),se=/Edge\//.test(J),rt=!se&&/WebKit\//.test(J),B=de||_e?"Meta-":"Ctrl-",oe=rt,Pe="onbeforeinput"in document&&"inputType"in new InputEvent("input"),I=/[^ \t\r\n]/;var ct=/^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/,dt=new Set(["BR","HR","IFRAME","IMG","INPUT"]),ft=0,xe=1,Ue=2,qe=3,ue=new WeakMap,We=()=>{ue=new WeakMap},M=o=>dt.has(o.nodeName),be=o=>{switch(o.nodeType){case 3:return xe;case 1:case 11:if(ue.has(o))return ue.get(o);break;default:return ft}let t;return Array.from(o.childNodes).every(N)?ct.test(o.nodeName)?t=xe:t=Ue:t=qe,ue.set(o,t),t},N=o=>be(o)===xe,P=o=>be(o)===Ue,Q=o=>be(o)===qe;var p=(o,t,e)=>{let n=document.createElement(o);if(t instanceof Array&&(e=t,t=null),t)for(let i in t){let s=t[i];s!==void 0&&n.setAttribute(i,s)}return e&&e.forEach(i=>n.appendChild(i)),n},Le=(o,t)=>M(o)||o.nodeType!==t.nodeType||o.nodeName!==t.nodeName?!1:o instanceof HTMLElement&&t instanceof HTMLElement?o.nodeName!=="A"&&o.className===t.className&&o.style.cssText===t.style.cssText:!0,he=(o,t,e)=>{if(o.nodeName!==t)return!1;for(let n in e)if(!("getAttribute"in o)||o.getAttribute(n)!==e[n])return!1;return!0},S=(o,t,e,n)=>{for(;o&&o!==t;){if(he(o,e,n))return o;o=o.parentNode}return null},me=(o,t)=>{let e=o.childNodes;for(;t&&o instanceof Element;)o=e[t-1],e=o.childNodes,t=e.length;return o},Re=(o,t)=>{let e=o;if(e instanceof Element){let n=e.childNodes;if(to instanceof Element||o instanceof DocumentFragment?o.childNodes.length:o instanceof CharacterData?o.length:0,C=o=>{let t=document.createDocumentFragment(),e=o.firstChild;for(;e;)t.appendChild(e),e=o.firstChild;return t},E=o=>{let t=o.parentNode;return t&&t.removeChild(o),o},R=(o,t)=>{let e=o.parentNode;e&&e.replaceChild(t,o)};var ut=o=>o instanceof Element?o.nodeName==="BR":I.test(o.data),ee=(o,t)=>{let e=o.parentNode;for(;N(e);)e=e.parentNode;let n=new T(e,5,ut);return n.currentNode=o,!!n.nextNode()||t&&!n.previousNode()},re=(o,t)=>{let e=new T(o,4),n,i;for(;n=e.nextNode();)for(;(i=n.data.indexOf(y))>-1&&(!t||n.parentNode!==t);)if(n.length===1){let s=n,r=s.parentNode;for(;r&&(r.removeChild(s),e.currentNode=r,!(!N(r)||k(r)));)s=r,r=s.parentNode;break}else n.deleteData(i,1)};var ht=0,mt=1,pt=2,Nt=3,K=(o,t,e)=>{let n=document.createRange();if(n.selectNode(t),e){let i=o.compareBoundaryPoints(Nt,n)>-1,s=o.compareBoundaryPoints(mt,n)<1;return!i&&!s}else{let i=o.compareBoundaryPoints(ht,n)<1,s=o.compareBoundaryPoints(pt,n)>-1;return i&&s}},_=o=>{let{startContainer:t,startOffset:e,endContainer:n,endOffset:i}=o;for(;!(t instanceof Text);){let s=t.childNodes[e];if(!s||M(s)){if(e&&(s=t.childNodes[e-1],s instanceof Text)){let r=s,l;for(;!r.length&&(l=r.previousSibling)&&l instanceof Text;)r.remove(),r=l;t=r,e=r.data.length}break}t=s,e=0}if(i)for(;!(n instanceof Text);){let s=n.childNodes[i-1];if(!s||M(s)){if(s&&s.nodeName==="BR"&&!ee(s,!1)){i-=1;continue}break}n=s,i=k(n)}else for(;!(n instanceof Text);){let s=n.firstChild;if(!s||M(s))break;n=s}o.setStart(t,e),o.setEnd(n,i)},q=(o,t,e,n)=>{let i=o.startContainer,s=o.startOffset,r=o.endContainer,l=o.endOffset,a;for(t||(t=o.commonAncestorContainer),e||(e=t);!s&&i!==t&&i!==n;)a=i.parentNode,s=Array.from(a.childNodes).indexOf(i),i=a;for(;!(r===e||r===n||(r.nodeType!==3&&r.childNodes[l]&&r.childNodes[l].nodeName==="BR"&&!ee(r.childNodes[l],!1)&&(l+=1),l!==k(r)));)a=r.parentNode,l=Array.from(a.childNodes).indexOf(r)+1,r=a;o.setStart(i,s),o.setEnd(r,l)},Oe=(o,t,e)=>{let n=S(o.endContainer,e,t);if(n&&(n=n.parentNode)){let i=o.cloneRange();q(i,n,n,e),i.endContainer===n&&(o.setStart(i.endContainer,i.endOffset),o.setEnd(i.endContainer,i.endOffset))}return o};var x=o=>{let t=null;if(o instanceof Text)return o;if(N(o)){let e=o.firstChild;if(oe)for(;e&&e instanceof Text&&!e.data;)o.removeChild(e),e=o.firstChild;e||(oe?t=document.createTextNode(y):t=document.createTextNode(""))}else if((o instanceof Element||o instanceof DocumentFragment)&&!o.querySelector("BR")){t=p("BR");let e=o,n;for(;(n=e.lastElementChild)&&!N(n);)e=n;o=e}if(t)try{o.appendChild(t)}catch(e){}return o},D=(o,t)=>{let e=null;return Array.from(o.childNodes).forEach(n=>{let i=n.nodeName==="BR";!i&&N(n)?(e||(e=p("DIV")),e.appendChild(n)):(i||e)&&(e||(e=p("DIV")),x(e),i?o.replaceChild(e,n):o.insertBefore(e,n),e=null),Q(n)&&D(n,t)}),e&&o.appendChild(x(e)),o},w=(o,t,e,n)=>{if(o instanceof Text&&o!==e){if(typeof t!="number")throw new Error("Offset must be a number to split text node!");if(!o.parentNode)throw new Error("Cannot split text node with no parent!");return w(o.parentNode,o.splitText(t),e,n)}let i=typeof t=="number"?t{let e=o.childNodes,n=e.length,i=[];for(;n--;){let s=e[n],r=n?e[n-1]:null;if(r&&N(s)&&Le(s,r))t.startContainer===s&&(t.startContainer=r,t.startOffset+=k(r)),t.endContainer===s&&(t.endContainer=r,t.endOffset+=k(r)),t.startContainer===o&&(t.startOffset>n?t.startOffset-=1:t.startOffset===n&&(t.startContainer=r,t.startOffset=k(r))),t.endContainer===o&&(t.endOffset>n?t.endOffset-=1:t.endOffset===n&&(t.endContainer=r,t.endOffset=k(r))),E(s),s instanceof Text?r.appendData(s.data):i.push(C(s));else if(s instanceof Element){let l;for(;l=i.pop();)s.appendChild(l);Ke(s,t)}}},te=(o,t)=>{let e=o instanceof Text?o.parentNode:o;if(e instanceof Element){let n={startContainer:t.startContainer,startOffset:t.startOffset,endContainer:t.endContainer,endOffset:t.endOffset};Ke(e,n),t.setStart(n.startContainer,n.startOffset),t.setEnd(n.endContainer,n.endOffset)}},Y=(o,t,e,n)=>{let i=t,s,r;for(;(s=i.parentNode)&&s!==n&&s instanceof Element&&s.childNodes.length===1;)i=s;E(i),r=o.childNodes.length;let l=o.lastChild;l&&l.nodeName==="BR"&&(o.removeChild(l),r-=1),o.appendChild(C(t)),e.setStart(o,r),e.collapse(!0),te(o,e)},F=(o,t)=>{let e=o.previousSibling,n=o.firstChild,i=o.nodeName==="LI";if(!(i&&(!n||!/^[OU]L$/.test(n.nodeName)))){if(e&&Le(e,o)){if(!Q(e))if(i){let r=p("DIV");r.appendChild(C(e)),e.appendChild(r)}else return;E(o);let s=!Q(o);e.appendChild(C(o)),s&&D(e,t),n&&F(n,t)}else if(i){let s=p("DIV");o.insertBefore(s,n),x(s)}}};var ze={"font-weight":{regexp:/^bold|^700/i,replace(){return p("B")}},"font-style":{regexp:/^italic/i,replace(){return p("I")}},"font-family":{regexp:I,replace(o,t){return p("SPAN",{class:o.fontFamily,style:"font-family:"+t})}},"font-size":{regexp:I,replace(o,t){return p("SPAN",{class:o.fontSize,style:"font-size:"+t})}},"text-decoration":{regexp:/^underline/i,replace(){return p("U")}}},St=(o,t,e)=>{let n=o.style,i,s;for(let r in ze){let l=ze[r],a=n.getPropertyValue(r);if(a&&l.regexp.test(a)){let d=l.replace(e.classNames,a);if(d.nodeName===o.nodeName&&d.className===o.className)continue;s||(s=d),i&&i.appendChild(d),i=d,o.style.removeProperty(r)}}return s&&i&&(i.appendChild(C(o)),o.style.cssText?o.appendChild(s):R(o,s)),i||o},pe=o=>(t,e)=>{let n=p(o),i=t.attributes;for(let s=0,r=i.length;s{let n=o,i=n.face,s=n.size,r=n.color,l=e.classNames,a,d,c,f,u;return i&&(a=p("SPAN",{class:l.fontFamily,style:"font-family:"+i}),u=a,f=a),s&&(d=p("SPAN",{class:l.fontSize,style:"font-size:"+gt[s]+"px"}),u||(u=d),f&&f.appendChild(d),f=d),r&&/^#?([\dA-F]{3}){1,2}$/i.test(r)&&(r.charAt(0)!=="#"&&(r="#"+r),c=p("SPAN",{class:l.color,style:"color:"+r}),u||(u=c),f&&f.appendChild(c),f=c),(!u||!f)&&(u=f=p("SPAN")),t.replaceChild(u,n),f.appendChild(C(n)),f},TT:(o,t,e)=>{let n=p("SPAN",{class:e.classNames.fontFamily,style:'font-family:menlo,consolas,"courier new",monospace'});return t.replaceChild(n,o),n.appendChild(C(o)),n}},Tt=/^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|IGCAPTION|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|COL(?:GROUP)?|UL)$/,Ct=/^(?:HEAD|META|STYLE)/,Ne=(o,t,e)=>{let n=o.childNodes,i=o;for(;N(i);)i=i.parentNode;let s=new T(i,5);for(let r=0,l=n.length;r{let t=o.childNodes,e=t.length;for(;e--;){let n=t[e];n instanceof Element&&!M(n)?(Se(n),N(n)&&!n.firstChild&&o.removeChild(n)):n instanceof Text&&!n.data&&o.removeChild(n)}},le=(o,t,e)=>{let n=o.querySelectorAll("BR"),i=[],s=n.length;for(let r=0;ro.split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""");var ae=(o,t)=>{let e=new T(t,1,P);return e.currentNode=o,e},z=(o,t)=>{let e=ae(o,t).previousNode();return e!==t?e:null},W=(o,t)=>{let e=ae(o,t).nextNode();return e!==t?e:null},ce=o=>!o.textContent&&!o.querySelector("IMG");var L=(o,t)=>{let e=o.startContainer,n;if(N(e))n=z(e,t);else if(e!==t&&e instanceof HTMLElement&&P(e))n=e;else{let i=me(e,o.startOffset);n=W(i,t)}return n&&K(o,n,!0)?n:null},G=(o,t)=>{let e=o.endContainer,n;if(N(e))n=z(e,t);else if(e!==t&&e instanceof HTMLElement&&P(e))n=e;else{let i=Re(e,o.endOffset);if(!i||!t.contains(i)){i=t;let s;for(;s=i.lastChild;)i=s}n=z(i,t)}return n&&K(o,n,!0)?n:null},Ge=o=>o instanceof Text?I.test(o.data):o.nodeName==="IMG",X=(o,t)=>{let e=o.startContainer,n=o.startOffset,i;if(e instanceof Text){let l=e.data;for(let a=n;a>0;a-=1)if(l.charAt(a-1)!==y)return!1;i=e}else if(i=Re(e,n),i&&!t.contains(i)&&(i=null),!i&&(i=me(e,n),i instanceof Text&&i.length))return!1;let s=L(o,t);if(!s)return!1;let r=new T(s,5,Ge);return r.currentNode=i,!r.previousNode()},Z=(o,t)=>{let e=o.endContainer,n=o.endOffset,i;if(e instanceof Text){let l=e.data,a=l.length;for(let d=n;d{let e=L(o,t),n=G(o,t),i;e&&n&&(i=e.parentNode,o.setStart(i,Array.from(i.childNodes).indexOf(e)),i=n.parentNode,o.setEnd(i,Array.from(i.childNodes).indexOf(n)+1))};function V(o,t,e,n){let i=document.createRange();return i.setStart(o,t),e&&typeof n=="number"?i.setEnd(e,n):i.setEnd(o,t),i}var j=(o,t)=>{let{startContainer:e,startOffset:n,endContainer:i,endOffset:s}=o,r;if(e instanceof Text){let a=e.parentNode;if(r=a.childNodes,n===e.length)n=Array.from(r).indexOf(e)+1,o.collapsed&&(i=a,s=n);else{if(n){let d=e.splitText(n);i===e?(s-=n,i=d):i===a&&(s+=1),e=d}n=Array.from(r).indexOf(e)}e=a}else r=e.childNodes;let l=r.length;n===l?e.appendChild(t):e.insertBefore(t,r[n]),e===i&&(s+=r.length-l),o.setStart(e,n),o.setEnd(i,s)},Be=(o,t,e)=>{let n=document.createDocumentFragment();if(o.collapsed)return n;t||(t=o.commonAncestorContainer),t instanceof Text&&(t=t.parentNode);let i=o.startContainer,s=o.startOffset,r=w(o.endContainer,o.endOffset,t,e),l=0,a=w(i,s,t,e);for(;a&&a!==r;){let d=a.nextSibling;n.appendChild(a),a=d}return i instanceof Text&&r instanceof Text&&(i.appendData(r.data),E(r),r=i,l=s),o.setStart(i,s),r?o.setEnd(r,l):o.setEnd(t,t.childNodes.length),x(t),n},Ze=(o,t,e)=>{o.currentNode=e;let n;for(;n=o[t]();){if(n instanceof Text||M(n))return n;if(!N(n))return null}return null},H=(o,t)=>{let e=L(o,t),n=G(o,t),i=e!==n;e&&n&&(_(o),q(o,e,n,t));let s=Be(o,null,t);_(o),i&&(n=G(o,t),e&&n&&e!==n&&Y(e,n,o,t)),e&&x(e);let r=t.firstChild;(!r||r.nodeName==="BR")&&(x(t),t.firstChild&&o.selectNodeContents(t.firstChild)),o.collapse(!0);let l=o.startContainer,a=o.startOffset,d=new T(t,5),c=l,f=a;(!(c instanceof Text)||f===c.data.length)&&(c=Ze(d,"nextNode",c),f=0);let u=l,m=a-1;(!(u instanceof Text)||m===-1)&&(u=Ze(d,"previousPONode",c||(l instanceof Text?l:l.childNodes[a]||l)),u instanceof Text&&(m=u.data.length));let h=null,g=0;return c instanceof Text&&c.data.charAt(f)===" "&&X(o,t)?(h=c,g=f):u instanceof Text&&u.data.charAt(m)===" "&&(c instanceof Text&&c.data.charAt(f)===" "||Z(o,t))&&(h=u,g=m),h&&h.replaceData(g,1,"\xA0"),o.setStart(l,a),o.collapse(!0),s},je=(o,t,e)=>{let n=t.firstChild&&N(t.firstChild),i;for(D(t,e),i=t;i=W(i,e);)x(i);o.collapsed||H(o,e),_(o),o.collapse(!1);let s=S(o.endContainer,e,"BLOCKQUOTE")||e,r=L(o,e),l=null,a=W(t,t),d=!n&&!!r&&ce(r);if(r&&a&&!d&&!S(a,t,"PRE")&&!S(a,t,"TABLE")){q(o,r,r,e),o.collapse(!0);let c=o.endContainer,f=o.endOffset;if(le(r,e,!1),N(c)){let u=w(c,f,z(c,e)||e,e);c=u.parentNode,f=Array.from(c.childNodes).indexOf(u)}if(f!==k(c))for(l=document.createDocumentFragment();i=c.childNodes[f];)l.appendChild(i);Y(c,a,o,e),f=Array.from(c.parentNode.childNodes).indexOf(c)+1,c=c.parentNode,o.setEnd(c,f)}if(k(t)){d&&r&&(o.setEndBefore(r),o.collapse(!1),E(r)),q(o,s,s,e);let c=w(o.endContainer,o.endOffset,s,e),f=c?c.previousSibling:s.lastChild;s.insertBefore(t,c),c?o.setEndBefore(c):o.setEnd(s,k(s)),r=G(o,e),_(o);let u=o.endContainer,m=o.endOffset;c&&Q(c)&&F(c,e),c=f&&f.nextSibling,c&&Q(c)&&F(c,e),o.setEnd(u,m)}if(l&&r){let c=o.cloneRange();x(l),Y(r,l,c,e),o.setEnd(c.endContainer,c.endOffset)}_(o)};var ge=o=>{if(o.collapsed)return"";let t=o.startContainer,e=o.endContainer,n=new T(o.commonAncestorContainer,5,a=>K(o,a,!0));n.currentNode=t;let i=t,s="",r=!1,l;for((!(i instanceof Element)&&!(i instanceof Text)||!n.filter(i))&&(i=n.nextNode());i;)i instanceof Text?(l=i.data,l&&/\S/.test(l)&&(i===e&&(l=l.slice(0,o.endOffset)),i===t&&(l=l.slice(o.startOffset)),s+=l,r=!0)):(i.nodeName==="BR"||r&&!N(i))&&(s+=` -`,r=!1),i=n.nextNode();return s=s.replace(/ /g," "),s};var De=Array.prototype.indexOf,Qe=(o,t,e,n,i,s,r)=>{let l=o.clipboardData;if(se||!l)return!1;let a=s?"":ge(t),d=L(t,e),c=G(t,e),f=e;d===c&&(d!=null&&d.contains(t.commonAncestorContainer))&&(f=d);let u;n?u=H(t,e):(t=t.cloneRange(),_(t),q(t,f,f,e),u=t.cloneContents());let m=t.commonAncestorContainer;for(m instanceof Text&&(m=m.parentNode);m&&m!==f;){let g=m.cloneNode(!1);g.appendChild(u),u=g,m=m.parentNode}let h;if(u.childNodes.length===1&&u.childNodes[0]instanceof Text)a=u.childNodes[0].data.replace(/ /g," "),r=!0;else{let g=p("DIV");g.appendChild(u),h=g.innerHTML,i&&(h=i(h))}return s&&h!==void 0&&(a=s(h)),fe&&(a=a.replace(/\r?\n/g,`\r -`)),!r&&h&&a!==h&&l.setData("text/html",h),l.setData("text/plain",a),o.preventDefault(),!0},Xe=function(o){let t=this.getSelection(),e=this._root;if(t.collapsed){o.preventDefault();return}this.saveUndoState(t),Qe(o,t,e,!0,this._config.willCutCopy,this._config.toPlainText,!1)||setTimeout(()=>{try{this._ensureBottomLine()}catch(i){this._config.didError(i)}},0),this.setSelection(t)},Ve=function(o){Qe(o,this.getSelection(),this._root,!1,this._config.willCutCopy,this._config.toPlainText,!1)},Ae=function(o){this._isShiftDown=o.shiftKey},$e=function(o){let t=o.clipboardData,e=t==null?void 0:t.items,n=this._isShiftDown,i=!1,s=!1,r=null,l=null;if(e){let v=e.length;for(;v--;){let O=e[v],A=O.type;A==="text/html"?l=O:A==="text/plain"||A==="text/uri-list"?r=O:A==="text/rtf"?i=!0:/^image\/.*/.test(A)&&(s=!0)}if(s&&!(i&&l)){o.preventDefault(),this.fireEvent("pasteImage",{clipboardData:t});return}if(!se){o.preventDefault(),l&&(!n||!r)?l.getAsString(O=>{this.insertHTML(O,!0)}):r&&r.getAsString(O=>{let A=!1,Ie=this.getSelection();if(!Ie.collapsed&&I.test(Ie.toString())){let Me=this.linkRegExp.exec(O);A=!!Me&&Me[0].length===O.length}A?this.makeLink(O):this.insertPlainText(O,!0)});return}}let a=t==null?void 0:t.types;if(!se&&a&&(De.call(a,"text/html")>-1||!He&&De.call(a,"text/plain")>-1&&De.call(a,"text/rtf")<0)){o.preventDefault();let v;!n&&(v=t.getData("text/html"))?this.insertHTML(v,!0):((v=t.getData("text/plain"))||(v=t.getData("text/uri-list")))&&this.insertPlainText(v,!0);return}let d=document.body,c=this.getSelection(),f=c.startContainer,u=c.startOffset,m=c.endContainer,h=c.endOffset,g=p("DIV",{contenteditable:"true",style:"position:fixed; overflow:hidden; top:0; right:100%; width:1px; height:1px;"});d.appendChild(g),c.selectNodeContents(g),this.setSelection(c),setTimeout(()=>{try{let v="",O=g,A;for(;g=O;)O=g.nextSibling,E(g),A=g.firstChild,A&&A===g.lastChild&&A instanceof HTMLDivElement&&(g=A),v+=g.innerHTML;this.setSelection(V(f,u,m,h)),v&&this.insertHTML(v,!0)}catch(v){this._config.didError(v)}},0)},Ye=function(o){if(!o.dataTransfer)return;let t=o.dataTransfer.types,e=t.length,n=!1,i=!1;for(;e--;)switch(t[e]){case"text/plain":n=!0;break;case"text/html":i=!0;break;default:return}(i||n&&this.saveUndoState)&&this.saveUndoState()};var we=(o,t,e)=>{t.preventDefault(),o.splitBlock(t.shiftKey,e)};var ne=(o,t)=>{try{t||(t=o.getSelection());let e=t.startContainer;e instanceof Text&&(e=e.parentNode);let n=e;for(;N(n)&&(!n.textContent||n.textContent===y);)e=n,n=e.parentNode;e!==n&&(t.setStart(n,Array.from(n.childNodes).indexOf(e)),t.collapse(!0),n.removeChild(e),P(n)||(n=z(n,o._root)||o._root),x(n),_(t)),e===o._root&&(e=e.firstChild)&&e.nodeName==="BR"&&E(e),o._ensureBottomLine(),o.setSelection(t),o._updatePath(t,!0)}catch(e){o._config.didError(e)}},Ee=(o,t)=>{let e;for(;(e=o.parentNode)&&!(e===t||e.isContentEditable);)o=e;E(o)},Te=(o,t,e)=>{if(S(t,o._root,"A"))return;let n=t.data||"",i=Math.max(n.lastIndexOf(" ",e-1),n.lastIndexOf("\xA0",e-1))+1,s=n.slice(i,e),r=o.linkRegExp.exec(s);if(r){let l=o.getSelection();o._docWasChanged(),o._recordUndoState(l),o._getRangeAndRemoveBookmark(l);let a=i+r.index,d=a+r[0].length,c=l.startContainer===t,f=l.startOffset-d;a&&(t=t.splitText(a));let u=o._config.tagAttributes.a,m=p("A",Object.assign({href:r[1]?/^(?:ht|f)tps?:/i.test(r[1])?r[1]:"http://"+r[1]:"mailto:"+r[0]},u));m.textContent=n.slice(a,d),t.parentNode.insertBefore(m,t),t.data=n.slice(d),c&&(l.setStart(t,f),l.setEnd(t,f)),o.setSelection(l)}};var Je=(o,t,e)=>{let n=o._root;if(o._removeZWS(),o.saveUndoState(e),!e.collapsed)t.preventDefault(),H(e,n),ne(o,e);else if(X(e,n)){t.preventDefault();let i=L(e,n);if(!i)return;let s=i;D(s.parentNode,n);let r=z(s,n);if(r){if(!r.isContentEditable){Ee(r,n);return}for(Y(r,s,e,n),s=r.parentNode;s!==n&&!s.nextSibling;)s=s.parentNode;s!==n&&(s=s.nextSibling)&&F(s,n),o.setSelection(e)}else if(s){if(S(s,n,"UL")||S(s,n,"OL")){o.decreaseListLevel(e);return}else if(S(s,n,"BLOCKQUOTE")){o.removeQuote(e);return}o.setSelection(e),o._updatePath(e,!0)}}else{_(e);let i=e.startContainer,s=e.startOffset,r=i.parentNode;i instanceof Text&&r instanceof HTMLAnchorElement&&s&&r.href.includes(i.data)?(i.deleteData(s-1,1),o.setSelection(e),o.removeLink(),t.preventDefault()):(o.setSelection(e),setTimeout(()=>{ne(o)},0))}};var et=(o,t,e)=>{let n=o._root,i,s,r,l,a,d;if(o._removeZWS(),o.saveUndoState(e),!e.collapsed)t.preventDefault(),H(e,n),ne(o,e);else if(Z(e,n)){if(t.preventDefault(),i=L(e,n),!i)return;if(D(i.parentNode,n),s=W(i,n),s){if(!s.isContentEditable){Ee(s,n);return}for(Y(i,s,e,n),s=i.parentNode;s!==n&&!s.nextSibling;)s=s.parentNode;s!==n&&(s=s.nextSibling)&&F(s,n),o.setSelection(e),o._updatePath(e,!0)}}else{if(r=e.cloneRange(),q(e,n,n,n),l=e.endContainer,a=e.endOffset,l instanceof Element&&(d=l.childNodes[a],d&&d.nodeName==="IMG")){t.preventDefault(),E(d),_(e),ne(o,e);return}o.setSelection(r),setTimeout(()=>{ne(o)},0)}};var tt=(o,t,e)=>{let n=o._root;if(o._removeZWS(),e.collapsed&&X(e,n)){let i=L(e,n),s;for(;s=i.parentNode;){if(s.nodeName==="UL"||s.nodeName==="OL"){t.preventDefault(),o.increaseListLevel(e);break}i=s}}},nt=(o,t,e)=>{let n=o._root;if(o._removeZWS(),e.collapsed&&X(e,n)){let i=e.startContainer;(S(i,n,"UL")||S(i,n,"OL"))&&(t.preventDefault(),o.decreaseListLevel(e))}};var ot=(o,t,e)=>{var s;let n,i=o._root;if(o._recordUndoState(e),o._getRangeAndRemoveBookmark(e),!e.collapsed)H(e,i),o._ensureBottomLine(),o.setSelection(e),o._updatePath(e,!0);else if(Z(e,i)){let r=L(e,i);if(r&&r.nodeName!=="PRE"){let l=(s=r.textContent)==null?void 0:s.trimEnd().replace(y,"");if(l==="*"||l==="1."){t.preventDefault(),o.insertPlainText(" ",!1),o._docWasChanged(),o.saveUndoState(e);let a=new T(r,4),d;for(;d=a.nextNode();)E(d);l==="*"?o.makeUnorderedList():o.makeOrderedList();return}}}if(n=e.endContainer,e.endOffset===k(n))do if(n.nodeName==="A"){e.setStartAfter(n);break}while(!n.nextSibling&&(n=n.parentNode)&&n!==i);if(o._config.addLinks){let r=e.cloneRange();_(r);let l=r.startContainer,a=r.startOffset;setTimeout(()=>{Te(o,l,a)},0)}o.setSelection(e)};var it=function(o){if(o.defaultPrevented||o.isComposing)return;let t=o.key,e="";t!=="Backspace"&&t!=="Delete"&&(o.altKey&&(e+="Alt-"),o.ctrlKey&&(e+="Ctrl-"),o.metaKey&&(e+="Meta-"),o.shiftKey&&(e+="Shift-")),fe&&o.shiftKey&&t==="Delete"&&(e+="Shift-"),t=e+t;let n=this.getSelection();this._keyHandlers[t]?this._keyHandlers[t](this,o,n):!n.collapsed&&!o.ctrlKey&&!o.metaKey&&t.length===1&&(this.saveUndoState(n),H(n,this._root),this._ensureBottomLine(),this.setSelection(n),this._updatePath(n,!0))},b={Backspace:Je,Delete:et,Tab:tt,"Shift-Tab":nt," ":ot,ArrowLeft(o){o._removeZWS()},ArrowRight(o,t,e){o._removeZWS();let n=o.getRoot();if(Z(e,n)){_(e);let i=e.endContainer;do if(i.nodeName==="CODE"){let s=i.nextSibling;if(!(s instanceof Text)){let r=document.createTextNode("\xA0");i.parentNode.insertBefore(r,s),s=r}e.setStart(s,1),o.setSelection(e),t.preventDefault();break}while(!i.nextSibling&&(i=i.parentNode)&&i!==n)}}};Pe||(b.Enter=we,b["Shift-Enter"]=we);!de&&!_e&&(b.PageUp=o=>{o.moveCursorToStart()},b.PageDown=o=>{o.moveCursorToEnd()});var ie=(o,t)=>(t=t||null,(e,n)=>{n.preventDefault();let i=e.getSelection();e.hasFormat(o,null,i)?e.changeFormat(null,{tag:o},i):e.changeFormat({tag:o},t,i)});b[B+"b"]=ie("B");b[B+"i"]=ie("I");b[B+"u"]=ie("U");b[B+"Shift-7"]=ie("S");b[B+"Shift-5"]=ie("SUB",{tag:"SUP"});b[B+"Shift-6"]=ie("SUP",{tag:"SUB"});b[B+"Shift-8"]=(o,t)=>{t.preventDefault();let e=o.getPath();/(?:^|>)UL/.test(e)?o.removeList():o.makeUnorderedList()};b[B+"Shift-9"]=(o,t)=>{t.preventDefault();let e=o.getPath();/(?:^|>)OL/.test(e)?o.removeList():o.makeOrderedList()};b[B+"["]=(o,t)=>{t.preventDefault();let e=o.getPath();/(?:^|>)BLOCKQUOTE/.test(e)||!/(?:^|>)[OU]L/.test(e)?o.decreaseQuoteLevel():o.decreaseListLevel()};b[B+"]"]=(o,t)=>{t.preventDefault();let e=o.getPath();/(?:^|>)BLOCKQUOTE/.test(e)||!/(?:^|>)[OU]L/.test(e)?o.increaseQuoteLevel():o.increaseListLevel()};b[B+"d"]=(o,t)=>{t.preventDefault(),o.toggleCode()};b[B+"z"]=(o,t)=>{t.preventDefault(),o.undo()};b[B+"y"]=b[B+"Shift-z"]=(o,t)=>{t.preventDefault(),o.redo()};var Ce=class{constructor(t,e){this.customEvents=new Set(["pathChange","select","input","pasteImage","undoStateChange"]);this.startSelectionId="squire-selection-start";this.endSelectionId="squire-selection-end";this.linkRegExp=/\b(?:((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9][a-z0-9.\-]*[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:[^\s?&`!()\[\]{};:'".,<>«»“”‘’]|\([^\s()<>]+\)))|([\w\-.%+]+@(?:[\w\-]+\.)+[a-z]{2,}\b(?:[?][^&?\s]+=[^\s?&`!()\[\]{};:'".,<>«»“”‘’]+(?:&[^&?\s]+=[^\s?&`!()\[\]{};:'".,<>«»“”‘’]+)*)?))/i;this.tagAfterSplit={DT:"DD",DD:"DT",LI:"LI",PRE:"PRE"};this._root=t,this._config=this._makeConfig(e),this._isFocused=!1,this._lastSelection=V(t,0),this._willRestoreSelection=!1,this._mayHaveZWS=!1,this._lastAnchorNode=null,this._lastFocusNode=null,this._path="",this._events=new Map,this._undoIndex=-1,this._undoStack=[],this._undoStackLength=0,this._isInUndoState=!1,this._ignoreChange=!1,this._ignoreAllChanges=!1,this.addEventListener("selectionchange",this._updatePathOnEvent),this.addEventListener("blur",this._enableRestoreSelection),this.addEventListener("mousedown",this._disableRestoreSelection),this.addEventListener("touchstart",this._disableRestoreSelection),this.addEventListener("focus",this._restoreSelection),this._isShiftDown=!1,this.addEventListener("cut",Xe),this.addEventListener("copy",Ve),this.addEventListener("paste",$e),this.addEventListener("drop",Ye),this.addEventListener("keydown",Ae),this.addEventListener("keyup",Ae),this.addEventListener("keydown",it),this._keyHandlers=Object.create(b);let n=new MutationObserver(()=>this._docWasChanged());n.observe(t,{childList:!0,attributes:!0,characterData:!0,subtree:!0}),this._mutation=n,t.setAttribute("contenteditable","true"),this.addEventListener("beforeinput",this._beforeInput),this.setHTML("")}destroy(){this._events.forEach((t,e)=>{this.removeEventListener(e)}),this._mutation.disconnect(),this._undoIndex=-1,this._undoStack=[],this._undoStackLength=0}_makeConfig(t){let e={blockTag:"DIV",blockAttributes:null,tagAttributes:{},classNames:{color:"color",fontFamily:"font",fontSize:"size",highlight:"highlight"},undo:{documentSizeThreshold:-1,undoLimit:-1},addLinks:!0,willCutCopy:null,toPlainText:null,sanitizeToDOMFragment:n=>{let i=DOMPurify.sanitize(n,{ALLOW_UNKNOWN_PROTOCOLS:!0,WHOLE_DOCUMENT:!1,RETURN_DOM:!0,RETURN_DOM_FRAGMENT:!0,FORCE_BODY:!1});return i?document.importNode(i,!0):document.createDocumentFragment()},didError:n=>console.log(n)};return t&&(Object.assign(e,t),e.blockTag=e.blockTag.toUpperCase()),e}setKeyHandler(t,e){return this._keyHandlers[t]=e,this}_beforeInput(t){switch(t.inputType){case"insertText":Fe&&t.data&&t.data.includes(` -`)&&t.preventDefault();break;case"insertLineBreak":t.preventDefault(),this.splitBlock(!0);break;case"insertParagraph":t.preventDefault(),this.splitBlock(!1);break;case"insertOrderedList":t.preventDefault(),this.makeOrderedList();break;case"insertUnoderedList":t.preventDefault(),this.makeUnorderedList();break;case"historyUndo":t.preventDefault(),this.undo();break;case"historyRedo":t.preventDefault(),this.redo();break;case"formatBold":t.preventDefault(),this.bold();break;case"formaItalic":t.preventDefault(),this.italic();break;case"formatUnderline":t.preventDefault(),this.underline();break;case"formatStrikeThrough":t.preventDefault(),this.strikethrough();break;case"formatSuperscript":t.preventDefault(),this.superscript();break;case"formatSubscript":t.preventDefault(),this.subscript();break;case"formatJustifyFull":case"formatJustifyCenter":case"formatJustifyRight":case"formatJustifyLeft":{t.preventDefault();let e=t.inputType.slice(13).toLowerCase();e==="full"&&(e="justify"),this.setTextAlignment(e);break}case"formatRemove":t.preventDefault(),this.removeAllFormatting();break;case"formatSetBlockTextDirection":{t.preventDefault();let e=t.data;e==="null"&&(e=null),this.setTextDirection(e);break}case"formatBackColor":t.preventDefault(),this.setHighlightColor(t.data);break;case"formatFontColor":t.preventDefault(),this.setTextColor(t.data);break;case"formatFontName":t.preventDefault(),this.setFontFace(t.data);break}}handleEvent(t){this.fireEvent(t.type,t)}fireEvent(t,e){let n=this._events.get(t);if(/^(?:focus|blur)/.test(t)){let i=this._root===document.activeElement;if(t==="focus"){if(!i||this._isFocused)return this;this._isFocused=!0}else{if(i||!this._isFocused)return this;this._isFocused=!1}}if(n){let i=e instanceof Event?e:new CustomEvent(t,{detail:e});n=n.slice();for(let s of n)try{"handleEvent"in s?s.handleEvent(i):s.call(this,i)}catch(r){this._config.didError(r)}}return this}addEventListener(t,e){let n=this._events.get(t),i=this._root;return n||(n=[],this._events.set(t,n),this.customEvents.has(t)||(t==="selectionchange"&&(i=document),i.addEventListener(t,this,!0))),n.push(e),this}removeEventListener(t,e){let n=this._events.get(t),i=this._root;if(n){if(e){let s=n.length;for(;s--;)n[s]===e&&n.splice(s,1)}else n.length=0;n.length||(this._events.delete(t),this.customEvents.has(t)||(t==="selectionchange"&&(i=document),i.removeEventListener(t,this,!0)))}return this}focus(){return this._root.focus({preventScroll:!0}),this}blur(){return this._root.blur(),this}_enableRestoreSelection(){this._willRestoreSelection=!0}_disableRestoreSelection(){this._willRestoreSelection=!1}_restoreSelection(){this._willRestoreSelection&&this.setSelection(this._lastSelection)}_removeZWS(){this._mayHaveZWS&&(re(this._root),this._mayHaveZWS=!1)}_saveRangeToBookmark(t){let e=p("INPUT",{id:this.startSelectionId,type:"hidden"}),n=p("INPUT",{id:this.endSelectionId,type:"hidden"}),i;j(t,e),t.collapse(!1),j(t,n),e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING&&(e.id=this.endSelectionId,n.id=this.startSelectionId,i=e,e=n,n=i),t.setStartAfter(e),t.setEndBefore(n)}_getRangeAndRemoveBookmark(t){let e=this._root,n=e.querySelector("#"+this.startSelectionId),i=e.querySelector("#"+this.endSelectionId);if(n&&i){let s=n.parentNode,r=i.parentNode,l=Array.from(s.childNodes).indexOf(n),a=Array.from(r.childNodes).indexOf(i);s===r&&(a-=1),n.remove(),i.remove(),t||(t=document.createRange()),t.setStart(s,l),t.setEnd(r,a),te(s,t),s!==r&&te(r,t),t.collapsed&&(s=t.startContainer,s instanceof Text&&(r=s.childNodes[t.startOffset],(!r||!(r instanceof Text))&&(r=s.childNodes[t.startOffset-1]),r&&r instanceof Text&&(t.setStart(r,0),t.collapse(!0))))}return t||null}getSelection(){let t=window.getSelection(),e=this._root,n=null;if(this._isFocused&&t&&t.rangeCount){n=t.getRangeAt(0).cloneRange();let i=n.startContainer,s=n.endContainer;i&&M(i)&&n.setStartBefore(i),s&&M(s)&&n.setEndBefore(s)}return n&&e.contains(n.commonAncestorContainer)?this._lastSelection=n:(n=this._lastSelection,document.contains(n.commonAncestorContainer)||(n=null)),n||(n=V(e.firstElementChild||e,0)),n}setSelection(t){if(this._lastSelection=t,!this._isFocused)this._enableRestoreSelection();else{let e=window.getSelection();e&&("setBaseAndExtent"in Selection.prototype?e.setBaseAndExtent(t.startContainer,t.startOffset,t.endContainer,t.endOffset):(e.removeAllRanges(),e.addRange(t)))}return this}_moveCursorTo(t){let e=this._root,n=V(e,t?0:e.childNodes.length);return _(n),this.setSelection(n),this}moveCursorToStart(){return this._moveCursorTo(!0)}moveCursorToEnd(){return this._moveCursorTo(!1)}getCursorPosition(){let t=this.getSelection(),e=t.getBoundingClientRect();if(e&&!e.top){this._ignoreChange=!0;let n=p("SPAN");n.textContent=y,j(t,n),e=n.getBoundingClientRect();let i=n.parentNode;i.removeChild(n),te(i,t)}return e}getPath(){return this._path}_updatePathOnEvent(){this._isFocused&&this._updatePath(this.getSelection())}_updatePath(t,e){let n=t.startContainer,i=t.endContainer,s;(e||n!==this._lastAnchorNode||i!==this._lastFocusNode)&&(this._lastAnchorNode=n,this._lastFocusNode=i,s=n&&i?n===i?this._getPath(i):"(selection)":"",this._path!==s&&(this._path=s,this.fireEvent("pathChange",{path:s}))),this.fireEvent(t.collapsed?"cursor":"select",{range:t})}_getPath(t){let e=this._root,n=this._config,i="";if(t&&t!==e){let s=t.parentNode;if(i=s?this._getPath(s):"",t instanceof HTMLElement){let r=t.id,l=t.classList,a=Array.from(l).sort(),d=t.dir,c=n.classNames;i+=(i?">":"")+t.nodeName,r&&(i+="#"+r),a.length&&(i+=".",i+=a.join(".")),d&&(i+="[dir="+d+"]"),l.contains(c.highlight)&&(i+="[backgroundColor="+t.style.backgroundColor.replace(/ /g,"")+"]"),l.contains(c.color)&&(i+="[color="+t.style.color.replace(/ /g,"")+"]"),l.contains(c.fontFamily)&&(i+="[fontFamily="+t.style.fontFamily.replace(/ /g,"")+"]"),l.contains(c.fontSize)&&(i+="[fontSize="+t.style.fontSize+"]")}}return i}modifyDocument(t){let e=this._mutation;return e&&(e.takeRecords().length&&this._docWasChanged(),e.disconnect()),this._ignoreAllChanges=!0,t(),this._ignoreAllChanges=!1,e&&(e.observe(this._root,{childList:!0,attributes:!0,characterData:!0,subtree:!0}),this._ignoreChange=!1),this}_docWasChanged(){if(We(),this._mayHaveZWS=!0,!this._ignoreAllChanges){if(this._ignoreChange){this._ignoreChange=!1;return}this._isInUndoState&&(this._isInUndoState=!1,this.fireEvent("undoStateChange",{canUndo:!0,canRedo:!1})),this.fireEvent("input")}}_recordUndoState(t,e){let n=this._isInUndoState;if(!n||e){let i=this._undoIndex+1,s=this._undoStack,r=this._config.undo,l=r.documentSizeThreshold,a=r.undoLimit;if(i-1&&d.length*2>l&&a>-1&&i>a&&(s.splice(0,i-a),i=a,this._undoStackLength=a),s[i]=d,this._undoIndex=i,this._undoStackLength+=1,this._isInUndoState=!0}return this}saveUndoState(t){return t||(t=this.getSelection()),this._recordUndoState(t,this._isInUndoState),this._getRangeAndRemoveBookmark(t),this}undo(){if(this._undoIndex!==0||!this._isInUndoState){this._recordUndoState(this.getSelection(),!1),this._undoIndex-=1,this._setRawHTML(this._undoStack[this._undoIndex]);let t=this._getRangeAndRemoveBookmark();t&&this.setSelection(t),this._isInUndoState=!0,this.fireEvent("undoStateChange",{canUndo:this._undoIndex!==0,canRedo:!0}),this.fireEvent("input")}return this.focus()}redo(){let t=this._undoIndex,e=this._undoStackLength;if(t+1",d="<"+r;for(let c in l)d+=" "+c+'="'+ke(l[c])+'"';d+=">";for(let c=0,f=i.length;c")+a),i[c]=u}return this.insertHTML(i.join(""),e)}getSelectedText(t){return ge(t||this.getSelection())}getFontInfo(t){let e={color:void 0,backgroundColor:void 0,fontFamily:void 0,fontSize:void 0};t||(t=this.getSelection());let n=0,i=t.commonAncestorContainer;if(t.collapsed||i instanceof Text)for(i instanceof Text&&(i=i.parentNode);n<4&&i;){let s=i.style;if(s){let r=s.color;!e.color&&r&&(e.color=r,n+=1);let l=s.backgroundColor;!e.backgroundColor&&l&&(e.backgroundColor=l,n+=1);let a=s.fontFamily;!e.fontFamily&&a&&(e.fontFamily=a,n+=1);let d=s.fontSize;!e.fontSize&&d&&(e.fontSize=d,n+=1)}i=i.parentNode}return e}hasFormat(t,e,n){t=t.toUpperCase(),e||(e={}),n||(n=this.getSelection()),!n.collapsed&&n.startContainer instanceof Text&&n.startOffset===n.startContainer.length&&n.startContainer.nextSibling&&n.setStartBefore(n.startContainer.nextSibling),!n.collapsed&&n.endContainer instanceof Text&&n.endOffset===0&&n.endContainer.previousSibling&&n.setEndAfter(n.endContainer.previousSibling);let i=this._root,s=n.commonAncestorContainer;if(S(s,i,t,e))return!0;if(s instanceof Text)return!1;let r=new T(s,4,d=>K(n,d,!0)),l=!1,a;for(;a=r.nextNode();){if(!S(a,i,t,e))return!1;l=!0}return l}changeFormat(t,e,n,i){return n||(n=this.getSelection()),this.saveUndoState(n),e&&(n=this._removeFormat(e.tag.toUpperCase(),e.attributes||{},n,i)),t&&(n=this._addFormat(t.tag.toUpperCase(),t.attributes||{},n)),this.setSelection(n),this._updatePath(n,!0),this.focus()}_addFormat(t,e,n){let i=this._root;if(n.collapsed){let s=x(p(t,e));j(n,s);let r=s.firstChild||s,l=r instanceof Text?r.length:0;n.setStart(r,l),n.collapse(!0);let a=s;for(;N(a);)a=a.parentNode;re(a,s)}else{let s=new T(n.commonAncestorContainer,5,c=>(c instanceof Text||c.nodeName==="BR"||c.nodeName==="IMG")&&K(n,c,!0)),{startContainer:r,startOffset:l,endContainer:a,endOffset:d}=n;if(s.currentNode=r,!(r instanceof Element)&&!(r instanceof Text)||!s.filter(r)){let c=s.nextNode();if(!c)return n;r=c,l=0}do{let c=s.currentNode;if(!S(c,i,t,e)){c===a&&c.length>d&&c.splitText(d),c===r&&l&&(c=c.splitText(l),a===r?(a=c,d-=l):a===r.parentNode&&(d+=1),r=c,l=0);let u=p(t,e);R(c,u),u.appendChild(c)}}while(s.nextNode());n=V(r,l,a,d)}return n}_removeFormat(t,e,n,i){this._saveRangeToBookmark(n);let s;n.collapsed&&(oe?s=document.createTextNode(y):s=document.createTextNode(""),j(n,s));let r=n.commonAncestorContainer;for(;N(r);)r=r.parentNode;let l=n.startContainer,a=n.startOffset,d=n.endContainer,c=n.endOffset,f=[],u=(h,g)=>{if(K(n,h,!1))return;let v,O;if(!K(n,h,!0)){!(h instanceof HTMLInputElement)&&(!(h instanceof Text)||h.data)&&f.push([g,h]);return}if(h instanceof Text)h===d&&c!==h.length&&f.push([g,h.splitText(c)]),h===l&&a&&(h.splitText(a),f.push([g,h]));else for(v=h.firstChild;v;v=O)O=v.nextSibling,u(v,g)},m=Array.from(r.getElementsByTagName(t)).filter(h=>K(n,h,!0)&&he(h,t,e));if(i||m.forEach(h=>{u(h,h)}),f.forEach(([h,g])=>{h=h.cloneNode(!1),R(g,h),h.appendChild(g)}),m.forEach(h=>{R(h,C(h))}),oe&&s){s=s.parentNode;let h=s;for(;h&&N(h);)h=h.parentNode;h&&re(h,s)}return this._getRangeAndRemoveBookmark(n),s&&n.collapse(!1),te(r,n),n}bold(){return this.changeFormat({tag:"B"})}removeBold(){return this.changeFormat(null,{tag:"B"})}italic(){return this.changeFormat({tag:"I"})}removeItalic(){return this.changeFormat(null,{tag:"I"})}underline(){return this.changeFormat({tag:"U"})}removeUnderline(){return this.changeFormat(null,{tag:"U"})}strikethrough(){return this.changeFormat({tag:"S"})}removeStrikethrough(){return this.changeFormat(null,{tag:"S"})}subscript(){return this.changeFormat({tag:"SUB"},{tag:"SUP"})}removeSubscript(){return this.changeFormat(null,{tag:"SUB"})}superscript(){return this.changeFormat({tag:"SUP"},{tag:"SUB"})}removeSuperscript(){return this.changeFormat(null,{tag:"SUP"})}makeLink(t,e){let n=this.getSelection();if(n.collapsed){let i=t.indexOf(":")+1;if(i)for(;t[i]==="/";)i+=1;j(n,document.createTextNode(t.slice(i)))}return e=Object.assign({href:t},this._config.tagAttributes.a,e),this.changeFormat({tag:"A",attributes:e},{tag:"A"},n)}removeLink(){return this.changeFormat(null,{tag:"A"},this.getSelection(),!0)}addDetectedLinks(t,e){let n=new T(t,4,l=>!S(l,e||this._root,"A")),i=this.linkRegExp,s=this._config.tagAttributes.a,r;for(;r=n.nextNode();){let l=r.parentNode,a=r.data,d;for(;d=i.exec(a);){let c=d.index,f=c+d[0].length;c&&l.insertBefore(document.createTextNode(a.slice(0,c)),r);let u=p("A",Object.assign({href:d[1]?/^(?:ht|f)tps?:/i.test(d[1])?d[1]:"http://"+d[1]:"mailto:"+d[0]},s));u.textContent=a.slice(c,f),l.insertBefore(u,r),r.data=a=a.slice(f)}}return this}setFontFace(t){let e=this._config.classNames.fontFamily;return this.changeFormat(t?{tag:"SPAN",attributes:{class:e,style:"font-family: "+t+", sans-serif;"}}:null,{tag:"SPAN",attributes:{class:e}})}setFontSize(t){let e=this._config.classNames.fontSize;return this.changeFormat(t?{tag:"SPAN",attributes:{class:e,style:"font-size: "+(typeof t=="number"?t+"px":t)}}:null,{tag:"SPAN",attributes:{class:e}})}setTextColor(t){let e=this._config.classNames.color;return this.changeFormat(t?{tag:"SPAN",attributes:{class:e,style:"color:"+t}}:null,{tag:"SPAN",attributes:{class:e}})}setHighlightColor(t){let e=this._config.classNames.highlight;return this.changeFormat(t?{tag:"SPAN",attributes:{class:e,style:"background-color:"+t}}:null,{tag:"SPAN",attributes:{class:e}})}_ensureBottomLine(){let t=this._root,e=t.lastElementChild;(!e||e.nodeName!==this._config.blockTag||!P(e))&&t.appendChild(this.createDefaultBlock())}createDefaultBlock(t){let e=this._config;return x(p(e.blockTag,e.blockAttributes,t))}splitBlock(t,e){e||(e=this.getSelection());let n=this._root,i,s,r,l;if(this._recordUndoState(e),this._removeZWS(),this._getRangeAndRemoveBookmark(e),e.collapsed||H(e,n),this._config.addLinks){_(e);let u=e.startContainer,m=e.startOffset;setTimeout(()=>{Te(this,u,m)},0)}if(i=L(e,n),i&&(s=S(i,n,"PRE"))){_(e),r=e.startContainer;let u=e.startOffset;return r instanceof Text||(r=document.createTextNode(""),s.insertBefore(r,s.firstChild)),!t&&r instanceof Text&&(r.data.charAt(u-1)===` +"use strict";(()=>{var it=()=>!0,T=class{constructor(t,e,n){this.root=t,this.currentNode=t,this.nodeType=e,this.filter=n||it}isAcceptableNode(t){let e=t.nodeType;return!!((e===Node.ELEMENT_NODE?1:e===Node.TEXT_NODE?4:0)&this.nodeType)&&this.filter(t)}nextNode(){let t=this.root,e=this.currentNode,n;for(;;){for(n=e.firstChild;!n&&e&&e!==t;)n=e.nextSibling,n||(e=e.parentNode);if(!n)return null;if(this.isAcceptableNode(n))return this.currentNode=n,n;e=n}}previousNode(){let t=this.root,e=this.currentNode,n;for(;;){if(e===t)return null;if(n=e.previousSibling,n)for(;e=n.lastChild;)n=e;else n=e.parentNode;if(!n)return null;if(this.isAcceptableNode(n))return this.currentNode=n,n;e=n}}previousPONode(){let t=this.root,e=this.currentNode,n;for(;;){for(n=e.lastChild;!n&&e&&e!==t;)n=e.previousSibling,n||(e=e.parentNode);if(!n)return null;if(this.isAcceptableNode(n))return this.currentNode=n,n;e=n}}};var B="\u200B",J=navigator.userAgent,de=/Mac OS X/.test(J),fe=/Windows NT/.test(J),_e=/iP(?:ad|hone|od)/.test(J)||de&&!!navigator.maxTouchPoints,xt=/Android/.test(J),Fe=/Gecko\//.test(J),se=/Edge\//.test(J),st=!se&&/WebKit\//.test(J),k=de||_e?"Meta-":"Ctrl-",oe=st,He="onbeforeinput"in document&&"inputType"in new InputEvent("input"),I=/[^ \t\r\n]/;var at=/^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/,ct=new Set(["BR","HR","IFRAME","IMG","INPUT"]),dt=0,xe=1,Pe=2,Ue=3,ue=new WeakMap,qe=()=>{ue=new WeakMap},M=o=>ct.has(o.nodeName),be=o=>{switch(o.nodeType){case 3:return xe;case 1:case 11:if(ue.has(o))return ue.get(o);break;default:return dt}let t;return Array.from(o.childNodes).every(N)?at.test(o.nodeName)?t=xe:t=Pe:t=Ue,ue.set(o,t),t},N=o=>be(o)===xe,P=o=>be(o)===Pe,Q=o=>be(o)===Ue;var p=(o,t,e)=>{let n=document.createElement(o);if(t instanceof Array&&(e=t,t=null),t)for(let i in t){let s=t[i];s!==void 0&&n.setAttribute(i,s)}return e&&e.forEach(i=>n.appendChild(i)),n},Le=(o,t)=>M(o)||o.nodeType!==t.nodeType||o.nodeName!==t.nodeName?!1:o instanceof HTMLElement&&t instanceof HTMLElement?o.nodeName!=="A"&&o.className===t.className&&o.style.cssText===t.style.cssText:!0,he=(o,t,e)=>{if(o.nodeName!==t)return!1;for(let n in e)if(!("getAttribute"in o)||o.getAttribute(n)!==e[n])return!1;return!0},S=(o,t,e,n)=>{for(;o&&o!==t;){if(he(o,e,n))return o;o=o.parentNode}return null},me=(o,t)=>{let e=o.childNodes;for(;t&&o instanceof Element;)o=e[t-1],e=o.childNodes,t=e.length;return o},Re=(o,t)=>{let e=o;if(e instanceof Element){let n=e.childNodes;if(to instanceof Element||o instanceof DocumentFragment?o.childNodes.length:o instanceof CharacterData?o.length:0,C=o=>{let t=document.createDocumentFragment(),e=o.firstChild;for(;e;)t.appendChild(e),e=o.firstChild;return t},E=o=>{let t=o.parentNode;return t&&t.removeChild(o),o},R=(o,t)=>{let e=o.parentNode;e&&e.replaceChild(t,o)};var ft=o=>o instanceof Element?o.nodeName==="BR":I.test(o.data),ee=(o,t)=>{let e=o.parentNode;for(;N(e);)e=e.parentNode;let n=new T(e,5,ft);return n.currentNode=o,!!n.nextNode()||t&&!n.previousNode()},re=(o,t)=>{let e=new T(o,4),n,i;for(;n=e.nextNode();)for(;(i=n.data.indexOf(B))>-1&&(!t||n.parentNode!==t);)if(n.length===1){let s=n,r=s.parentNode;for(;r&&(r.removeChild(s),e.currentNode=r,!(!N(r)||y(r)));)s=r,r=s.parentNode;break}else n.deleteData(i,1)};var ut=0,ht=1,mt=2,pt=3,K=(o,t,e)=>{let n=document.createRange();if(n.selectNode(t),e){let i=o.compareBoundaryPoints(pt,n)>-1,s=o.compareBoundaryPoints(ht,n)<1;return!i&&!s}else{let i=o.compareBoundaryPoints(ut,n)<1,s=o.compareBoundaryPoints(mt,n)>-1;return i&&s}},_=o=>{let{startContainer:t,startOffset:e,endContainer:n,endOffset:i}=o;for(;!(t instanceof Text);){let s=t.childNodes[e];if(!s||M(s)){if(e&&(s=t.childNodes[e-1],s instanceof Text)){let r=s,l;for(;!r.length&&(l=r.previousSibling)&&l instanceof Text;)r.remove(),r=l;t=r,e=r.data.length}break}t=s,e=0}if(i)for(;!(n instanceof Text);){let s=n.childNodes[i-1];if(!s||M(s)){if(s&&s.nodeName==="BR"&&!ee(s,!1)){i-=1;continue}break}n=s,i=y(n)}else for(;!(n instanceof Text);){let s=n.firstChild;if(!s||M(s))break;n=s}o.setStart(t,e),o.setEnd(n,i)},q=(o,t,e,n)=>{let i=o.startContainer,s=o.startOffset,r=o.endContainer,l=o.endOffset,a;for(t||(t=o.commonAncestorContainer),e||(e=t);!s&&i!==t&&i!==n;)a=i.parentNode,s=Array.from(a.childNodes).indexOf(i),i=a;for(;!(r===e||r===n||(r.nodeType!==3&&r.childNodes[l]&&r.childNodes[l].nodeName==="BR"&&!ee(r.childNodes[l],!1)&&(l+=1),l!==y(r)));)a=r.parentNode,l=Array.from(a.childNodes).indexOf(r)+1,r=a;o.setStart(i,s),o.setEnd(r,l)},Oe=(o,t,e)=>{let n=S(o.endContainer,e,t);if(n&&(n=n.parentNode)){let i=o.cloneRange();q(i,n,n,e),i.endContainer===n&&(o.setStart(i.endContainer,i.endOffset),o.setEnd(i.endContainer,i.endOffset))}return o};var x=o=>{let t=null;if(o instanceof Text)return o;if(N(o)){let e=o.firstChild;if(oe)for(;e&&e instanceof Text&&!e.data;)o.removeChild(e),e=o.firstChild;e||(oe?t=document.createTextNode(B):t=document.createTextNode(""))}else if((o instanceof Element||o instanceof DocumentFragment)&&!o.querySelector("BR")){t=p("BR");let e=o,n;for(;(n=e.lastElementChild)&&!N(n);)e=n;o=e}if(t)try{o.appendChild(t)}catch(e){}return o},D=(o,t)=>{let e=null;return Array.from(o.childNodes).forEach(n=>{let i=n.nodeName==="BR";!i&&N(n)?(e||(e=p("DIV")),e.appendChild(n)):(i||e)&&(e||(e=p("DIV")),x(e),i?o.replaceChild(e,n):o.insertBefore(e,n),e=null),Q(n)&&D(n,t)}),e&&o.appendChild(x(e)),o},w=(o,t,e,n)=>{if(o instanceof Text&&o!==e){if(typeof t!="number")throw new Error("Offset must be a number to split text node!");if(!o.parentNode)throw new Error("Cannot split text node with no parent!");return w(o.parentNode,o.splitText(t),e,n)}let i=typeof t=="number"?t{let e=o.childNodes,n=e.length,i=[];for(;n--;){let s=e[n],r=n?e[n-1]:null;if(r&&N(s)&&Le(s,r))t.startContainer===s&&(t.startContainer=r,t.startOffset+=y(r)),t.endContainer===s&&(t.endContainer=r,t.endOffset+=y(r)),t.startContainer===o&&(t.startOffset>n?t.startOffset-=1:t.startOffset===n&&(t.startContainer=r,t.startOffset=y(r))),t.endContainer===o&&(t.endOffset>n?t.endOffset-=1:t.endOffset===n&&(t.endContainer=r,t.endOffset=y(r))),E(s),s instanceof Text?r.appendData(s.data):i.push(C(s));else if(s instanceof Element){let l;for(;l=i.pop();)s.appendChild(l);We(s,t)}}},te=(o,t)=>{let e=o instanceof Text?o.parentNode:o;if(e instanceof Element){let n={startContainer:t.startContainer,startOffset:t.startOffset,endContainer:t.endContainer,endOffset:t.endOffset};We(e,n),t.setStart(n.startContainer,n.startOffset),t.setEnd(n.endContainer,n.endOffset)}},Y=(o,t,e,n)=>{let i=t,s,r;for(;(s=i.parentNode)&&s!==n&&s instanceof Element&&s.childNodes.length===1;)i=s;E(i),r=o.childNodes.length;let l=o.lastChild;l&&l.nodeName==="BR"&&(o.removeChild(l),r-=1),o.appendChild(C(t)),e.setStart(o,r),e.collapse(!0),te(o,e)},F=(o,t)=>{let e=o.previousSibling,n=o.firstChild,i=o.nodeName==="LI";if(!(i&&(!n||!/^[OU]L$/.test(n.nodeName)))){if(e&&Le(e,o)){if(!Q(e))if(i){let r=p("DIV");r.appendChild(C(e)),e.appendChild(r)}else return;E(o);let s=!Q(o);e.appendChild(C(o)),s&&D(e,t),n&&F(n,t)}else if(i){let s=p("DIV");o.insertBefore(s,n),x(s)}}};var Ke={"font-weight":{regexp:/^bold|^700/i,replace(){return p("B")}},"font-style":{regexp:/^italic/i,replace(){return p("I")}},"font-family":{regexp:I,replace(o,t){return p("SPAN",{class:o.fontFamily,style:"font-family:"+t})}},"font-size":{regexp:I,replace(o,t){return p("SPAN",{class:o.fontSize,style:"font-size:"+t})}},"text-decoration":{regexp:/^underline/i,replace(){return p("U")}}},Nt=(o,t,e)=>{let n=o.style,i,s;for(let r in Ke){let l=Ke[r],a=n.getPropertyValue(r);if(a&&l.regexp.test(a)){let d=l.replace(e.classNames,a);if(d.nodeName===o.nodeName&&d.className===o.className)continue;s||(s=d),i&&i.appendChild(d),i=d,o.style.removeProperty(r)}}return s&&i&&(i.appendChild(C(o)),o.style.cssText?o.appendChild(s):R(o,s)),i||o},pe=o=>(t,e)=>{let n=p(o),i=t.attributes;for(let s=0,r=i.length;s{let n=o,i=n.face,s=n.size,r=n.color,l=e.classNames,a,d,c,f,u;return i&&(a=p("SPAN",{class:l.fontFamily,style:"font-family:"+i}),u=a,f=a),s&&(d=p("SPAN",{class:l.fontSize,style:"font-size:"+St[s]+"px"}),u||(u=d),f&&f.appendChild(d),f=d),r&&/^#?([\dA-F]{3}){1,2}$/i.test(r)&&(r.charAt(0)!=="#"&&(r="#"+r),c=p("SPAN",{class:l.color,style:"color:"+r}),u||(u=c),f&&f.appendChild(c),f=c),(!u||!f)&&(u=f=p("SPAN")),t.replaceChild(u,n),f.appendChild(C(n)),f},TT:(o,t,e)=>{let n=p("SPAN",{class:e.classNames.fontFamily,style:'font-family:menlo,consolas,"courier new",monospace'});return t.replaceChild(n,o),n.appendChild(C(o)),n}},Et=/^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|IGCAPTION|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|COL(?:GROUP)?|UL)$/,Tt=/^(?:HEAD|META|STYLE)/,Ne=(o,t,e)=>{let n=o.childNodes,i=o;for(;N(i);)i=i.parentNode;let s=new T(i,5);for(let r=0,l=n.length;r{let t=o.childNodes,e=t.length;for(;e--;){let n=t[e];n instanceof Element&&!M(n)?(Se(n),N(n)&&!n.firstChild&&o.removeChild(n)):n instanceof Text&&!n.data&&o.removeChild(n)}},le=(o,t,e)=>{let n=o.querySelectorAll("BR"),i=[],s=n.length;for(let r=0;ro.split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""");var ae=(o,t)=>{let e=new T(t,1,P);return e.currentNode=o,e},z=(o,t)=>{let e=ae(o,t).previousNode();return e!==t?e:null},W=(o,t)=>{let e=ae(o,t).nextNode();return e!==t?e:null},ce=o=>!o.textContent&&!o.querySelector("IMG");var L=(o,t)=>{let e=o.startContainer,n;if(N(e))n=z(e,t);else if(e!==t&&e instanceof HTMLElement&&P(e))n=e;else{let i=me(e,o.startOffset);n=W(i,t)}return n&&K(o,n,!0)?n:null},G=(o,t)=>{let e=o.endContainer,n;if(N(e))n=z(e,t);else if(e!==t&&e instanceof HTMLElement&&P(e))n=e;else{let i=Re(e,o.endOffset);if(!i||!t.contains(i)){i=t;let s;for(;s=i.lastChild;)i=s}n=z(i,t)}return n&&K(o,n,!0)?n:null},ze=o=>o instanceof Text?I.test(o.data):o.nodeName==="IMG",X=(o,t)=>{let e=o.startContainer,n=o.startOffset,i;if(e instanceof Text){let l=e.data;for(let a=n;a>0;a-=1)if(l.charAt(a-1)!==B)return!1;i=e}else if(i=Re(e,n),i&&!t.contains(i)&&(i=null),!i&&(i=me(e,n),i instanceof Text&&i.length))return!1;let s=L(o,t);if(!s)return!1;let r=new T(s,5,ze);return r.currentNode=i,!r.previousNode()},Z=(o,t)=>{let e=o.endContainer,n=o.endOffset,i;if(e instanceof Text){let l=e.data,a=l.length;for(let d=n;d{let e=L(o,t),n=G(o,t),i;e&&n&&(i=e.parentNode,o.setStart(i,Array.from(i.childNodes).indexOf(e)),i=n.parentNode,o.setEnd(i,Array.from(i.childNodes).indexOf(n)+1))};function $(o,t,e,n){let i=document.createRange();return i.setStart(o,t),e&&typeof n=="number"?i.setEnd(e,n):i.setEnd(o,t),i}var j=(o,t)=>{let{startContainer:e,startOffset:n,endContainer:i,endOffset:s}=o,r;if(e instanceof Text){let a=e.parentNode;if(r=a.childNodes,n===e.length)n=Array.from(r).indexOf(e)+1,o.collapsed&&(i=a,s=n);else{if(n){let d=e.splitText(n);i===e?(s-=n,i=d):i===a&&(s+=1),e=d}n=Array.from(r).indexOf(e)}e=a}else r=e.childNodes;let l=r.length;n===l?e.appendChild(t):e.insertBefore(t,r[n]),e===i&&(s+=r.length-l),o.setStart(e,n),o.setEnd(i,s)},Be=(o,t,e)=>{let n=document.createDocumentFragment();if(o.collapsed)return n;t||(t=o.commonAncestorContainer),t instanceof Text&&(t=t.parentNode);let i=o.startContainer,s=o.startOffset,r=w(o.endContainer,o.endOffset,t,e),l=0,a=w(i,s,t,e);for(;a&&a!==r;){let d=a.nextSibling;n.appendChild(a),a=d}return i instanceof Text&&r instanceof Text&&(i.appendData(r.data),E(r),r=i,l=s),o.setStart(i,s),r?o.setEnd(r,l):o.setEnd(t,t.childNodes.length),x(t),n},Ge=(o,t,e)=>{o.currentNode=e;let n;for(;n=o[t]();){if(n instanceof Text||M(n))return n;if(!N(n))return null}return null},H=(o,t)=>{let e=L(o,t),n=G(o,t),i=e!==n;e&&n&&(_(o),q(o,e,n,t));let s=Be(o,null,t);_(o),i&&(n=G(o,t),e&&n&&e!==n&&Y(e,n,o,t)),e&&x(e);let r=t.firstChild;(!r||r.nodeName==="BR")&&(x(t),t.firstChild&&o.selectNodeContents(t.firstChild)),o.collapse(!0);let l=o.startContainer,a=o.startOffset,d=new T(t,5),c=l,f=a;(!(c instanceof Text)||f===c.data.length)&&(c=Ge(d,"nextNode",c),f=0);let u=l,m=a-1;(!(u instanceof Text)||m===-1)&&(u=Ge(d,"previousPONode",c||(l instanceof Text?l:l.childNodes[a]||l)),u instanceof Text&&(m=u.data.length));let h=null,g=0;return c instanceof Text&&c.data.charAt(f)===" "&&X(o,t)?(h=c,g=f):u instanceof Text&&u.data.charAt(m)===" "&&(c instanceof Text&&c.data.charAt(f)===" "||Z(o,t))&&(h=u,g=m),h&&h.replaceData(g,1,"\xA0"),o.setStart(l,a),o.collapse(!0),s},Ze=(o,t,e)=>{let n=t.firstChild&&N(t.firstChild),i;for(D(t,e),i=t;i=W(i,e);)x(i);o.collapsed||H(o,e),_(o),o.collapse(!1);let s=S(o.endContainer,e,"BLOCKQUOTE")||e,r=L(o,e),l=null,a=W(t,t),d=!n&&!!r&&ce(r);if(r&&a&&!d&&!S(a,t,"PRE")&&!S(a,t,"TABLE")){q(o,r,r,e),o.collapse(!0);let c=o.endContainer,f=o.endOffset;if(le(r,e,!1),N(c)){let u=w(c,f,z(c,e)||e,e);c=u.parentNode,f=Array.from(c.childNodes).indexOf(u)}if(f!==y(c))for(l=document.createDocumentFragment();i=c.childNodes[f];)l.appendChild(i);Y(c,a,o,e),f=Array.from(c.parentNode.childNodes).indexOf(c)+1,c=c.parentNode,o.setEnd(c,f)}if(y(t)){d&&r&&(o.setEndBefore(r),o.collapse(!1),E(r)),q(o,s,s,e);let c=w(o.endContainer,o.endOffset,s,e),f=c?c.previousSibling:s.lastChild;s.insertBefore(t,c),c?o.setEndBefore(c):o.setEnd(s,y(s)),r=G(o,e),_(o);let u=o.endContainer,m=o.endOffset;c&&Q(c)&&F(c,e),c=f&&f.nextSibling,c&&Q(c)&&F(c,e),o.setEnd(u,m)}if(l&&r){let c=o.cloneRange();x(l),Y(r,l,c,e),o.setEnd(c.endContainer,c.endOffset)}_(o)};var ge=o=>{if(o.collapsed)return"";let t=o.startContainer,e=o.endContainer,n=new T(o.commonAncestorContainer,5,a=>K(o,a,!0));n.currentNode=t;let i=t,s="",r=!1,l;for((!(i instanceof Element)&&!(i instanceof Text)||!n.filter(i))&&(i=n.nextNode());i;)i instanceof Text?(l=i.data,l&&/\S/.test(l)&&(i===e&&(l=l.slice(0,o.endOffset)),i===t&&(l=l.slice(o.startOffset)),s+=l,r=!0)):(i.nodeName==="BR"||r&&!N(i))&&(s+=` +`,r=!1),i=n.nextNode();return s=s.replace(/ /g," "),s};var De=Array.prototype.indexOf,je=(o,t,e,n,i,s,r)=>{let l=o.clipboardData;if(se||!l)return!1;let a=s?"":ge(t),d=L(t,e),c=G(t,e),f=e;d===c&&(d!=null&&d.contains(t.commonAncestorContainer))&&(f=d);let u;n?u=H(t,e):(t=t.cloneRange(),_(t),q(t,f,f,e),u=t.cloneContents());let m=t.commonAncestorContainer;for(m instanceof Text&&(m=m.parentNode);m&&m!==f;){let g=m.cloneNode(!1);g.appendChild(u),u=g,m=m.parentNode}let h;if(u.childNodes.length===1&&u.childNodes[0]instanceof Text)a=u.childNodes[0].data.replace(/ /g," "),r=!0;else{let g=p("DIV");g.appendChild(u),h=g.innerHTML,i&&(h=i(h))}return s&&h!==void 0&&(a=s(h)),fe&&(a=a.replace(/\r?\n/g,`\r +`)),!r&&h&&a!==h&&l.setData("text/html",h),l.setData("text/plain",a),o.preventDefault(),!0},Qe=function(o){let t=this.getSelection(),e=this._root;if(t.collapsed){o.preventDefault();return}this.saveUndoState(t),je(o,t,e,!0,this._config.willCutCopy,this._config.toPlainText,!1)||setTimeout(()=>{try{this._ensureBottomLine()}catch(i){this._config.didError(i)}},0),this.setSelection(t)},Xe=function(o){je(o,this.getSelection(),this._root,!1,this._config.willCutCopy,this._config.toPlainText,!1)},Ae=function(o){this._isShiftDown=o.shiftKey},$e=function(o){let t=o.clipboardData,e=t==null?void 0:t.items,n=this._isShiftDown,i=!1,s=!1,r=null,l=null;if(e){let b=e.length;for(;b--;){let O=e[b],A=O.type;A==="text/html"?l=O:A==="text/plain"||A==="text/uri-list"?r=O:A==="text/rtf"?i=!0:/^image\/.*/.test(A)&&(s=!0)}if(s&&!(i&&l)){o.preventDefault(),this.fireEvent("pasteImage",{clipboardData:t});return}if(!se){o.preventDefault(),l&&(!n||!r)?l.getAsString(O=>{this.insertHTML(O,!0)}):r&&r.getAsString(O=>{let A=!1,Ie=this.getSelection();if(!Ie.collapsed&&I.test(Ie.toString())){let Me=this.linkRegExp.exec(O);A=!!Me&&Me[0].length===O.length}A?this.makeLink(O):this.insertPlainText(O,!0)});return}}let a=t==null?void 0:t.types;if(!se&&a&&(De.call(a,"text/html")>-1||!Fe&&De.call(a,"text/plain")>-1&&De.call(a,"text/rtf")<0)){o.preventDefault();let b;!n&&(b=t.getData("text/html"))?this.insertHTML(b,!0):((b=t.getData("text/plain"))||(b=t.getData("text/uri-list")))&&this.insertPlainText(b,!0);return}let d=document.body,c=this.getSelection(),f=c.startContainer,u=c.startOffset,m=c.endContainer,h=c.endOffset,g=p("DIV",{contenteditable:"true",style:"position:fixed; overflow:hidden; top:0; right:100%; width:1px; height:1px;"});d.appendChild(g),c.selectNodeContents(g),this.setSelection(c),setTimeout(()=>{try{let b="",O=g,A;for(;g=O;)O=g.nextSibling,E(g),A=g.firstChild,A&&A===g.lastChild&&A instanceof HTMLDivElement&&(g=A),b+=g.innerHTML;this.setSelection($(f,u,m,h)),b&&this.insertHTML(b,!0)}catch(b){this._config.didError(b)}},0)},Ve=function(o){if(!o.dataTransfer)return;let t=o.dataTransfer.types,e=t.length,n=!1,i=!1;for(;e--;)switch(t[e]){case"text/plain":n=!0;break;case"text/html":i=!0;break;default:return}(i||n&&this.saveUndoState)&&this.saveUndoState()};var we=(o,t,e)=>{t.preventDefault(),o.splitBlock(t.shiftKey,e)};var ne=(o,t)=>{try{t||(t=o.getSelection());let e=t.startContainer;e instanceof Text&&(e=e.parentNode);let n=e;for(;N(n)&&(!n.textContent||n.textContent===B);)e=n,n=e.parentNode;e!==n&&(t.setStart(n,Array.from(n.childNodes).indexOf(e)),t.collapse(!0),n.removeChild(e),P(n)||(n=z(n,o._root)||o._root),x(n),_(t)),e===o._root&&(e=e.firstChild)&&e.nodeName==="BR"&&E(e),o._ensureBottomLine(),o.setSelection(t),o._updatePath(t,!0)}catch(e){o._config.didError(e)}},Ee=(o,t)=>{let e;for(;(e=o.parentNode)&&!(e===t||e.isContentEditable);)o=e;E(o)},Te=(o,t,e)=>{if(S(t,o._root,"A"))return;let n=t.data||"",i=Math.max(n.lastIndexOf(" ",e-1),n.lastIndexOf("\xA0",e-1))+1,s=n.slice(i,e),r=o.linkRegExp.exec(s);if(r){let l=o.getSelection();o._docWasChanged(),o._recordUndoState(l),o._getRangeAndRemoveBookmark(l);let a=i+r.index,d=a+r[0].length,c=l.startContainer===t,f=l.startOffset-d;a&&(t=t.splitText(a));let u=o._config.tagAttributes.a,m=p("A",Object.assign({href:r[1]?/^(?:ht|f)tps?:/i.test(r[1])?r[1]:"http://"+r[1]:"mailto:"+r[0]},u));m.textContent=n.slice(a,d),t.parentNode.insertBefore(m,t),t.data=n.slice(d),c&&(l.setStart(t,f),l.setEnd(t,f)),o.setSelection(l)}};var Ye=(o,t,e)=>{let n=o._root;if(o._removeZWS(),o.saveUndoState(e),!e.collapsed)t.preventDefault(),H(e,n),ne(o,e);else if(X(e,n)){t.preventDefault();let i=L(e,n);if(!i)return;let s=i;D(s.parentNode,n);let r=z(s,n);if(r){if(!r.isContentEditable){Ee(r,n);return}for(Y(r,s,e,n),s=r.parentNode;s!==n&&!s.nextSibling;)s=s.parentNode;s!==n&&(s=s.nextSibling)&&F(s,n),o.setSelection(e)}else if(s){if(S(s,n,"UL")||S(s,n,"OL")){o.decreaseListLevel(e);return}else if(S(s,n,"BLOCKQUOTE")){o.removeQuote(e);return}o.setSelection(e),o._updatePath(e,!0)}}else{_(e);let i=e.startContainer,s=e.startOffset,r=i.parentNode;i instanceof Text&&r instanceof HTMLAnchorElement&&s&&r.href.includes(i.data)?(i.deleteData(s-1,1),o.setSelection(e),o.removeLink(),t.preventDefault()):(o.setSelection(e),setTimeout(()=>{ne(o)},0))}};var Je=(o,t,e)=>{let n=o._root,i,s,r,l,a,d;if(o._removeZWS(),o.saveUndoState(e),!e.collapsed)t.preventDefault(),H(e,n),ne(o,e);else if(Z(e,n)){if(t.preventDefault(),i=L(e,n),!i)return;if(D(i.parentNode,n),s=W(i,n),s){if(!s.isContentEditable){Ee(s,n);return}for(Y(i,s,e,n),s=i.parentNode;s!==n&&!s.nextSibling;)s=s.parentNode;s!==n&&(s=s.nextSibling)&&F(s,n),o.setSelection(e),o._updatePath(e,!0)}}else{if(r=e.cloneRange(),q(e,n,n,n),l=e.endContainer,a=e.endOffset,l instanceof Element&&(d=l.childNodes[a],d&&d.nodeName==="IMG")){t.preventDefault(),E(d),_(e),ne(o,e);return}o.setSelection(r),setTimeout(()=>{ne(o)},0)}};var et=(o,t,e)=>{let n=o._root;if(o._removeZWS(),e.collapsed&&X(e,n)){let i=L(e,n),s;for(;s=i.parentNode;){if(s.nodeName==="UL"||s.nodeName==="OL"){t.preventDefault(),o.increaseListLevel(e);break}i=s}}},tt=(o,t,e)=>{let n=o._root;if(o._removeZWS(),e.collapsed&&X(e,n)){let i=e.startContainer;(S(i,n,"UL")||S(i,n,"OL"))&&(t.preventDefault(),o.decreaseListLevel(e))}};var nt=(o,t,e)=>{var s;let n,i=o._root;if(o._recordUndoState(e),o._getRangeAndRemoveBookmark(e),!e.collapsed)H(e,i),o._ensureBottomLine(),o.setSelection(e),o._updatePath(e,!0);else if(Z(e,i)){let r=L(e,i);if(r&&r.nodeName!=="PRE"){let l=(s=r.textContent)==null?void 0:s.trimEnd().replace(B,"");if(l==="*"||l==="1."){t.preventDefault(),o.insertPlainText(" ",!1),o._docWasChanged(),o.saveUndoState(e);let a=new T(r,4),d;for(;d=a.nextNode();)E(d);l==="*"?o.makeUnorderedList():o.makeOrderedList();return}}}if(n=e.endContainer,e.endOffset===y(n))do if(n.nodeName==="A"){e.setStartAfter(n);break}while(!n.nextSibling&&(n=n.parentNode)&&n!==i);if(o._config.addLinks){let r=e.cloneRange();_(r);let l=r.startContainer,a=r.startOffset;setTimeout(()=>{Te(o,l,a)},0)}o.setSelection(e)};var ot=function(o){if(o.defaultPrevented||o.isComposing)return;let t=o.key,e="",n=o.code;/^Digit\d$/.test(n)&&(t=n.slice(-1)),t!=="Backspace"&&t!=="Delete"&&(o.altKey&&(e+="Alt-"),o.ctrlKey&&(e+="Ctrl-"),o.metaKey&&(e+="Meta-"),o.shiftKey&&(e+="Shift-")),fe&&o.shiftKey&&t==="Delete"&&(e+="Shift-"),t=e+t;let i=this.getSelection();this._keyHandlers[t]?this._keyHandlers[t](this,o,i):!i.collapsed&&!o.ctrlKey&&!o.metaKey&&t.length===1&&(this.saveUndoState(i),H(i,this._root),this._ensureBottomLine(),this.setSelection(i),this._updatePath(i,!0))},v={Backspace:Ye,Delete:Je,Tab:et,"Shift-Tab":tt," ":nt,ArrowLeft(o){o._removeZWS()},ArrowRight(o,t,e){o._removeZWS();let n=o.getRoot();if(Z(e,n)){_(e);let i=e.endContainer;do if(i.nodeName==="CODE"){let s=i.nextSibling;if(!(s instanceof Text)){let r=document.createTextNode("\xA0");i.parentNode.insertBefore(r,s),s=r}e.setStart(s,1),o.setSelection(e),t.preventDefault();break}while(!i.nextSibling&&(i=i.parentNode)&&i!==n)}}};He||(v.Enter=we,v["Shift-Enter"]=we);!de&&!_e&&(v.PageUp=o=>{o.moveCursorToStart()},v.PageDown=o=>{o.moveCursorToEnd()});var ie=(o,t)=>(t=t||null,(e,n)=>{n.preventDefault();let i=e.getSelection();e.hasFormat(o,null,i)?e.changeFormat(null,{tag:o},i):e.changeFormat({tag:o},t,i)});v[k+"b"]=ie("B");v[k+"i"]=ie("I");v[k+"u"]=ie("U");v[k+"Shift-7"]=ie("S");v[k+"Shift-5"]=ie("SUB",{tag:"SUP"});v[k+"Shift-6"]=ie("SUP",{tag:"SUB"});v[k+"Shift-8"]=(o,t)=>{t.preventDefault();let e=o.getPath();/(?:^|>)UL/.test(e)?o.removeList():o.makeUnorderedList()};v[k+"Shift-9"]=(o,t)=>{t.preventDefault();let e=o.getPath();/(?:^|>)OL/.test(e)?o.removeList():o.makeOrderedList()};v[k+"["]=(o,t)=>{t.preventDefault();let e=o.getPath();/(?:^|>)BLOCKQUOTE/.test(e)||!/(?:^|>)[OU]L/.test(e)?o.decreaseQuoteLevel():o.decreaseListLevel()};v[k+"]"]=(o,t)=>{t.preventDefault();let e=o.getPath();/(?:^|>)BLOCKQUOTE/.test(e)||!/(?:^|>)[OU]L/.test(e)?o.increaseQuoteLevel():o.increaseListLevel()};v[k+"d"]=(o,t)=>{t.preventDefault(),o.toggleCode()};v[k+"z"]=(o,t)=>{t.preventDefault(),o.undo()};v[k+"y"]=v[k+"Shift-z"]=v[k+"Shift-Z"]=(o,t)=>{t.preventDefault(),o.redo()};var Ce=class{constructor(t,e){this.customEvents=new Set(["pathChange","select","input","pasteImage","undoStateChange"]);this.startSelectionId="squire-selection-start";this.endSelectionId="squire-selection-end";this.linkRegExp=/\b(?:((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9][a-z0-9.\-]*[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:[^\s?&`!()\[\]{};:'".,<>«»“”‘’]|\([^\s()<>]+\)))|([\w\-.%+]+@(?:[\w\-]+\.)+[a-z]{2,}\b(?:[?][^&?\s]+=[^\s?&`!()\[\]{};:'".,<>«»“”‘’]+(?:&[^&?\s]+=[^\s?&`!()\[\]{};:'".,<>«»“”‘’]+)*)?))/i;this.tagAfterSplit={DT:"DD",DD:"DT",LI:"LI",PRE:"PRE"};this._root=t,this._config=this._makeConfig(e),this._isFocused=!1,this._lastSelection=$(t,0),this._willRestoreSelection=!1,this._mayHaveZWS=!1,this._lastAnchorNode=null,this._lastFocusNode=null,this._path="",this._events=new Map,this._undoIndex=-1,this._undoStack=[],this._undoStackLength=0,this._isInUndoState=!1,this._ignoreChange=!1,this._ignoreAllChanges=!1,this.addEventListener("selectionchange",this._updatePathOnEvent),this.addEventListener("blur",this._enableRestoreSelection),this.addEventListener("mousedown",this._disableRestoreSelection),this.addEventListener("touchstart",this._disableRestoreSelection),this.addEventListener("focus",this._restoreSelection),this._isShiftDown=!1,this.addEventListener("cut",Qe),this.addEventListener("copy",Xe),this.addEventListener("paste",$e),this.addEventListener("drop",Ve),this.addEventListener("keydown",Ae),this.addEventListener("keyup",Ae),this.addEventListener("keydown",ot),this._keyHandlers=Object.create(v);let n=new MutationObserver(()=>this._docWasChanged());n.observe(t,{childList:!0,attributes:!0,characterData:!0,subtree:!0}),this._mutation=n,t.setAttribute("contenteditable","true"),this.addEventListener("beforeinput",this._beforeInput),this.setHTML("")}destroy(){this._events.forEach((t,e)=>{this.removeEventListener(e)}),this._mutation.disconnect(),this._undoIndex=-1,this._undoStack=[],this._undoStackLength=0}_makeConfig(t){let e={blockTag:"DIV",blockAttributes:null,tagAttributes:{},classNames:{color:"color",fontFamily:"font",fontSize:"size",highlight:"highlight"},undo:{documentSizeThreshold:-1,undoLimit:-1},addLinks:!0,willCutCopy:null,toPlainText:null,sanitizeToDOMFragment:n=>{let i=DOMPurify.sanitize(n,{ALLOW_UNKNOWN_PROTOCOLS:!0,WHOLE_DOCUMENT:!1,RETURN_DOM:!0,RETURN_DOM_FRAGMENT:!0,FORCE_BODY:!1});return i?document.importNode(i,!0):document.createDocumentFragment()},didError:n=>console.log(n)};return t&&(Object.assign(e,t),e.blockTag=e.blockTag.toUpperCase()),e}setKeyHandler(t,e){return this._keyHandlers[t]=e,this}_beforeInput(t){switch(t.inputType){case"insertLineBreak":t.preventDefault(),this.splitBlock(!0);break;case"insertParagraph":t.preventDefault(),this.splitBlock(!1);break;case"insertOrderedList":t.preventDefault(),this.makeOrderedList();break;case"insertUnoderedList":t.preventDefault(),this.makeUnorderedList();break;case"historyUndo":t.preventDefault(),this.undo();break;case"historyRedo":t.preventDefault(),this.redo();break;case"formatBold":t.preventDefault(),this.bold();break;case"formaItalic":t.preventDefault(),this.italic();break;case"formatUnderline":t.preventDefault(),this.underline();break;case"formatStrikeThrough":t.preventDefault(),this.strikethrough();break;case"formatSuperscript":t.preventDefault(),this.superscript();break;case"formatSubscript":t.preventDefault(),this.subscript();break;case"formatJustifyFull":case"formatJustifyCenter":case"formatJustifyRight":case"formatJustifyLeft":{t.preventDefault();let e=t.inputType.slice(13).toLowerCase();e==="full"&&(e="justify"),this.setTextAlignment(e);break}case"formatRemove":t.preventDefault(),this.removeAllFormatting();break;case"formatSetBlockTextDirection":{t.preventDefault();let e=t.data;e==="null"&&(e=null),this.setTextDirection(e);break}case"formatBackColor":t.preventDefault(),this.setHighlightColor(t.data);break;case"formatFontColor":t.preventDefault(),this.setTextColor(t.data);break;case"formatFontName":t.preventDefault(),this.setFontFace(t.data);break}}handleEvent(t){this.fireEvent(t.type,t)}fireEvent(t,e){let n=this._events.get(t);if(/^(?:focus|blur)/.test(t)){let i=this._root===document.activeElement;if(t==="focus"){if(!i||this._isFocused)return this;this._isFocused=!0}else{if(i||!this._isFocused)return this;this._isFocused=!1}}if(n){let i=e instanceof Event?e:new CustomEvent(t,{detail:e});n=n.slice();for(let s of n)try{"handleEvent"in s?s.handleEvent(i):s.call(this,i)}catch(r){this._config.didError(r)}}return this}addEventListener(t,e){let n=this._events.get(t),i=this._root;return n||(n=[],this._events.set(t,n),this.customEvents.has(t)||(t==="selectionchange"&&(i=document),i.addEventListener(t,this,!0))),n.push(e),this}removeEventListener(t,e){let n=this._events.get(t),i=this._root;if(n){if(e){let s=n.length;for(;s--;)n[s]===e&&n.splice(s,1)}else n.length=0;n.length||(this._events.delete(t),this.customEvents.has(t)||(t==="selectionchange"&&(i=document),i.removeEventListener(t,this,!0)))}return this}focus(){return this._root.focus({preventScroll:!0}),this}blur(){return this._root.blur(),this}_enableRestoreSelection(){this._willRestoreSelection=!0}_disableRestoreSelection(){this._willRestoreSelection=!1}_restoreSelection(){this._willRestoreSelection&&this.setSelection(this._lastSelection)}_removeZWS(){this._mayHaveZWS&&(re(this._root),this._mayHaveZWS=!1)}_saveRangeToBookmark(t){let e=p("INPUT",{id:this.startSelectionId,type:"hidden"}),n=p("INPUT",{id:this.endSelectionId,type:"hidden"}),i;j(t,e),t.collapse(!1),j(t,n),e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING&&(e.id=this.endSelectionId,n.id=this.startSelectionId,i=e,e=n,n=i),t.setStartAfter(e),t.setEndBefore(n)}_getRangeAndRemoveBookmark(t){let e=this._root,n=e.querySelector("#"+this.startSelectionId),i=e.querySelector("#"+this.endSelectionId);if(n&&i){let s=n.parentNode,r=i.parentNode,l=Array.from(s.childNodes).indexOf(n),a=Array.from(r.childNodes).indexOf(i);s===r&&(a-=1),n.remove(),i.remove(),t||(t=document.createRange()),t.setStart(s,l),t.setEnd(r,a),te(s,t),s!==r&&te(r,t),t.collapsed&&(s=t.startContainer,s instanceof Text&&(r=s.childNodes[t.startOffset],(!r||!(r instanceof Text))&&(r=s.childNodes[t.startOffset-1]),r&&r instanceof Text&&(t.setStart(r,0),t.collapse(!0))))}return t||null}getSelection(){let t=window.getSelection(),e=this._root,n=null;if(this._isFocused&&t&&t.rangeCount){n=t.getRangeAt(0).cloneRange();let i=n.startContainer,s=n.endContainer;i&&M(i)&&n.setStartBefore(i),s&&M(s)&&n.setEndBefore(s)}return n&&e.contains(n.commonAncestorContainer)?this._lastSelection=n:(n=this._lastSelection,document.contains(n.commonAncestorContainer)||(n=null)),n||(n=$(e.firstElementChild||e,0)),n}setSelection(t){if(this._lastSelection=t,!this._isFocused)this._enableRestoreSelection();else{let e=window.getSelection();e&&("setBaseAndExtent"in Selection.prototype?e.setBaseAndExtent(t.startContainer,t.startOffset,t.endContainer,t.endOffset):(e.removeAllRanges(),e.addRange(t)))}return this}_moveCursorTo(t){let e=this._root,n=$(e,t?0:e.childNodes.length);return _(n),this.setSelection(n),this}moveCursorToStart(){return this._moveCursorTo(!0)}moveCursorToEnd(){return this._moveCursorTo(!1)}getCursorPosition(){let t=this.getSelection(),e=t.getBoundingClientRect();if(e&&!e.top){this._ignoreChange=!0;let n=p("SPAN");n.textContent=B,j(t,n),e=n.getBoundingClientRect();let i=n.parentNode;i.removeChild(n),te(i,t)}return e}getPath(){return this._path}_updatePathOnEvent(){this._isFocused&&this._updatePath(this.getSelection())}_updatePath(t,e){let n=t.startContainer,i=t.endContainer,s;(e||n!==this._lastAnchorNode||i!==this._lastFocusNode)&&(this._lastAnchorNode=n,this._lastFocusNode=i,s=n&&i?n===i?this._getPath(i):"(selection)":"",this._path!==s&&(this._path=s,this.fireEvent("pathChange",{path:s}))),this.fireEvent(t.collapsed?"cursor":"select",{range:t})}_getPath(t){let e=this._root,n=this._config,i="";if(t&&t!==e){let s=t.parentNode;if(i=s?this._getPath(s):"",t instanceof HTMLElement){let r=t.id,l=t.classList,a=Array.from(l).sort(),d=t.dir,c=n.classNames;i+=(i?">":"")+t.nodeName,r&&(i+="#"+r),a.length&&(i+=".",i+=a.join(".")),d&&(i+="[dir="+d+"]"),l.contains(c.highlight)&&(i+="[backgroundColor="+t.style.backgroundColor.replace(/ /g,"")+"]"),l.contains(c.color)&&(i+="[color="+t.style.color.replace(/ /g,"")+"]"),l.contains(c.fontFamily)&&(i+="[fontFamily="+t.style.fontFamily.replace(/ /g,"")+"]"),l.contains(c.fontSize)&&(i+="[fontSize="+t.style.fontSize+"]")}}return i}modifyDocument(t){let e=this._mutation;return e&&(e.takeRecords().length&&this._docWasChanged(),e.disconnect()),this._ignoreAllChanges=!0,t(),this._ignoreAllChanges=!1,e&&(e.observe(this._root,{childList:!0,attributes:!0,characterData:!0,subtree:!0}),this._ignoreChange=!1),this}_docWasChanged(){if(qe(),this._mayHaveZWS=!0,!this._ignoreAllChanges){if(this._ignoreChange){this._ignoreChange=!1;return}this._isInUndoState&&(this._isInUndoState=!1,this.fireEvent("undoStateChange",{canUndo:!0,canRedo:!1})),this.fireEvent("input")}}_recordUndoState(t,e){let n=this._isInUndoState;if(!n||e){let i=this._undoIndex+1,s=this._undoStack,r=this._config.undo,l=r.documentSizeThreshold,a=r.undoLimit;if(i-1&&d.length*2>l&&a>-1&&i>a&&(s.splice(0,i-a),i=a,this._undoStackLength=a),s[i]=d,this._undoIndex=i,this._undoStackLength+=1,this._isInUndoState=!0}return this}saveUndoState(t){return t||(t=this.getSelection()),this._recordUndoState(t,this._isInUndoState),this._getRangeAndRemoveBookmark(t),this}undo(){if(this._undoIndex!==0||!this._isInUndoState){this._recordUndoState(this.getSelection(),!1),this._undoIndex-=1,this._setRawHTML(this._undoStack[this._undoIndex]);let t=this._getRangeAndRemoveBookmark();t&&this.setSelection(t),this._isInUndoState=!0,this.fireEvent("undoStateChange",{canUndo:this._undoIndex!==0,canRedo:!0}),this.fireEvent("input")}return this.focus()}redo(){let t=this._undoIndex,e=this._undoStackLength;if(t+1",d="<"+r;for(let c in l)d+=" "+c+'="'+ke(l[c])+'"';d+=">";for(let c=0,f=i.length;c")+a),i[c]=u}return this.insertHTML(i.join(""),e)}getSelectedText(t){return ge(t||this.getSelection())}getFontInfo(t){let e={color:void 0,backgroundColor:void 0,fontFamily:void 0,fontSize:void 0};t||(t=this.getSelection());let n=0,i=t.commonAncestorContainer;if(t.collapsed||i instanceof Text)for(i instanceof Text&&(i=i.parentNode);n<4&&i;){let s=i.style;if(s){let r=s.color;!e.color&&r&&(e.color=r,n+=1);let l=s.backgroundColor;!e.backgroundColor&&l&&(e.backgroundColor=l,n+=1);let a=s.fontFamily;!e.fontFamily&&a&&(e.fontFamily=a,n+=1);let d=s.fontSize;!e.fontSize&&d&&(e.fontSize=d,n+=1)}i=i.parentNode}return e}hasFormat(t,e,n){t=t.toUpperCase(),e||(e={}),n||(n=this.getSelection()),!n.collapsed&&n.startContainer instanceof Text&&n.startOffset===n.startContainer.length&&n.startContainer.nextSibling&&n.setStartBefore(n.startContainer.nextSibling),!n.collapsed&&n.endContainer instanceof Text&&n.endOffset===0&&n.endContainer.previousSibling&&n.setEndAfter(n.endContainer.previousSibling);let i=this._root,s=n.commonAncestorContainer;if(S(s,i,t,e))return!0;if(s instanceof Text)return!1;let r=new T(s,4,d=>K(n,d,!0)),l=!1,a;for(;a=r.nextNode();){if(!S(a,i,t,e))return!1;l=!0}return l}changeFormat(t,e,n,i){return n||(n=this.getSelection()),this.saveUndoState(n),e&&(n=this._removeFormat(e.tag.toUpperCase(),e.attributes||{},n,i)),t&&(n=this._addFormat(t.tag.toUpperCase(),t.attributes||{},n)),this.setSelection(n),this._updatePath(n,!0),this.focus()}_addFormat(t,e,n){let i=this._root;if(n.collapsed){let s=x(p(t,e));j(n,s);let r=s.firstChild||s,l=r instanceof Text?r.length:0;n.setStart(r,l),n.collapse(!0);let a=s;for(;N(a);)a=a.parentNode;re(a,s)}else{let s=new T(n.commonAncestorContainer,5,c=>(c instanceof Text||c.nodeName==="BR"||c.nodeName==="IMG")&&K(n,c,!0)),{startContainer:r,startOffset:l,endContainer:a,endOffset:d}=n;if(s.currentNode=r,!(r instanceof Element)&&!(r instanceof Text)||!s.filter(r)){let c=s.nextNode();if(!c)return n;r=c,l=0}do{let c=s.currentNode;if(!S(c,i,t,e)){c===a&&c.length>d&&c.splitText(d),c===r&&l&&(c=c.splitText(l),a===r?(a=c,d-=l):a===r.parentNode&&(d+=1),r=c,l=0);let u=p(t,e);R(c,u),u.appendChild(c)}}while(s.nextNode());n=$(r,l,a,d)}return n}_removeFormat(t,e,n,i){this._saveRangeToBookmark(n);let s;n.collapsed&&(oe?s=document.createTextNode(B):s=document.createTextNode(""),j(n,s));let r=n.commonAncestorContainer;for(;N(r);)r=r.parentNode;let l=n.startContainer,a=n.startOffset,d=n.endContainer,c=n.endOffset,f=[],u=(h,g)=>{if(K(n,h,!1))return;let b,O;if(!K(n,h,!0)){!(h instanceof HTMLInputElement)&&(!(h instanceof Text)||h.data)&&f.push([g,h]);return}if(h instanceof Text)h===d&&c!==h.length&&f.push([g,h.splitText(c)]),h===l&&a&&(h.splitText(a),f.push([g,h]));else for(b=h.firstChild;b;b=O)O=b.nextSibling,u(b,g)},m=Array.from(r.getElementsByTagName(t)).filter(h=>K(n,h,!0)&&he(h,t,e));if(i||m.forEach(h=>{u(h,h)}),f.forEach(([h,g])=>{h=h.cloneNode(!1),R(g,h),h.appendChild(g)}),m.forEach(h=>{R(h,C(h))}),oe&&s){s=s.parentNode;let h=s;for(;h&&N(h);)h=h.parentNode;h&&re(h,s)}return this._getRangeAndRemoveBookmark(n),s&&n.collapse(!1),te(r,n),n}bold(){return this.changeFormat({tag:"B"})}removeBold(){return this.changeFormat(null,{tag:"B"})}italic(){return this.changeFormat({tag:"I"})}removeItalic(){return this.changeFormat(null,{tag:"I"})}underline(){return this.changeFormat({tag:"U"})}removeUnderline(){return this.changeFormat(null,{tag:"U"})}strikethrough(){return this.changeFormat({tag:"S"})}removeStrikethrough(){return this.changeFormat(null,{tag:"S"})}subscript(){return this.changeFormat({tag:"SUB"},{tag:"SUP"})}removeSubscript(){return this.changeFormat(null,{tag:"SUB"})}superscript(){return this.changeFormat({tag:"SUP"},{tag:"SUB"})}removeSuperscript(){return this.changeFormat(null,{tag:"SUP"})}makeLink(t,e){let n=this.getSelection();if(n.collapsed){let i=t.indexOf(":")+1;if(i)for(;t[i]==="/";)i+=1;j(n,document.createTextNode(t.slice(i)))}return e=Object.assign({href:t},this._config.tagAttributes.a,e),this.changeFormat({tag:"A",attributes:e},{tag:"A"},n)}removeLink(){return this.changeFormat(null,{tag:"A"},this.getSelection(),!0)}addDetectedLinks(t,e){let n=new T(t,4,l=>!S(l,e||this._root,"A")),i=this.linkRegExp,s=this._config.tagAttributes.a,r;for(;r=n.nextNode();){let l=r.parentNode,a=r.data,d;for(;d=i.exec(a);){let c=d.index,f=c+d[0].length;c&&l.insertBefore(document.createTextNode(a.slice(0,c)),r);let u=p("A",Object.assign({href:d[1]?/^(?:ht|f)tps?:/i.test(d[1])?d[1]:"http://"+d[1]:"mailto:"+d[0]},s));u.textContent=a.slice(c,f),l.insertBefore(u,r),r.data=a=a.slice(f)}}return this}setFontFace(t){let e=this._config.classNames.fontFamily;return this.changeFormat(t?{tag:"SPAN",attributes:{class:e,style:"font-family: "+t+", sans-serif;"}}:null,{tag:"SPAN",attributes:{class:e}})}setFontSize(t){let e=this._config.classNames.fontSize;return this.changeFormat(t?{tag:"SPAN",attributes:{class:e,style:"font-size: "+(typeof t=="number"?t+"px":t)}}:null,{tag:"SPAN",attributes:{class:e}})}setTextColor(t){let e=this._config.classNames.color;return this.changeFormat(t?{tag:"SPAN",attributes:{class:e,style:"color:"+t}}:null,{tag:"SPAN",attributes:{class:e}})}setHighlightColor(t){let e=this._config.classNames.highlight;return this.changeFormat(t?{tag:"SPAN",attributes:{class:e,style:"background-color:"+t}}:null,{tag:"SPAN",attributes:{class:e}})}_ensureBottomLine(){let t=this._root,e=t.lastElementChild;(!e||e.nodeName!==this._config.blockTag||!P(e))&&t.appendChild(this.createDefaultBlock())}createDefaultBlock(t){let e=this._config;return x(p(e.blockTag,e.blockAttributes,t))}splitBlock(t,e){e||(e=this.getSelection());let n=this._root,i,s,r,l;if(this._recordUndoState(e),this._removeZWS(),this._getRangeAndRemoveBookmark(e),e.collapsed||H(e,n),this._config.addLinks){_(e);let u=e.startContainer,m=e.startOffset;setTimeout(()=>{Te(this,u,m)},0)}if(i=L(e,n),i&&(s=S(i,n,"PRE"))){_(e),r=e.startContainer;let u=e.startOffset;return r instanceof Text||(r=document.createTextNode(""),s.insertBefore(r,s.firstChild)),!t&&r instanceof Text&&(r.data.charAt(u-1)===` `||X(e,n))&&(r.data.charAt(u)===` `||Z(e,n))?(r.deleteData(u&&u-1,u?2:1),l=w(r,u&&u-1,n,n),r=l.previousSibling,r.textContent||E(r),r=this.createDefaultBlock(),l.parentNode.insertBefore(r,l),l.textContent||E(l),e.setStart(r,0)):(r.insertData(u,` -`),x(s),r.length===u+1?e.setStartAfter(r):e.setStart(r,u+1)),e.collapse(!0),this.setSelection(e),this._updatePath(e,!0),this._docWasChanged(),this}if(!i||t||/^T[HD]$/.test(i.nodeName))return Oe(e,"A",n),j(e,p("BR")),e.collapse(!1),this.setSelection(e),this._updatePath(e,!0),this;if((s=S(i,n,"LI"))&&(i=s),ce(i)){if(S(i,n,"UL")||S(i,n,"OL"))return this.decreaseListLevel(e),this;if(S(i,n,"BLOCKQUOTE"))return this.removeQuote(e),this}r=e.startContainer;let a=e.startOffset,d=this.tagAfterSplit[i.nodeName];l=w(r,a,i.parentNode,this._root);let c=this._config,f=null;for(d||(d=c.blockTag,f=c.blockAttributes),he(l,d,f)||(i=p(d,f),l.dir&&(i.dir=l.dir),R(l,i),i.appendChild(C(l)),l=i),re(i),Se(i),x(i);l instanceof Element;){let u=l.firstChild,m;if(l.nodeName==="A"&&(!l.textContent||l.textContent===y)){u=document.createTextNode(""),R(l,u),l=u;break}for(;u&&u instanceof Text&&!u.data&&(m=u.nextSibling,!(!m||m.nodeName==="BR"));)E(u),u=m;if(!u||u.nodeName==="BR"||u instanceof Text)break;l=u}return e=V(l,0),this.setSelection(e),this._updatePath(e,!0),this}forEachBlock(t,e,n){n||(n=this.getSelection()),e&&this.saveUndoState(n);let i=this._root,s=L(n,i),r=G(n,i);if(s&&r)do if(t(s)||s===r)break;while(s=W(s,i));return e&&(this.setSelection(n),this._updatePath(n,!0)),this}modifyBlocks(t,e){e||(e=this.getSelection()),this._recordUndoState(e,this._isInUndoState);let n=this._root;ye(e,n),q(e,n,n,n);let i=Be(e,n,n);if(!e.collapsed){let s=e.endContainer;if(s===n)e.collapse(!1);else{for(;s.parentNode!==n;)s=s.parentNode;e.setStartBefore(s),e.collapse(!0)}}return j(e,t.call(this,i)),e.endOffset{let n=e.className.split(/\s+/).filter(i=>!!i&&!/^align/.test(i)).join(" ");t?(e.className=n+" align-"+t,e.style.textAlign=t):(e.className=n,e.style.textAlign="")},!0),this.focus()}setTextDirection(t){return this.forEachBlock(e=>{t?e.dir=t:e.removeAttribute("dir")},!0),this.focus()}_getListSelection(t,e){let n=t.commonAncestorContainer,i=t.startContainer,s=t.endContainer;for(;n&&n!==e&&!/^[OU]L$/.test(n.nodeName);)n=n.parentNode;if(!n||n===e)return null;for(i===n&&(i=i.childNodes[t.startOffset]),s===n&&(s=s.childNodes[t.endOffset]);i&&i.parentNode!==n;)i=i.parentNode;for(;s&&s.parentNode!==n;)s=s.parentNode;return[n,i,s]}increaseListLevel(t){t||(t=this.getSelection());let e=this._root,n=this._getListSelection(t,e);if(!n)return this.focus();let[i,s,r]=n;if(!s||s===i.firstChild)return this.focus();this._recordUndoState(t,this._isInUndoState);let l=i.nodeName,a=s.previousSibling,d,c;a.nodeName!==l&&(d=this._config.tagAttributes[l.toLowerCase()],a=p(l,d),i.insertBefore(a,s));do c=s===r?null:s.nextSibling,a.appendChild(s);while(s=c);return c=a.nextSibling,c&&F(c,e),this._getRangeAndRemoveBookmark(t),this.setSelection(t),this._updatePath(t,!0),this.focus()}decreaseListLevel(t){t||(t=this.getSelection());let e=this._root,n=this._getListSelection(t,e);if(!n)return this.focus();let[i,s,r]=n;s||(s=i.firstChild),r||(r=i.lastChild),this._recordUndoState(t,this._isInUndoState);let l,a=null;if(s){let d=i.parentNode;if(a=r.nextSibling?w(i,r.nextSibling,d,e):i.nextSibling,d!==e&&d.nodeName==="LI"){for(d=d.parentNode;a;)l=a.nextSibling,r.appendChild(a),a=l;a=i.parentNode.nextSibling}let c=!/^[OU]L$/.test(d.nodeName);do l=s===r?null:s.nextSibling,i.removeChild(s),c&&s.nodeName==="LI"&&(s=this.createDefaultBlock([C(s)])),d.insertBefore(s,a);while(s=l)}return i.firstChild||E(i),a&&F(a,e),this._getRangeAndRemoveBookmark(t),this.setSelection(t),this._updatePath(t,!0),this.focus()}_makeList(t,e){let n=ae(t,this._root),i=this._config.tagAttributes,s=i[e.toLowerCase()],r=i.li,l;for(;l=n.nextNode();)if(l.parentNode instanceof HTMLLIElement&&(l=l.parentNode,n.currentNode=l.lastChild),l instanceof HTMLLIElement){l=l.parentNode;let a=l.nodeName;a!==e&&/^[OU]L$/.test(a)&&R(l,p(e,s,[C(l)]))}else{let a=p("LI",r);l.dir&&(a.dir=l.dir);let d=l.previousSibling;d&&d.nodeName===e?(d.appendChild(a),E(l)):R(l,p(e,s,[a])),a.appendChild(C(l)),n.currentNode=a}return t}makeUnorderedList(){return this.modifyBlocks(t=>this._makeList(t,"UL")),this.focus()}makeOrderedList(){return this.modifyBlocks(t=>this._makeList(t,"OL")),this.focus()}removeList(){return this.modifyBlocks(t=>{let e=t.querySelectorAll("UL, OL"),n=t.querySelectorAll("LI"),i=this._root;for(let s=0,r=e.length;sp("BLOCKQUOTE",this._config.tagAttributes.blockquote,[e]),t),this.focus()}decreaseQuoteLevel(t){return this.modifyBlocks(e=>(Array.from(e.querySelectorAll("blockquote")).filter(n=>!S(n.parentNode,e,"BLOCKQUOTE")).forEach(n=>{R(n,C(n))}),e),t),this.focus()}removeQuote(t){return this.modifyBlocks(()=>this.createDefaultBlock([p("INPUT",{id:this.startSelectionId,type:"hidden"}),p("INPUT",{id:this.endSelectionId,type:"hidden"})]),t),this.focus()}code(){let t=this.getSelection();return t.collapsed||Q(t.commonAncestorContainer)?(this.modifyBlocks(e=>{let n=this._root,i=document.createDocumentFragment(),s=ae(e,n),r;for(;r=s.nextNode();){let a=r.querySelectorAll("BR"),d=[],c=a.length;for(let f=0;f{let n=e.className.split(/\s+/).filter(i=>!!i&&!/^align/.test(i)).join(" ");t?(e.className=n+" align-"+t,e.style.textAlign=t):(e.className=n,e.style.textAlign="")},!0),this.focus()}setTextDirection(t){return this.forEachBlock(e=>{t?e.dir=t:e.removeAttribute("dir")},!0),this.focus()}_getListSelection(t,e){let n=t.commonAncestorContainer,i=t.startContainer,s=t.endContainer;for(;n&&n!==e&&!/^[OU]L$/.test(n.nodeName);)n=n.parentNode;if(!n||n===e)return null;for(i===n&&(i=i.childNodes[t.startOffset]),s===n&&(s=s.childNodes[t.endOffset]);i&&i.parentNode!==n;)i=i.parentNode;for(;s&&s.parentNode!==n;)s=s.parentNode;return[n,i,s]}increaseListLevel(t){t||(t=this.getSelection());let e=this._root,n=this._getListSelection(t,e);if(!n)return this.focus();let[i,s,r]=n;if(!s||s===i.firstChild)return this.focus();this._recordUndoState(t,this._isInUndoState);let l=i.nodeName,a=s.previousSibling,d,c;a.nodeName!==l&&(d=this._config.tagAttributes[l.toLowerCase()],a=p(l,d),i.insertBefore(a,s));do c=s===r?null:s.nextSibling,a.appendChild(s);while(s=c);return c=a.nextSibling,c&&F(c,e),this._getRangeAndRemoveBookmark(t),this.setSelection(t),this._updatePath(t,!0),this.focus()}decreaseListLevel(t){t||(t=this.getSelection());let e=this._root,n=this._getListSelection(t,e);if(!n)return this.focus();let[i,s,r]=n;s||(s=i.firstChild),r||(r=i.lastChild),this._recordUndoState(t,this._isInUndoState);let l,a=null;if(s){let d=i.parentNode;if(a=r.nextSibling?w(i,r.nextSibling,d,e):i.nextSibling,d!==e&&d.nodeName==="LI"){for(d=d.parentNode;a;)l=a.nextSibling,r.appendChild(a),a=l;a=i.parentNode.nextSibling}let c=!/^[OU]L$/.test(d.nodeName);do l=s===r?null:s.nextSibling,i.removeChild(s),c&&s.nodeName==="LI"&&(s=this.createDefaultBlock([C(s)])),d.insertBefore(s,a);while(s=l)}return i.firstChild||E(i),a&&F(a,e),this._getRangeAndRemoveBookmark(t),this.setSelection(t),this._updatePath(t,!0),this.focus()}_makeList(t,e){let n=ae(t,this._root),i=this._config.tagAttributes,s=i[e.toLowerCase()],r=i.li,l;for(;l=n.nextNode();)if(l.parentNode instanceof HTMLLIElement&&(l=l.parentNode,n.currentNode=l.lastChild),l instanceof HTMLLIElement){l=l.parentNode;let a=l.nodeName;a!==e&&/^[OU]L$/.test(a)&&R(l,p(e,s,[C(l)]))}else{let a=p("LI",r);l.dir&&(a.dir=l.dir);let d=l.previousSibling;d&&d.nodeName===e?(d.appendChild(a),E(l)):R(l,p(e,s,[a])),a.appendChild(C(l)),n.currentNode=a}return t}makeUnorderedList(){return this.modifyBlocks(t=>this._makeList(t,"UL")),this.focus()}makeOrderedList(){return this.modifyBlocks(t=>this._makeList(t,"OL")),this.focus()}removeList(){return this.modifyBlocks(t=>{let e=t.querySelectorAll("UL, OL"),n=t.querySelectorAll("LI"),i=this._root;for(let s=0,r=e.length;sp("BLOCKQUOTE",this._config.tagAttributes.blockquote,[e]),t),this.focus()}decreaseQuoteLevel(t){return this.modifyBlocks(e=>(Array.from(e.querySelectorAll("blockquote")).filter(n=>!S(n.parentNode,e,"BLOCKQUOTE")).forEach(n=>{R(n,C(n))}),e),t),this.focus()}removeQuote(t){return this.modifyBlocks(()=>this.createDefaultBlock([p("INPUT",{id:this.startSelectionId,type:"hidden"}),p("INPUT",{id:this.endSelectionId,type:"hidden"})]),t),this.focus()}code(){let t=this.getSelection();return t.collapsed||Q(t.commonAncestorContainer)?(this.modifyBlocks(e=>{let n=this._root,i=document.createDocumentFragment(),s=ae(e,n),r;for(;r=s.nextNode();){let a=r.querySelectorAll("BR"),d=[],c=a.length;for(let f=0;f{let s=this._root,r=i.querySelectorAll("PRE"),l=r.length;for(;l--;){let a=r[l],d=new T(a,4),c;for(;c=d.nextNode();){let f=c.data;f=f.replace(/ (?= )/g,"\xA0");let u=document.createDocumentFragment(),m;for(;(m=f.indexOf(` `))>-1;)u.appendChild(document.createTextNode(f.slice(0,m))),u.appendChild(p("BR")),f=f.slice(m+1);c.parentNode.insertBefore(u,c),c.data=f}D(a,s),R(a,C(a))}return i},t),this.focus()):this.changeFormat(null,{tag:"CODE"},t),this}toggleCode(){return this.hasFormat("PRE")||this.hasFormat("CODE")?this.removeCode():this.code(),this}_removeFormatting(t,e){for(let n=t.firstChild,i;n;n=i){if(i=n.nextSibling,N(n)){if(n instanceof Text||n.nodeName==="BR"||n.nodeName==="IMG"){e.appendChild(n);continue}}else if(P(n)){e.appendChild(this.createDefaultBlock([this._removeFormatting(n,document.createDocumentFragment())]));continue}this._removeFormatting(n,e)}return e}removeAllFormatting(t){if(t||(t=this.getSelection()),t.collapsed)return this.focus();let e=this._root,n=t.commonAncestorContainer;for(;n&&!P(n);)n=n.parentNode;if(n||(ye(t,e),n=e),n instanceof Text)return this.focus();this.saveUndoState(t),q(t,n,n,e);let i=t.startContainer,s=t.startOffset,r=t.endContainer,l=t.endOffset,a=document.createDocumentFragment(),d=document.createDocumentFragment(),c=w(r,l,n,e),f=w(i,s,n,e),u;for(;f!==c;)u=f.nextSibling,a.appendChild(f),f=u;if(this._removeFormatting(a,d),d.normalize(),f=d.firstChild,u=d.lastChild,f){n.insertBefore(d,c);let m=Array.from(n.childNodes);s=m.indexOf(f),l=u?m.indexOf(u)+1:0}else c&&(s=Array.from(n.childNodes).indexOf(c),l=s);return t.setStart(n,s),t.setEnd(n,l),te(n,t),_(t),this.setSelection(t),this._updatePath(t,!0),this.focus()}};window.Squire=Ce;})(); diff --git a/dist/squire.js.map b/dist/squire.js.map index f6b0ef4..5ed469e 100644 --- a/dist/squire.js.map +++ b/dist/squire.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../source/node/TreeIterator.ts", "../source/Constants.ts", "../source/node/Category.ts", "../source/node/Node.ts", "../source/node/Whitespace.ts", "../source/range/Boundaries.ts", "../source/node/MergeSplit.ts", "../source/Clean.ts", "../source/node/Block.ts", "../source/range/Block.ts", "../source/range/InsertDelete.ts", "../source/range/Contents.ts", "../source/Clipboard.ts", "../source/keyboard/Enter.ts", "../source/keyboard/KeyHelpers.ts", "../source/keyboard/Backspace.ts", "../source/keyboard/Delete.ts", "../source/keyboard/Tab.ts", "../source/keyboard/Space.ts", "../source/keyboard/KeyHandlers.ts", "../source/Editor.ts", "../source/Legacy.ts"], - "sourcesContent": ["type NODE_TYPE = 1 | 4 | 5;\nconst SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT;\nconst SHOW_TEXT = 4; // NodeFilter.SHOW_TEXT;\nconst SHOW_ELEMENT_OR_TEXT = 5; // SHOW_ELEMENT|SHOW_TEXT;\n\nconst always = (): true => true;\n\nclass TreeIterator {\n root: Node;\n currentNode: Node;\n nodeType: NODE_TYPE;\n filter: (n: T) => boolean;\n\n constructor(root: Node, nodeType: NODE_TYPE, filter?: (n: T) => boolean) {\n this.root = root;\n this.currentNode = root;\n this.nodeType = nodeType;\n this.filter = filter || always;\n }\n\n isAcceptableNode(node: Node): boolean {\n const nodeType = node.nodeType;\n const nodeFilterType =\n nodeType === Node.ELEMENT_NODE\n ? SHOW_ELEMENT\n : nodeType === Node.TEXT_NODE\n ? SHOW_TEXT\n : 0;\n return !!(nodeFilterType & this.nodeType) && this.filter(node as T);\n }\n\n nextNode(): T | null {\n const root = this.root;\n let current: Node | null = this.currentNode;\n let node: Node | null;\n while (true) {\n node = current.firstChild;\n while (!node && current) {\n if (current === root) {\n break;\n }\n node = current.nextSibling;\n if (!node) {\n current = current.parentNode;\n }\n }\n if (!node) {\n return null;\n }\n\n if (this.isAcceptableNode(node)) {\n this.currentNode = node;\n return node as T;\n }\n current = node;\n }\n }\n\n previousNode(): T | null {\n const root = this.root;\n let current: Node | null = this.currentNode;\n let node: Node | null;\n while (true) {\n if (current === root) {\n return null;\n }\n node = current.previousSibling;\n if (node) {\n while ((current = node.lastChild)) {\n node = current;\n }\n } else {\n node = current.parentNode;\n }\n if (!node) {\n return null;\n }\n if (this.isAcceptableNode(node)) {\n this.currentNode = node;\n return node as T;\n }\n current = node;\n }\n }\n\n // Previous node in post-order.\n previousPONode(): T | null {\n const root = this.root;\n let current: Node | null = this.currentNode;\n let node: Node | null;\n while (true) {\n node = current.lastChild;\n while (!node && current) {\n if (current === root) {\n break;\n }\n node = current.previousSibling;\n if (!node) {\n current = current.parentNode;\n }\n }\n if (!node) {\n return null;\n }\n if (this.isAcceptableNode(node)) {\n this.currentNode = node;\n return node as T;\n }\n current = node;\n }\n }\n}\n\n// ---\n\nexport { TreeIterator, SHOW_ELEMENT, SHOW_TEXT, SHOW_ELEMENT_OR_TEXT };\n", "const DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING\nconst ELEMENT_NODE = 1; // Node.ELEMENT_NODE;\nconst TEXT_NODE = 3; // Node.TEXT_NODE;\nconst DOCUMENT_NODE = 9; // Node.DOCUMENT_NODE;\nconst DOCUMENT_FRAGMENT_NODE = 11; // Node.DOCUMENT_FRAGMENT_NODE;\n\nconst ZWS = '\\u200B';\n\nconst ua = navigator.userAgent;\n\nconst isMac = /Mac OS X/.test(ua);\nconst isWin = /Windows NT/.test(ua);\nconst isIOS =\n /iP(?:ad|hone|od)/.test(ua) || (isMac && !!navigator.maxTouchPoints);\nconst isAndroid = /Android/.test(ua);\n\nconst isGecko = /Gecko\\//.test(ua);\nconst isLegacyEdge = /Edge\\//.test(ua);\nconst isWebKit = !isLegacyEdge && /WebKit\\//.test(ua);\n\nconst ctrlKey = isMac || isIOS ? 'Meta-' : 'Ctrl-';\n\nconst cantFocusEmptyTextNodes = isWebKit;\n\nconst supportsInputEvents =\n 'onbeforeinput' in document && 'inputType' in new InputEvent('input');\n\n// Use [^ \\t\\r\\n] instead of \\S so that nbsp does not count as white-space\nconst notWS = /[^ \\t\\r\\n]/;\n\n// ---\n\nexport {\n DOCUMENT_POSITION_PRECEDING,\n ELEMENT_NODE,\n TEXT_NODE,\n DOCUMENT_NODE,\n DOCUMENT_FRAGMENT_NODE,\n notWS,\n ZWS,\n ua,\n isMac,\n isWin,\n isIOS,\n isAndroid,\n isGecko,\n isLegacyEdge,\n isWebKit,\n ctrlKey,\n cantFocusEmptyTextNodes,\n supportsInputEvents,\n};\n", "import { ELEMENT_NODE, TEXT_NODE, DOCUMENT_FRAGMENT_NODE } from '../Constants';\n\n// ---\n\nconst inlineNodeNames =\n /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/;\n\nconst leafNodeNames = new Set(['BR', 'HR', 'IFRAME', 'IMG', 'INPUT']);\n\nconst UNKNOWN = 0;\nconst INLINE = 1;\nconst BLOCK = 2;\nconst CONTAINER = 3;\n\n// ---\n\nlet cache: WeakMap = new WeakMap();\n\nconst resetNodeCategoryCache = (): void => {\n cache = new WeakMap();\n};\n\n// ---\n\nconst isLeaf = (node: Node): boolean => {\n return leafNodeNames.has(node.nodeName);\n};\n\nconst getNodeCategory = (node: Node): number => {\n switch (node.nodeType) {\n case TEXT_NODE:\n return INLINE;\n case ELEMENT_NODE:\n case DOCUMENT_FRAGMENT_NODE:\n if (cache.has(node)) {\n return cache.get(node) as number;\n }\n break;\n default:\n return UNKNOWN;\n }\n\n let nodeCategory: number;\n if (!Array.from(node.childNodes).every(isInline)) {\n // Malformed HTML can have block tags inside inline tags. Need to treat\n // these as containers rather than inline. See #239.\n nodeCategory = CONTAINER;\n } else if (inlineNodeNames.test(node.nodeName)) {\n nodeCategory = INLINE;\n } else {\n nodeCategory = BLOCK;\n }\n cache.set(node, nodeCategory);\n return nodeCategory;\n};\n\nconst isInline = (node: Node): boolean => {\n return getNodeCategory(node) === INLINE;\n};\n\nconst isBlock = (node: Node): boolean => {\n return getNodeCategory(node) === BLOCK;\n};\n\nconst isContainer = (node: Node): boolean => {\n return getNodeCategory(node) === CONTAINER;\n};\n\n// ---\n\nexport {\n getNodeCategory,\n isBlock,\n isContainer,\n isInline,\n isLeaf,\n leafNodeNames,\n resetNodeCategoryCache,\n};\n", "import { isLeaf } from './Category';\n\n// ---\n\nconst createElement = (\n tag: string,\n props?: Record | null,\n children?: Node[],\n): HTMLElement => {\n const el = document.createElement(tag);\n if (props instanceof Array) {\n children = props;\n props = null;\n }\n if (props) {\n for (const attr in props) {\n const value = props[attr];\n if (value !== undefined) {\n el.setAttribute(attr, value);\n }\n }\n }\n if (children) {\n children.forEach((node) => el.appendChild(node));\n }\n return el;\n};\n\n// --- Tests\n\nconst areAlike = (\n node: HTMLElement | Node,\n node2: HTMLElement | Node,\n): boolean => {\n if (isLeaf(node)) {\n return false;\n }\n if (node.nodeType !== node2.nodeType || node.nodeName !== node2.nodeName) {\n return false;\n }\n if (node instanceof HTMLElement && node2 instanceof HTMLElement) {\n return (\n node.nodeName !== 'A' &&\n node.className === node2.className &&\n node.style.cssText === node2.style.cssText\n );\n }\n return true;\n};\n\nconst hasTagAttributes = (\n node: Node | Element,\n tag: string,\n attributes?: Record | null,\n): boolean => {\n if (node.nodeName !== tag) {\n return false;\n }\n for (const attr in attributes) {\n if (\n !('getAttribute' in node) ||\n node.getAttribute(attr) !== attributes[attr]\n ) {\n return false;\n }\n }\n return true;\n};\n\n// --- Traversal\n\nconst getNearest = (\n node: Node | null,\n root: Element | DocumentFragment,\n tag: string,\n attributes?: Record | null,\n): Node | null => {\n while (node && node !== root) {\n if (hasTagAttributes(node, tag, attributes)) {\n return node;\n }\n node = node.parentNode;\n }\n return null;\n};\n\nconst getNodeBeforeOffset = (node: Node, offset: number): Node => {\n let children = node.childNodes;\n while (offset && node instanceof Element) {\n node = children[offset - 1];\n children = node.childNodes;\n offset = children.length;\n }\n return node;\n};\n\nconst getNodeAfterOffset = (node: Node, offset: number): Node | null => {\n let returnNode: Node | null = node;\n if (returnNode instanceof Element) {\n const children = returnNode.childNodes;\n if (offset < children.length) {\n returnNode = children[offset];\n } else {\n while (returnNode && !returnNode.nextSibling) {\n returnNode = returnNode.parentNode;\n }\n if (returnNode) {\n returnNode = returnNode.nextSibling;\n }\n }\n }\n return returnNode;\n};\n\nconst getLength = (node: Node): number => {\n return node instanceof Element || node instanceof DocumentFragment\n ? node.childNodes.length\n : node instanceof CharacterData\n ? node.length\n : 0;\n};\n\n// --- Manipulation\n\nconst empty = (node: Node): DocumentFragment => {\n const frag = document.createDocumentFragment();\n let child = node.firstChild;\n while (child) {\n frag.appendChild(child);\n child = node.firstChild;\n }\n return frag;\n};\n\nconst detach = (node: Node): Node => {\n const parent = node.parentNode;\n if (parent) {\n parent.removeChild(node);\n }\n return node;\n};\n\nconst replaceWith = (node: Node, node2: Node): void => {\n const parent = node.parentNode;\n if (parent) {\n parent.replaceChild(node2, node);\n }\n};\n\n// --- Export\n\nexport {\n areAlike,\n createElement,\n detach,\n empty,\n getLength,\n getNearest,\n getNodeAfterOffset,\n getNodeBeforeOffset,\n hasTagAttributes,\n replaceWith,\n};\n", "import { ZWS, notWS } from '../Constants';\nimport { isInline } from './Category';\nimport { getLength } from './Node';\nimport { SHOW_ELEMENT_OR_TEXT, SHOW_TEXT, TreeIterator } from './TreeIterator';\n\n// ---\n\nconst notWSTextNode = (node: Node): boolean => {\n return node instanceof Element\n ? node.nodeName === 'BR'\n : // okay if data is 'undefined' here.\n notWS.test((node as CharacterData).data);\n};\n\nconst isLineBreak = (br: Element, isLBIfEmptyBlock: boolean): boolean => {\n let block = br.parentNode!;\n while (isInline(block)) {\n block = block.parentNode!;\n }\n const walker = new TreeIterator(\n block,\n SHOW_ELEMENT_OR_TEXT,\n notWSTextNode,\n );\n walker.currentNode = br;\n return !!walker.nextNode() || (isLBIfEmptyBlock && !walker.previousNode());\n};\n\n// --- Workaround for browsers that can't focus empty text nodes\n\n// WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256\n\n// Walk down the tree starting at the root and remove any ZWS. If the node only\n// contained ZWS space then remove it too. We may want to keep one ZWS node at\n// the bottom of the tree so the block can be selected. Define that node as the\n// keepNode.\nconst removeZWS = (root: Node, keepNode?: Node | null): void => {\n const walker = new TreeIterator(root, SHOW_TEXT);\n let textNode: Text | null;\n let index: number;\n while ((textNode = walker.nextNode())) {\n while (\n (index = textNode.data.indexOf(ZWS)) > -1 &&\n // eslint-disable-next-line no-unmodified-loop-condition\n (!keepNode || textNode.parentNode !== keepNode)\n ) {\n if (textNode.length === 1) {\n let node: Node = textNode;\n let parent = node.parentNode;\n while (parent) {\n parent.removeChild(node);\n walker.currentNode = parent;\n if (!isInline(parent) || getLength(parent)) {\n break;\n }\n node = parent;\n parent = node.parentNode;\n }\n break;\n } else {\n textNode.deleteData(index, 1);\n }\n }\n }\n};\n\n// ---\n\nexport { isLineBreak, removeZWS };\n", "import { isLeaf } from '../node/Category';\nimport { getLength, getNearest } from '../node/Node';\nimport { isLineBreak } from '../node/Whitespace';\nimport { TEXT_NODE } from '../Constants';\n\n// ---\n\nconst START_TO_START = 0; // Range.START_TO_START\nconst START_TO_END = 1; // Range.START_TO_END\nconst END_TO_END = 2; // Range.END_TO_END\nconst END_TO_START = 3; // Range.END_TO_START\n\nconst isNodeContainedInRange = (\n range: Range,\n node: Node,\n partial: boolean,\n): boolean => {\n const nodeRange = document.createRange();\n nodeRange.selectNode(node);\n if (partial) {\n // Node must not finish before range starts or start after range\n // finishes.\n const nodeEndBeforeStart =\n range.compareBoundaryPoints(END_TO_START, nodeRange) > -1;\n const nodeStartAfterEnd =\n range.compareBoundaryPoints(START_TO_END, nodeRange) < 1;\n return !nodeEndBeforeStart && !nodeStartAfterEnd;\n } else {\n // Node must start after range starts and finish before range\n // finishes\n const nodeStartAfterStart =\n range.compareBoundaryPoints(START_TO_START, nodeRange) < 1;\n const nodeEndBeforeEnd =\n range.compareBoundaryPoints(END_TO_END, nodeRange) > -1;\n return nodeStartAfterStart && nodeEndBeforeEnd;\n }\n};\n\n/**\n * Moves the range to an equivalent position with the start/end as deep in\n * the tree as possible.\n */\nconst moveRangeBoundariesDownTree = (range: Range): void => {\n let { startContainer, startOffset, endContainer, endOffset } = range;\n\n while (!(startContainer instanceof Text)) {\n let child: ChildNode | null = startContainer.childNodes[startOffset];\n if (!child || isLeaf(child)) {\n if (startOffset) {\n child = startContainer.childNodes[startOffset - 1];\n if (child instanceof Text) {\n // Need a new variable to satisfy TypeScript's type checker\n // for some reason.\n let textChild: Text = child;\n // If we have an empty text node next to another text node,\n // just skip and remove it.\n let prev: ChildNode | null;\n while (\n !textChild.length &&\n (prev = textChild.previousSibling) &&\n prev instanceof Text\n ) {\n textChild.remove();\n textChild = prev;\n }\n startContainer = textChild;\n startOffset = textChild.data.length;\n }\n }\n break;\n }\n startContainer = child;\n startOffset = 0;\n }\n if (endOffset) {\n while (!(endContainer instanceof Text)) {\n const child = endContainer.childNodes[endOffset - 1];\n if (!child || isLeaf(child)) {\n if (\n child &&\n child.nodeName === 'BR' &&\n !isLineBreak(child as Element, false)\n ) {\n endOffset -= 1;\n continue;\n }\n break;\n }\n endContainer = child;\n endOffset = getLength(endContainer);\n }\n } else {\n while (!(endContainer instanceof Text)) {\n const child = endContainer.firstChild!;\n if (!child || isLeaf(child)) {\n break;\n }\n endContainer = child;\n }\n }\n\n range.setStart(startContainer, startOffset);\n range.setEnd(endContainer, endOffset);\n};\n\nconst moveRangeBoundariesUpTree = (\n range: Range,\n startMax: Node,\n endMax: Node,\n root: Node,\n): void => {\n let startContainer = range.startContainer;\n let startOffset = range.startOffset;\n let endContainer = range.endContainer;\n let endOffset = range.endOffset;\n let parent: Node;\n\n if (!startMax) {\n startMax = range.commonAncestorContainer;\n }\n if (!endMax) {\n endMax = startMax;\n }\n\n while (\n !startOffset &&\n startContainer !== startMax &&\n startContainer !== root\n ) {\n parent = startContainer.parentNode!;\n startOffset = Array.from(parent.childNodes).indexOf(\n startContainer as ChildNode,\n );\n startContainer = parent;\n }\n\n while (true) {\n if (endContainer === endMax || endContainer === root) {\n break;\n }\n if (\n endContainer.nodeType !== TEXT_NODE &&\n endContainer.childNodes[endOffset] &&\n endContainer.childNodes[endOffset].nodeName === 'BR' &&\n !isLineBreak(endContainer.childNodes[endOffset] as Element, false)\n ) {\n endOffset += 1;\n }\n if (endOffset !== getLength(endContainer)) {\n break;\n }\n parent = endContainer.parentNode!;\n endOffset =\n Array.from(parent.childNodes).indexOf(endContainer as ChildNode) +\n 1;\n endContainer = parent;\n }\n\n range.setStart(startContainer, startOffset);\n range.setEnd(endContainer, endOffset);\n};\n\nconst moveRangeBoundaryOutOf = (\n range: Range,\n tag: string,\n root: Element,\n): Range => {\n let parent = getNearest(range.endContainer, root, tag);\n if (parent && (parent = parent.parentNode)) {\n const clone = range.cloneRange();\n moveRangeBoundariesUpTree(clone, parent, parent, root);\n if (clone.endContainer === parent) {\n range.setStart(clone.endContainer, clone.endOffset);\n range.setEnd(clone.endContainer, clone.endOffset);\n }\n }\n return range;\n};\n\n// ---\n\nexport {\n isNodeContainedInRange,\n moveRangeBoundariesDownTree,\n moveRangeBoundariesUpTree,\n moveRangeBoundaryOutOf,\n};\n", "import { ZWS, cantFocusEmptyTextNodes } from '../Constants';\nimport {\n createElement,\n getNearest,\n areAlike,\n getLength,\n detach,\n empty,\n} from './Node';\nimport { isInline, isContainer } from './Category';\n\n// ---\n\nconst fixCursor = (node: Node): Node => {\n // In Webkit and Gecko, block level elements are collapsed and\n // unfocusable if they have no content. To remedy this, a
must be\n // inserted. In Opera and IE, we just need a textnode in order for the\n // cursor to appear.\n let fixer: Element | Text | null = null;\n\n if (node instanceof Text) {\n return node;\n }\n\n if (isInline(node)) {\n let child = node.firstChild;\n if (cantFocusEmptyTextNodes) {\n while (child && child instanceof Text && !child.data) {\n node.removeChild(child);\n child = node.firstChild;\n }\n }\n if (!child) {\n if (cantFocusEmptyTextNodes) {\n fixer = document.createTextNode(ZWS);\n } else {\n fixer = document.createTextNode('');\n }\n }\n } else if (\n (node instanceof Element || node instanceof DocumentFragment) &&\n !node.querySelector('BR')\n ) {\n fixer = createElement('BR');\n let parent: Element | DocumentFragment = node;\n let child: Element | null;\n while ((child = parent.lastElementChild) && !isInline(child)) {\n parent = child;\n }\n node = parent;\n }\n if (fixer) {\n try {\n node.appendChild(fixer);\n } catch (error) {}\n }\n\n return node;\n};\n\n// Recursively examine container nodes and wrap any inline children.\nconst fixContainer = (\n container: Node,\n root: Element | DocumentFragment,\n): Node => {\n let wrapper: HTMLElement | null = null;\n Array.from(container.childNodes).forEach((child) => {\n const isBR = child.nodeName === 'BR';\n if (!isBR && isInline(child)) {\n if (!wrapper) {\n wrapper = createElement('DIV');\n }\n wrapper.appendChild(child);\n } else if (isBR || wrapper) {\n if (!wrapper) {\n wrapper = createElement('DIV');\n }\n fixCursor(wrapper);\n if (isBR) {\n container.replaceChild(wrapper, child);\n } else {\n container.insertBefore(wrapper, child);\n }\n wrapper = null;\n }\n if (isContainer(child)) {\n fixContainer(child, root);\n }\n });\n if (wrapper) {\n container.appendChild(fixCursor(wrapper));\n }\n return container;\n};\n\nconst split = (\n node: Node,\n offset: number | Node | null,\n stopNode: Node,\n root: Element | DocumentFragment,\n): Node | null => {\n if (node instanceof Text && node !== stopNode) {\n if (typeof offset !== 'number') {\n throw new Error('Offset must be a number to split text node!');\n }\n if (!node.parentNode) {\n throw new Error('Cannot split text node with no parent!');\n }\n return split(node.parentNode, node.splitText(offset), stopNode, root);\n }\n\n let nodeAfterSplit: Node | null =\n typeof offset === 'number'\n ? offset < node.childNodes.length\n ? node.childNodes[offset]\n : null\n : offset;\n const parent = node.parentNode;\n if (!parent || node === stopNode || !(node instanceof Element)) {\n return nodeAfterSplit;\n }\n\n // Clone node without children\n const clone = node.cloneNode(false) as Element;\n\n // Add right-hand siblings to the clone\n while (nodeAfterSplit) {\n const next = nodeAfterSplit.nextSibling;\n clone.appendChild(nodeAfterSplit);\n nodeAfterSplit = next;\n }\n\n // Maintain li numbering if inside a quote.\n if (\n node instanceof HTMLOListElement &&\n getNearest(node, root, 'BLOCKQUOTE')\n ) {\n (clone as HTMLOListElement).start =\n (+node.start || 1) + node.childNodes.length - 1;\n }\n\n // DO NOT NORMALISE. This may undo the fixCursor() call\n // of a node lower down the tree!\n // We need something in the element in order for the cursor to appear.\n fixCursor(node);\n fixCursor(clone);\n\n // Inject clone after original node\n parent.insertBefore(clone, node.nextSibling);\n\n // Keep on splitting up the tree\n return split(parent, clone, stopNode, root);\n};\n\nconst _mergeInlines = (\n node: Node,\n fakeRange: {\n startContainer: Node;\n startOffset: number;\n endContainer: Node;\n endOffset: number;\n },\n): void => {\n const children = node.childNodes;\n let l = children.length;\n const frags: DocumentFragment[] = [];\n while (l--) {\n const child = children[l];\n const prev = l ? children[l - 1] : null;\n if (prev && isInline(child) && areAlike(child, prev)) {\n if (fakeRange.startContainer === child) {\n fakeRange.startContainer = prev;\n fakeRange.startOffset += getLength(prev);\n }\n if (fakeRange.endContainer === child) {\n fakeRange.endContainer = prev;\n fakeRange.endOffset += getLength(prev);\n }\n if (fakeRange.startContainer === node) {\n if (fakeRange.startOffset > l) {\n fakeRange.startOffset -= 1;\n } else if (fakeRange.startOffset === l) {\n fakeRange.startContainer = prev;\n fakeRange.startOffset = getLength(prev);\n }\n }\n if (fakeRange.endContainer === node) {\n if (fakeRange.endOffset > l) {\n fakeRange.endOffset -= 1;\n } else if (fakeRange.endOffset === l) {\n fakeRange.endContainer = prev;\n fakeRange.endOffset = getLength(prev);\n }\n }\n detach(child);\n if (child instanceof Text) {\n (prev as Text).appendData(child.data);\n } else {\n frags.push(empty(child));\n }\n } else if (child instanceof Element) {\n let frag: DocumentFragment | undefined;\n while ((frag = frags.pop())) {\n child.appendChild(frag);\n }\n _mergeInlines(child, fakeRange);\n }\n }\n};\n\nconst mergeInlines = (node: Node, range: Range): void => {\n const element = node instanceof Text ? node.parentNode : node;\n if (element instanceof Element) {\n const fakeRange = {\n startContainer: range.startContainer,\n startOffset: range.startOffset,\n endContainer: range.endContainer,\n endOffset: range.endOffset,\n };\n _mergeInlines(element, fakeRange);\n range.setStart(fakeRange.startContainer, fakeRange.startOffset);\n range.setEnd(fakeRange.endContainer, fakeRange.endOffset);\n }\n};\n\nconst mergeWithBlock = (\n block: Node,\n next: Node,\n range: Range,\n root: Element,\n): void => {\n let container = next;\n let parent: Node | null;\n let offset: number;\n while (\n (parent = container.parentNode) &&\n parent !== root &&\n parent instanceof Element &&\n parent.childNodes.length === 1\n ) {\n container = parent;\n }\n detach(container);\n\n offset = block.childNodes.length;\n\n // Remove extra
fixer if present.\n const last = block.lastChild;\n if (last && last.nodeName === 'BR') {\n block.removeChild(last);\n offset -= 1;\n }\n\n block.appendChild(empty(next));\n\n range.setStart(block, offset);\n range.collapse(true);\n mergeInlines(block, range);\n};\n\nconst mergeContainers = (node: Node, root: Element): void => {\n const prev = node.previousSibling;\n const first = node.firstChild;\n const isListItem = node.nodeName === 'LI';\n\n // Do not merge LIs, unless it only contains a UL\n if (isListItem && (!first || !/^[OU]L$/.test(first.nodeName))) {\n return;\n }\n\n if (prev && areAlike(prev, node)) {\n if (!isContainer(prev)) {\n if (isListItem) {\n const block = createElement('DIV');\n block.appendChild(empty(prev));\n prev.appendChild(block);\n } else {\n return;\n }\n }\n detach(node);\n const needsFix = !isContainer(node);\n prev.appendChild(empty(node));\n if (needsFix) {\n fixContainer(prev, root);\n }\n if (first) {\n mergeContainers(first, root);\n }\n } else if (isListItem) {\n const block = createElement('DIV');\n node.insertBefore(block, first);\n fixCursor(block);\n }\n};\n\n// ---\n\nexport {\n fixContainer,\n fixCursor,\n mergeContainers,\n mergeInlines,\n mergeWithBlock,\n split,\n};\n", "import { notWS } from './Constants';\nimport { TreeIterator, SHOW_ELEMENT_OR_TEXT } from './node/TreeIterator';\nimport { createElement, empty, detach, replaceWith } from './node/Node';\nimport { isInline, isLeaf } from './node/Category';\nimport { fixContainer } from './node/MergeSplit';\nimport { isLineBreak } from './node/Whitespace';\n\nimport type { SquireConfig } from './Editor';\n\n// ---\n\ntype StyleRewriter = (\n node: HTMLElement,\n parent: Node,\n config: SquireConfig,\n) => HTMLElement;\n\n// ---\n\nconst styleToSemantic: Record<\n string,\n { regexp: RegExp; replace: (x: any, y: string) => HTMLElement }\n> = {\n 'font-weight': {\n regexp: /^bold|^700/i,\n replace(): HTMLElement {\n return createElement('B');\n },\n },\n 'font-style': {\n regexp: /^italic/i,\n replace(): HTMLElement {\n return createElement('I');\n },\n },\n 'font-family': {\n regexp: notWS,\n replace(\n classNames: { fontFamily: string },\n family: string,\n ): HTMLElement {\n return createElement('SPAN', {\n class: classNames.fontFamily,\n style: 'font-family:' + family,\n });\n },\n },\n 'font-size': {\n regexp: notWS,\n replace(classNames: { fontSize: string }, size: string): HTMLElement {\n return createElement('SPAN', {\n class: classNames.fontSize,\n style: 'font-size:' + size,\n });\n },\n },\n 'text-decoration': {\n regexp: /^underline/i,\n replace(): HTMLElement {\n return createElement('U');\n },\n },\n};\n\nconst replaceStyles = (\n node: HTMLElement,\n _: Node,\n config: SquireConfig,\n): HTMLElement => {\n const style = node.style;\n let newTreeBottom: HTMLElement | undefined;\n let newTreeTop: HTMLElement | undefined;\n\n for (const attr in styleToSemantic) {\n const converter = styleToSemantic[attr];\n const css = style.getPropertyValue(attr);\n if (css && converter.regexp.test(css)) {\n const el = converter.replace(config.classNames, css);\n if (\n el.nodeName === node.nodeName &&\n el.className === node.className\n ) {\n continue;\n }\n if (!newTreeTop) {\n newTreeTop = el;\n }\n if (newTreeBottom) {\n newTreeBottom.appendChild(el);\n }\n newTreeBottom = el;\n node.style.removeProperty(attr);\n }\n }\n\n if (newTreeTop && newTreeBottom) {\n newTreeBottom.appendChild(empty(node));\n if (node.style.cssText) {\n node.appendChild(newTreeTop);\n } else {\n replaceWith(node, newTreeTop);\n }\n }\n\n return newTreeBottom || node;\n};\n\nconst replaceWithTag = (tag: string) => {\n return (node: HTMLElement, parent: Node) => {\n const el = createElement(tag);\n const attributes = node.attributes;\n for (let i = 0, l = attributes.length; i < l; i += 1) {\n const attribute = attributes[i];\n el.setAttribute(attribute.name, attribute.value);\n }\n parent.replaceChild(el, node);\n el.appendChild(empty(node));\n return el;\n };\n};\n\nconst fontSizes: Record = {\n '1': '10',\n '2': '13',\n '3': '16',\n '4': '18',\n '5': '24',\n '6': '32',\n '7': '48',\n};\n\nconst stylesRewriters: Record = {\n STRONG: replaceWithTag('B'),\n EM: replaceWithTag('I'),\n INS: replaceWithTag('U'),\n STRIKE: replaceWithTag('S'),\n SPAN: replaceStyles,\n FONT: (\n node: HTMLElement,\n parent: Node,\n config: SquireConfig,\n ): HTMLElement => {\n const font = node as HTMLFontElement;\n const face = font.face;\n const size = font.size;\n let color = font.color;\n const classNames = config.classNames;\n let fontSpan: HTMLElement;\n let sizeSpan: HTMLElement;\n let colorSpan: HTMLElement;\n let newTreeBottom: HTMLElement | undefined;\n let newTreeTop: HTMLElement | undefined;\n if (face) {\n fontSpan = createElement('SPAN', {\n class: classNames.fontFamily,\n style: 'font-family:' + face,\n });\n newTreeTop = fontSpan;\n newTreeBottom = fontSpan;\n }\n if (size) {\n sizeSpan = createElement('SPAN', {\n class: classNames.fontSize,\n style: 'font-size:' + fontSizes[size] + 'px',\n });\n if (!newTreeTop) {\n newTreeTop = sizeSpan;\n }\n if (newTreeBottom) {\n newTreeBottom.appendChild(sizeSpan);\n }\n newTreeBottom = sizeSpan;\n }\n if (color && /^#?([\\dA-F]{3}){1,2}$/i.test(color)) {\n if (color.charAt(0) !== '#') {\n color = '#' + color;\n }\n colorSpan = createElement('SPAN', {\n class: classNames.color,\n style: 'color:' + color,\n });\n if (!newTreeTop) {\n newTreeTop = colorSpan;\n }\n if (newTreeBottom) {\n newTreeBottom.appendChild(colorSpan);\n }\n newTreeBottom = colorSpan;\n }\n if (!newTreeTop || !newTreeBottom) {\n newTreeTop = newTreeBottom = createElement('SPAN');\n }\n parent.replaceChild(newTreeTop, font);\n newTreeBottom.appendChild(empty(font));\n return newTreeBottom;\n },\n TT: (node: Node, parent: Node, config: SquireConfig): HTMLElement => {\n const el = createElement('SPAN', {\n class: config.classNames.fontFamily,\n style: 'font-family:menlo,consolas,\"courier new\",monospace',\n });\n parent.replaceChild(el, node);\n el.appendChild(empty(node));\n return el;\n },\n};\n\nconst allowedBlock =\n /^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|IGCAPTION|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|COL(?:GROUP)?|UL)$/;\n\nconst blacklist = /^(?:HEAD|META|STYLE)/;\n\n/*\n Two purposes:\n\n 1. Remove nodes we don't want, such as weird tags, comment nodes\n and whitespace nodes.\n 2. Convert inline tags into our preferred format.\n*/\nconst cleanTree = (\n node: Node,\n config: SquireConfig,\n preserveWS?: boolean,\n): Node => {\n const children = node.childNodes;\n\n let nonInlineParent = node;\n while (isInline(nonInlineParent)) {\n nonInlineParent = nonInlineParent.parentNode!;\n }\n const walker = new TreeIterator(\n nonInlineParent,\n SHOW_ELEMENT_OR_TEXT,\n );\n\n for (let i = 0, l = children.length; i < l; i += 1) {\n let child = children[i];\n const nodeName = child.nodeName;\n const rewriter = stylesRewriters[nodeName];\n if (child instanceof HTMLElement) {\n const childLength = child.childNodes.length;\n if (rewriter) {\n child = rewriter(child, node, config);\n } else if (blacklist.test(nodeName)) {\n node.removeChild(child);\n i -= 1;\n l -= 1;\n continue;\n } else if (!allowedBlock.test(nodeName) && !isInline(child)) {\n i -= 1;\n l += childLength - 1;\n node.replaceChild(empty(child), child);\n continue;\n }\n if (childLength) {\n cleanTree(child, config, preserveWS || nodeName === 'PRE');\n }\n } else {\n if (child instanceof Text) {\n let data = child.data;\n const startsWithWS = !notWS.test(data.charAt(0));\n const endsWithWS = !notWS.test(data.charAt(data.length - 1));\n if (preserveWS || (!startsWithWS && !endsWithWS)) {\n continue;\n }\n // Iterate through the nodes; if we hit some other content\n // before the start of a new block we don't trim\n if (startsWithWS) {\n walker.currentNode = child;\n let sibling;\n while ((sibling = walker.previousPONode())) {\n if (\n sibling.nodeName === 'IMG' ||\n (sibling instanceof Text &&\n notWS.test(sibling.data))\n ) {\n break;\n }\n if (!isInline(sibling)) {\n sibling = null;\n break;\n }\n }\n data = data.replace(/^[ \\t\\r\\n]+/g, sibling ? ' ' : '');\n }\n if (endsWithWS) {\n walker.currentNode = child;\n let sibling;\n while ((sibling = walker.nextNode())) {\n if (\n sibling.nodeName === 'IMG' ||\n (sibling instanceof Text &&\n notWS.test(sibling.data))\n ) {\n break;\n }\n if (!isInline(sibling)) {\n sibling = null;\n break;\n }\n }\n data = data.replace(/[ \\t\\r\\n]+$/g, sibling ? ' ' : '');\n }\n if (data) {\n child.data = data;\n continue;\n }\n }\n node.removeChild(child);\n i -= 1;\n l -= 1;\n }\n }\n return node;\n};\n\n// ---\n\nconst removeEmptyInlines = (node: Node): void => {\n const children = node.childNodes;\n let l = children.length;\n while (l--) {\n const child = children[l];\n if (child instanceof Element && !isLeaf(child)) {\n removeEmptyInlines(child);\n if (isInline(child) && !child.firstChild) {\n node.removeChild(child);\n }\n } else if (child instanceof Text && !child.data) {\n node.removeChild(child);\n }\n }\n};\n\n// ---\n\n//
elements are treated specially, and differently depending on the\n// browser, when in rich text editor mode. When adding HTML from external\n// sources, we must remove them, replacing the ones that actually affect\n// line breaks by wrapping the inline text in a
. Browsers that want
\n// elements at the end of each block will then have them added back in a later\n// fixCursor method call.\nconst cleanupBRs = (\n node: Element | DocumentFragment,\n root: Element,\n keepForBlankLine: boolean,\n): void => {\n const brs: NodeListOf = node.querySelectorAll('BR');\n const brBreaksLine: boolean[] = [];\n let l = brs.length;\n\n // Must calculate whether the
breaks a line first, because if we\n // have two
s next to each other, after the first one is converted\n // to a block split, the second will be at the end of a block and\n // therefore seem to not be a line break. But in its original context it\n // was, so we should also convert it to a block split.\n for (let i = 0; i < l; i += 1) {\n brBreaksLine[i] = isLineBreak(brs[i], keepForBlankLine);\n }\n while (l--) {\n const br = brs[l];\n // Cleanup may have removed it\n const parent = br.parentNode;\n if (!parent) {\n continue;\n }\n // If it doesn't break a line, just remove it; it's not doing\n // anything useful. We'll add it back later if required by the\n // browser. If it breaks a line, wrap the content in div tags\n // and replace the brs.\n if (!brBreaksLine[l]) {\n detach(br);\n } else if (!isInline(parent)) {\n fixContainer(parent, root);\n }\n }\n};\n\n// ---\n\nconst escapeHTML = (text: string): string => {\n return text\n .split('&')\n .join('&')\n .split('<')\n .join('<')\n .split('>')\n .join('>')\n .split('\"')\n .join('"');\n};\n\n// ---\n\nexport { cleanTree, cleanupBRs, isLineBreak, removeEmptyInlines, escapeHTML };\n", "import { TreeIterator, SHOW_ELEMENT } from './TreeIterator';\nimport { isBlock } from './Category';\n\n// ---\n\nconst getBlockWalker = (\n node: Node,\n root: Element | DocumentFragment,\n): TreeIterator => {\n const walker = new TreeIterator(root, SHOW_ELEMENT, isBlock);\n walker.currentNode = node;\n return walker;\n};\n\nconst getPreviousBlock = (\n node: Node,\n root: Element | DocumentFragment,\n): HTMLElement | null => {\n const block = getBlockWalker(node, root).previousNode();\n return block !== root ? block : null;\n};\n\nconst getNextBlock = (\n node: Node,\n root: Element | DocumentFragment,\n): HTMLElement | null => {\n const block = getBlockWalker(node, root).nextNode();\n return block !== root ? block : null;\n};\n\nconst isEmptyBlock = (block: Element): boolean => {\n return !block.textContent && !block.querySelector('IMG');\n};\n\n// ---\n\nexport { getBlockWalker, getPreviousBlock, getNextBlock, isEmptyBlock };\n", "import { isInline, isBlock } from '../node/Category';\nimport { getPreviousBlock, getNextBlock } from '../node/Block';\nimport { getNodeBeforeOffset, getNodeAfterOffset } from '../node/Node';\nimport { ZWS, notWS } from '../Constants';\nimport { isNodeContainedInRange } from './Boundaries';\nimport { TreeIterator, SHOW_ELEMENT_OR_TEXT } from '../node/TreeIterator';\n\n// ---\n\n// Returns the first block at least partially contained by the range,\n// or null if no block is contained by the range.\nconst getStartBlockOfRange = (\n range: Range,\n root: Element | DocumentFragment,\n): HTMLElement | null => {\n const container = range.startContainer;\n let block: HTMLElement | null;\n\n // If inline, get the containing block.\n if (isInline(container)) {\n block = getPreviousBlock(container, root);\n } else if (\n container !== root &&\n container instanceof HTMLElement &&\n isBlock(container)\n ) {\n block = container;\n } else {\n const node = getNodeBeforeOffset(container, range.startOffset);\n block = getNextBlock(node, root);\n }\n // Check the block actually intersects the range\n return block && isNodeContainedInRange(range, block, true) ? block : null;\n};\n\n// Returns the last block at least partially contained by the range,\n// or null if no block is contained by the range.\nconst getEndBlockOfRange = (\n range: Range,\n root: Element | DocumentFragment,\n): HTMLElement | null => {\n const container = range.endContainer;\n let block: HTMLElement | null;\n\n // If inline, get the containing block.\n if (isInline(container)) {\n block = getPreviousBlock(container, root);\n } else if (\n container !== root &&\n container instanceof HTMLElement &&\n isBlock(container)\n ) {\n block = container;\n } else {\n let node = getNodeAfterOffset(container, range.endOffset);\n if (!node || !root.contains(node)) {\n node = root;\n let child: Node | null;\n while ((child = node.lastChild)) {\n node = child;\n }\n }\n block = getPreviousBlock(node, root);\n }\n // Check the block actually intersects the range\n return block && isNodeContainedInRange(range, block, true) ? block : null;\n};\n\nconst isContent = (node: Element | Text): boolean => {\n return node instanceof Text\n ? notWS.test(node.data)\n : node.nodeName === 'IMG';\n};\n\nconst rangeDoesStartAtBlockBoundary = (\n range: Range,\n root: Element,\n): boolean => {\n const startContainer = range.startContainer;\n const startOffset = range.startOffset;\n let nodeAfterCursor: Node | null;\n\n // If in the middle or end of a text node, we're not at the boundary.\n if (startContainer instanceof Text) {\n const text = startContainer.data;\n for (let i = startOffset; i > 0; i -= 1) {\n if (text.charAt(i - 1) !== ZWS) {\n return false;\n }\n }\n nodeAfterCursor = startContainer;\n } else {\n nodeAfterCursor = getNodeAfterOffset(startContainer, startOffset);\n if (nodeAfterCursor && !root.contains(nodeAfterCursor)) {\n nodeAfterCursor = null;\n }\n // The cursor was right at the end of the document\n if (!nodeAfterCursor) {\n nodeAfterCursor = getNodeBeforeOffset(startContainer, startOffset);\n if (nodeAfterCursor instanceof Text && nodeAfterCursor.length) {\n return false;\n }\n }\n }\n\n // Otherwise, look for any previous content in the same block.\n const block = getStartBlockOfRange(range, root);\n if (!block) {\n return false;\n }\n const contentWalker = new TreeIterator(\n block,\n SHOW_ELEMENT_OR_TEXT,\n isContent,\n );\n contentWalker.currentNode = nodeAfterCursor;\n\n return !contentWalker.previousNode();\n};\n\nconst rangeDoesEndAtBlockBoundary = (range: Range, root: Element): boolean => {\n const endContainer = range.endContainer;\n const endOffset = range.endOffset;\n let currentNode: Node;\n\n // If in a text node with content, and not at the end, we're not\n // at the boundary. Ignore ZWS.\n if (endContainer instanceof Text) {\n const text = endContainer.data;\n const length = text.length;\n for (let i = endOffset; i < length; i += 1) {\n if (text.charAt(i) !== ZWS) {\n return false;\n }\n }\n currentNode = endContainer;\n } else {\n currentNode = getNodeBeforeOffset(endContainer, endOffset);\n }\n\n // Otherwise, look for any further content in the same block.\n const block = getEndBlockOfRange(range, root);\n if (!block) {\n return false;\n }\n const contentWalker = new TreeIterator(\n block,\n SHOW_ELEMENT_OR_TEXT,\n isContent,\n );\n contentWalker.currentNode = currentNode;\n return !contentWalker.nextNode();\n};\n\nconst expandRangeToBlockBoundaries = (range: Range, root: Element): void => {\n const start = getStartBlockOfRange(range, root);\n const end = getEndBlockOfRange(range, root);\n let parent: Node;\n\n if (start && end) {\n parent = start.parentNode!;\n range.setStart(parent, Array.from(parent.childNodes).indexOf(start));\n parent = end.parentNode!;\n range.setEnd(parent, Array.from(parent.childNodes).indexOf(end) + 1);\n }\n};\n\n// ---\n\nexport {\n getStartBlockOfRange,\n getEndBlockOfRange,\n rangeDoesStartAtBlockBoundary,\n rangeDoesEndAtBlockBoundary,\n expandRangeToBlockBoundaries,\n};\n", "import { cleanupBRs } from '../Clean';\nimport {\n split,\n fixCursor,\n mergeWithBlock,\n fixContainer,\n mergeContainers,\n} from '../node/MergeSplit';\nimport { detach, getNearest, getLength } from '../node/Node';\nimport { TreeIterator, SHOW_ELEMENT_OR_TEXT } from '../node/TreeIterator';\nimport { isInline, isContainer, isLeaf } from '../node/Category';\nimport { getNextBlock, isEmptyBlock, getPreviousBlock } from '../node/Block';\nimport {\n getStartBlockOfRange,\n getEndBlockOfRange,\n rangeDoesEndAtBlockBoundary,\n rangeDoesStartAtBlockBoundary,\n} from './Block';\nimport {\n moveRangeBoundariesDownTree,\n moveRangeBoundariesUpTree,\n} from './Boundaries';\n\n// ---\n\nfunction createRange(startContainer: Node, startOffset: number): Range;\nfunction createRange(\n startContainer: Node,\n startOffset: number,\n endContainer: Node,\n endOffset: number,\n): Range;\nfunction createRange(\n startContainer: Node,\n startOffset: number,\n endContainer?: Node,\n endOffset?: number,\n): Range {\n const range = document.createRange();\n range.setStart(startContainer, startOffset);\n if (endContainer && typeof endOffset === 'number') {\n range.setEnd(endContainer, endOffset);\n } else {\n range.setEnd(startContainer, startOffset);\n }\n return range;\n}\n\nconst insertNodeInRange = (range: Range, node: Node): void => {\n // Insert at start.\n let { startContainer, startOffset, endContainer, endOffset } = range;\n let children: NodeListOf;\n\n // If part way through a text node, split it.\n if (startContainer instanceof Text) {\n const parent = startContainer.parentNode!;\n children = parent.childNodes;\n if (startOffset === startContainer.length) {\n startOffset = Array.from(children).indexOf(startContainer) + 1;\n if (range.collapsed) {\n endContainer = parent;\n endOffset = startOffset;\n }\n } else {\n if (startOffset) {\n const afterSplit = startContainer.splitText(startOffset);\n if (endContainer === startContainer) {\n endOffset -= startOffset;\n endContainer = afterSplit;\n } else if (endContainer === parent) {\n endOffset += 1;\n }\n startContainer = afterSplit;\n }\n startOffset = Array.from(children).indexOf(\n startContainer as ChildNode,\n );\n }\n startContainer = parent;\n } else {\n children = startContainer.childNodes;\n }\n\n const childCount = children.length;\n\n if (startOffset === childCount) {\n startContainer.appendChild(node);\n } else {\n startContainer.insertBefore(node, children[startOffset]);\n }\n\n if (startContainer === endContainer) {\n endOffset += children.length - childCount;\n }\n\n range.setStart(startContainer, startOffset);\n range.setEnd(endContainer, endOffset);\n};\n\n/**\n * Removes the contents of the range and returns it as a DocumentFragment.\n * The range at the end will be at the same position, with the edges just\n * before/after the split. If the start/end have the same parents, it will\n * be collapsed.\n */\nconst extractContentsOfRange = (\n range: Range,\n common: Node | null,\n root: Element,\n): DocumentFragment => {\n const frag = document.createDocumentFragment();\n if (range.collapsed) {\n return frag;\n }\n\n if (!common) {\n common = range.commonAncestorContainer;\n }\n if (common instanceof Text) {\n common = common.parentNode!;\n }\n\n const startContainer = range.startContainer;\n const startOffset = range.startOffset;\n\n let endContainer = split(range.endContainer, range.endOffset, common, root);\n let endOffset = 0;\n\n let node = split(startContainer, startOffset, common, root);\n while (node && node !== endContainer) {\n const next = node.nextSibling;\n frag.appendChild(node);\n node = next;\n }\n\n // Merge text nodes if adjacent\n if (startContainer instanceof Text && endContainer instanceof Text) {\n startContainer.appendData(endContainer.data);\n detach(endContainer);\n endContainer = startContainer;\n endOffset = startOffset;\n }\n\n range.setStart(startContainer, startOffset);\n if (endContainer) {\n range.setEnd(endContainer, endOffset);\n } else {\n // endContainer will be null if at end of parent's child nodes list.\n range.setEnd(common, common.childNodes.length);\n }\n\n fixCursor(common);\n\n return frag;\n};\n\n/**\n * Returns the next/prev node that's part of the same inline content.\n */\nconst getAdjacentInlineNode = (\n iterator: TreeIterator,\n method: 'nextNode' | 'previousPONode',\n node: Node,\n): Node | null => {\n iterator.currentNode = node;\n let nextNode: Node | null;\n while ((nextNode = iterator[method]())) {\n if (nextNode instanceof Text || isLeaf(nextNode)) {\n return nextNode;\n }\n if (!isInline(nextNode)) {\n return null;\n }\n }\n return null;\n};\n\nconst deleteContentsOfRange = (\n range: Range,\n root: Element,\n): DocumentFragment => {\n const startBlock = getStartBlockOfRange(range, root);\n let endBlock = getEndBlockOfRange(range, root);\n const needsMerge = startBlock !== endBlock;\n\n // Move boundaries up as much as possible without exiting block,\n // to reduce need to split.\n if (startBlock && endBlock) {\n moveRangeBoundariesDownTree(range);\n moveRangeBoundariesUpTree(range, startBlock, endBlock, root);\n }\n\n // Remove selected range\n const frag = extractContentsOfRange(range, null, root);\n\n // Move boundaries back down tree as far as possible.\n moveRangeBoundariesDownTree(range);\n\n // If we split into two different blocks, merge the blocks.\n if (needsMerge) {\n // endBlock will have been split, so need to refetch\n endBlock = getEndBlockOfRange(range, root);\n if (startBlock && endBlock && startBlock !== endBlock) {\n mergeWithBlock(startBlock, endBlock, range, root);\n }\n }\n\n // Ensure block has necessary children\n if (startBlock) {\n fixCursor(startBlock);\n }\n\n // Ensure root has a block-level element in it.\n const child = root.firstChild;\n if (!child || child.nodeName === 'BR') {\n fixCursor(root);\n if (root.firstChild) {\n range.selectNodeContents(root.firstChild);\n }\n }\n\n range.collapse(true);\n\n // Now we may need to swap a space for a nbsp if the browser is going\n // to swallow it due to HTML whitespace rules:\n const startContainer = range.startContainer;\n const startOffset = range.startOffset;\n const iterator = new TreeIterator(root, SHOW_ELEMENT_OR_TEXT);\n\n // Find the character after cursor point\n let afterNode: Node | null = startContainer;\n let afterOffset = startOffset;\n if (!(afterNode instanceof Text) || afterOffset === afterNode.data.length) {\n afterNode = getAdjacentInlineNode(iterator, 'nextNode', afterNode);\n afterOffset = 0;\n }\n\n // Find the character before cursor point\n let beforeNode: Node | null = startContainer;\n let beforeOffset = startOffset - 1;\n if (!(beforeNode instanceof Text) || beforeOffset === -1) {\n beforeNode = getAdjacentInlineNode(\n iterator,\n 'previousPONode',\n afterNode ||\n (startContainer instanceof Text\n ? startContainer\n : startContainer.childNodes[startOffset] || startContainer),\n );\n if (beforeNode instanceof Text) {\n beforeOffset = beforeNode.data.length;\n }\n }\n\n // If range starts at block boundary and character after cursor point\n // is a space, replace with nbsp\n let node = null;\n let offset = 0;\n if (\n afterNode instanceof Text &&\n afterNode.data.charAt(afterOffset) === ' ' &&\n rangeDoesStartAtBlockBoundary(range, root)\n ) {\n node = afterNode;\n offset = afterOffset;\n } else if (\n beforeNode instanceof Text &&\n beforeNode.data.charAt(beforeOffset) === ' '\n ) {\n // If character before cursor point is a space, replace with nbsp\n // if either:\n // a) There is a space after it; or\n // b) The point after is the end of the block\n if (\n (afterNode instanceof Text &&\n afterNode.data.charAt(afterOffset) === ' ') ||\n rangeDoesEndAtBlockBoundary(range, root)\n ) {\n node = beforeNode;\n offset = beforeOffset;\n }\n }\n if (node) {\n node.replaceData(offset, 1, '\u00A0'); // nbsp\n }\n // Range needs to be put back in place\n range.setStart(startContainer, startOffset);\n range.collapse(true);\n\n return frag;\n};\n\n// Contents of range will be deleted.\n// After method, range will be around inserted content\nconst insertTreeFragmentIntoRange = (\n range: Range,\n frag: DocumentFragment,\n root: Element,\n): void => {\n const firstInFragIsInline = frag.firstChild && isInline(frag.firstChild);\n let node: Node | null;\n\n // Fixup content: ensure no top-level inline, and add cursor fix elements.\n fixContainer(frag, root);\n node = frag;\n while ((node = getNextBlock(node, root))) {\n fixCursor(node);\n }\n\n // Delete any selected content.\n if (!range.collapsed) {\n deleteContentsOfRange(range, root);\n }\n\n // Move range down into text nodes.\n moveRangeBoundariesDownTree(range);\n range.collapse(false); // collapse to end\n\n // Where will we split up to? First blockquote parent, otherwise root.\n const stopPoint =\n getNearest(range.endContainer, root, 'BLOCKQUOTE') || root;\n\n // Merge the contents of the first block in the frag with the focused block.\n // If there are contents in the block after the focus point, collect this\n // up to insert in the last block later. This preserves the style that was\n // present in this bit of the page.\n //\n // If the block being inserted into is empty though, replace it instead of\n // merging if the fragment had block contents.\n // e.g.

Foo

\n // This seems a reasonable approximation of user intent.\n let block = getStartBlockOfRange(range, root);\n let blockContentsAfterSplit: DocumentFragment | null = null;\n const firstBlockInFrag = getNextBlock(frag, frag);\n const replaceBlock = !firstInFragIsInline && !!block && isEmptyBlock(block);\n if (\n block &&\n firstBlockInFrag &&\n !replaceBlock &&\n // Don't merge table cells or PRE elements into block\n !getNearest(firstBlockInFrag, frag, 'PRE') &&\n !getNearest(firstBlockInFrag, frag, 'TABLE')\n ) {\n moveRangeBoundariesUpTree(range, block, block, root);\n range.collapse(true); // collapse to start\n let container = range.endContainer;\n let offset = range.endOffset;\n // Remove trailing
\u2013 we don't want this considered content to be\n // inserted again later\n cleanupBRs(block as HTMLElement, root, false);\n if (isInline(container)) {\n // Split up to block parent.\n const nodeAfterSplit = split(\n container,\n offset,\n getPreviousBlock(container, root) || root,\n root,\n ) as Node;\n container = nodeAfterSplit.parentNode!;\n offset = Array.from(container.childNodes).indexOf(\n nodeAfterSplit as ChildNode,\n );\n }\n if (/*isBlock( container ) && */ offset !== getLength(container)) {\n // Collect any inline contents of the block after the range point\n blockContentsAfterSplit = document.createDocumentFragment();\n while ((node = container.childNodes[offset])) {\n blockContentsAfterSplit.appendChild(node);\n }\n }\n // And merge the first block in.\n mergeWithBlock(container, firstBlockInFrag, range, root);\n\n // And where we will insert\n offset =\n Array.from(container.parentNode!.childNodes).indexOf(\n container as ChildNode,\n ) + 1;\n container = container.parentNode!;\n range.setEnd(container, offset);\n }\n\n // Is there still any content in the fragment?\n if (getLength(frag)) {\n if (replaceBlock && block) {\n range.setEndBefore(block);\n range.collapse(false);\n detach(block);\n }\n moveRangeBoundariesUpTree(range, stopPoint, stopPoint, root);\n // Now split after block up to blockquote (if a parent) or root\n let nodeAfterSplit = split(\n range.endContainer,\n range.endOffset,\n stopPoint,\n root,\n ) as Node | null;\n const nodeBeforeSplit = nodeAfterSplit\n ? nodeAfterSplit.previousSibling\n : stopPoint.lastChild;\n stopPoint.insertBefore(frag, nodeAfterSplit);\n if (nodeAfterSplit) {\n range.setEndBefore(nodeAfterSplit);\n } else {\n range.setEnd(stopPoint, getLength(stopPoint));\n }\n block = getEndBlockOfRange(range, root);\n\n // Get a reference that won't be invalidated if we merge containers.\n moveRangeBoundariesDownTree(range);\n const container = range.endContainer;\n const offset = range.endOffset;\n\n // Merge inserted containers with edges of split\n if (nodeAfterSplit && isContainer(nodeAfterSplit)) {\n mergeContainers(nodeAfterSplit, root);\n }\n nodeAfterSplit = nodeBeforeSplit && nodeBeforeSplit.nextSibling;\n if (nodeAfterSplit && isContainer(nodeAfterSplit)) {\n mergeContainers(nodeAfterSplit, root);\n }\n range.setEnd(container, offset);\n }\n\n // Insert inline content saved from before.\n if (blockContentsAfterSplit && block) {\n const tempRange = range.cloneRange();\n fixCursor(blockContentsAfterSplit);\n mergeWithBlock(block, blockContentsAfterSplit, tempRange, root);\n range.setEnd(tempRange.endContainer, tempRange.endOffset);\n }\n moveRangeBoundariesDownTree(range);\n};\n\n// ---\n\nexport {\n createRange,\n deleteContentsOfRange,\n extractContentsOfRange,\n insertNodeInRange,\n insertTreeFragmentIntoRange,\n};\n", "import { SHOW_ELEMENT_OR_TEXT, TreeIterator } from '../node/TreeIterator';\nimport { isNodeContainedInRange } from './Boundaries';\nimport { isInline } from '../node/Category';\n\n// ---\n\nconst getTextContentsOfRange = (range: Range) => {\n if (range.collapsed) {\n return '';\n }\n const startContainer = range.startContainer;\n const endContainer = range.endContainer;\n const walker = new TreeIterator(\n range.commonAncestorContainer,\n SHOW_ELEMENT_OR_TEXT,\n (node) => {\n return isNodeContainedInRange(range, node, true);\n },\n );\n walker.currentNode = startContainer;\n\n let node: Node | null = startContainer;\n let textContent = '';\n let addedTextInBlock = false;\n let value: string;\n\n if (\n (!(node instanceof Element) && !(node instanceof Text)) ||\n !walker.filter(node)\n ) {\n node = walker.nextNode();\n }\n\n while (node) {\n if (node instanceof Text) {\n value = node.data;\n if (value && /\\S/.test(value)) {\n if (node === endContainer) {\n value = value.slice(0, range.endOffset);\n }\n if (node === startContainer) {\n value = value.slice(range.startOffset);\n }\n textContent += value;\n addedTextInBlock = true;\n }\n } else if (\n node.nodeName === 'BR' ||\n (addedTextInBlock && !isInline(node))\n ) {\n textContent += '\\n';\n addedTextInBlock = false;\n }\n node = walker.nextNode();\n }\n // Replace nbsp with regular space;\n // eslint-disable-next-line no-irregular-whitespace\n textContent = textContent.replace(/\u00A0/g, ' ');\n\n return textContent;\n};\n\n// ---\n\nexport { getTextContentsOfRange };\n", "import { isWin, isGecko, isLegacyEdge, notWS } from './Constants';\nimport { createElement, detach } from './node/Node';\nimport { getStartBlockOfRange, getEndBlockOfRange } from './range/Block';\nimport { createRange, deleteContentsOfRange } from './range/InsertDelete';\nimport {\n moveRangeBoundariesDownTree,\n moveRangeBoundariesUpTree,\n} from './range/Boundaries';\n\nimport type { Squire } from './Editor';\nimport { getTextContentsOfRange } from './range/Contents';\n\n// ---\n\nconst indexOf = Array.prototype.indexOf;\n\nconst extractRangeToClipboard = (\n event: ClipboardEvent,\n range: Range,\n root: HTMLElement,\n removeRangeFromDocument: boolean,\n toCleanHTML: null | ((html: string) => string),\n toPlainText: null | ((html: string) => string),\n plainTextOnly: boolean,\n): boolean => {\n // Edge only seems to support setting plain text as of 2016-03-11.\n const clipboardData = event.clipboardData;\n if (isLegacyEdge || !clipboardData) {\n return false;\n }\n // First get the plain text version from the range (unless we have a custom\n // HTML -> Text conversion fn)\n let text = toPlainText ? '' : getTextContentsOfRange(range);\n\n // Clipboard content should include all parents within block, or all\n // parents up to root if selection across blocks\n const startBlock = getStartBlockOfRange(range, root);\n const endBlock = getEndBlockOfRange(range, root);\n let copyRoot = root;\n\n // If the content is not in well-formed blocks, the start and end block\n // may be the same, but actually the range goes outside it. Must check!\n if (\n startBlock === endBlock &&\n startBlock?.contains(range.commonAncestorContainer)\n ) {\n copyRoot = startBlock;\n }\n\n // Extract the contents\n let contents: Node;\n if (removeRangeFromDocument) {\n contents = deleteContentsOfRange(range, root);\n } else {\n // Clone range to mutate, then move up as high as possible without\n // passing the copy root node.\n range = range.cloneRange();\n moveRangeBoundariesDownTree(range);\n moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root);\n contents = range.cloneContents();\n }\n\n // Add any other parents not in extracted content, up to copy root\n let parent = range.commonAncestorContainer;\n if (parent instanceof Text) {\n parent = parent.parentNode!;\n }\n while (parent && parent !== copyRoot) {\n const newContents = parent.cloneNode(false);\n newContents.appendChild(contents);\n contents = newContents;\n parent = parent.parentNode!;\n }\n\n // Get HTML version of data\n let html: string | undefined;\n if (\n contents.childNodes.length === 1 &&\n contents.childNodes[0] instanceof Text\n ) {\n // Replace nbsp with regular space;\n // eslint-disable-next-line no-irregular-whitespace\n text = contents.childNodes[0].data.replace(/\u00A0/g, ' ');\n plainTextOnly = true;\n } else {\n const node = createElement('DIV') as HTMLDivElement;\n node.appendChild(contents);\n html = node.innerHTML;\n if (toCleanHTML) {\n html = toCleanHTML(html);\n }\n }\n\n // Get Text version of data if converting from HTML\n if (toPlainText && html !== undefined) {\n text = toPlainText(html);\n }\n\n // Firefox (and others?) returns unix line endings (\\n) even on Windows.\n // If on Windows, normalise to \\r\\n, since Notepad and some other crappy\n // apps do not understand just \\n.\n if (isWin) {\n text = text.replace(/\\r?\\n/g, '\\r\\n');\n }\n\n // Set clipboard data\n if (!plainTextOnly && html && text !== html) {\n clipboardData.setData('text/html', html);\n }\n clipboardData.setData('text/plain', text);\n event.preventDefault();\n\n return true;\n};\n\n// ---\n\nconst _onCut = function (this: Squire, event: ClipboardEvent): void {\n const range: Range = this.getSelection();\n const root: HTMLElement = this._root;\n\n // Nothing to do\n if (range.collapsed) {\n event.preventDefault();\n return;\n }\n\n // Save undo checkpoint\n this.saveUndoState(range);\n\n const handled = extractRangeToClipboard(\n event,\n range,\n root,\n true,\n this._config.willCutCopy,\n this._config.toPlainText,\n false,\n );\n if (!handled) {\n setTimeout(() => {\n try {\n // If all content removed, ensure div at start of root.\n this._ensureBottomLine();\n } catch (error) {\n this._config.didError(error);\n }\n }, 0);\n }\n\n this.setSelection(range);\n};\n\nconst _onCopy = function (this: Squire, event: ClipboardEvent): void {\n extractRangeToClipboard(\n event,\n this.getSelection(),\n this._root,\n false,\n this._config.willCutCopy,\n this._config.toPlainText,\n false,\n );\n};\n\n// Need to monitor for shift key like this, as event.shiftKey is not available\n// in paste event.\nconst _monitorShiftKey = function (this: Squire, event: KeyboardEvent): void {\n this._isShiftDown = event.shiftKey;\n};\n\nconst _onPaste = function (this: Squire, event: ClipboardEvent): void {\n const clipboardData = event.clipboardData;\n const items = clipboardData?.items;\n const choosePlain: boolean | undefined = this._isShiftDown;\n let hasRTF = false;\n let hasImage = false;\n let plainItem: null | DataTransferItem = null;\n let htmlItem: null | DataTransferItem = null;\n\n // Current HTML5 Clipboard interface\n // ---------------------------------\n // https://html.spec.whatwg.org/multipage/interaction.html\n if (items) {\n let l = items.length;\n while (l--) {\n const item = items[l];\n const type = item.type;\n if (type === 'text/html') {\n htmlItem = item;\n // iOS copy URL gives you type text/uri-list which is just a list\n // of 1 or more URLs separated by new lines. Can just treat as\n // plain text.\n } else if (type === 'text/plain' || type === 'text/uri-list') {\n plainItem = item;\n } else if (type === 'text/rtf') {\n hasRTF = true;\n } else if (/^image\\/.*/.test(type)) {\n hasImage = true;\n }\n }\n\n // Treat image paste as a drop of an image file. When you copy\n // an image in Chrome/Firefox (at least), it copies the image data\n // but also an HTML version (referencing the original URL of the image)\n // and a plain text version.\n //\n // However, when you copy in Excel, you get html, rtf, text, image;\n // in this instance you want the html version! So let's try using\n // the presence of text/rtf as an indicator to choose the html version\n // over the image.\n if (hasImage && !(hasRTF && htmlItem)) {\n event.preventDefault();\n this.fireEvent('pasteImage', {\n clipboardData,\n });\n return;\n }\n\n // Edge only provides access to plain text as of 2016-03-11 and gives no\n // indication there should be an HTML part. However, it does support\n // access to image data, so we check for that first. Otherwise though,\n // fall through to fallback clipboard handling methods\n if (!isLegacyEdge) {\n event.preventDefault();\n if (htmlItem && (!choosePlain || !plainItem)) {\n htmlItem.getAsString((html) => {\n this.insertHTML(html, true);\n });\n } else if (plainItem) {\n plainItem.getAsString((text) => {\n // If we have a selection and text is solely a URL,\n // just make the text a link.\n let isLink = false;\n const range = this.getSelection();\n if (!range.collapsed && notWS.test(range.toString())) {\n const match = this.linkRegExp.exec(text);\n isLink = !!match && match[0].length === text.length;\n }\n if (isLink) {\n this.makeLink(text);\n } else {\n this.insertPlainText(text, true);\n }\n });\n }\n return;\n }\n }\n\n // Old interface\n // -------------\n\n // Safari (and indeed many other OS X apps) copies stuff as text/rtf\n // rather than text/html; even from a webpage in Safari. The only way\n // to get an HTML version is to fallback to letting the browser insert\n // the content. Same for getting image data. *Sigh*.\n //\n // Firefox is even worse: it doesn't even let you know that there might be\n // an RTF version on the clipboard, but it will also convert to HTML if you\n // let the browser insert the content. I've filed\n // https://bugzilla.mozilla.org/show_bug.cgi?id=1254028\n const types = clipboardData?.types;\n if (\n !isLegacyEdge &&\n types &&\n (indexOf.call(types, 'text/html') > -1 ||\n (!isGecko &&\n indexOf.call(types, 'text/plain') > -1 &&\n indexOf.call(types, 'text/rtf') < 0))\n ) {\n event.preventDefault();\n // Abiword on Linux copies a plain text and html version, but the HTML\n // version is the empty string! So always try to get HTML, but if none,\n // insert plain text instead. On iOS, Facebook (and possibly other\n // apps?) copy links as type text/uri-list, but also insert a **blank**\n // text/plain item onto the clipboard. Why? Who knows.\n let data;\n if (!choosePlain && (data = clipboardData.getData('text/html'))) {\n this.insertHTML(data, true);\n } else if (\n (data = clipboardData.getData('text/plain')) ||\n (data = clipboardData.getData('text/uri-list'))\n ) {\n this.insertPlainText(data, true);\n }\n return;\n }\n\n // No interface. Includes all versions of IE :(\n // --------------------------------------------\n\n const body = document.body;\n const range = this.getSelection();\n const startContainer = range.startContainer;\n const startOffset = range.startOffset;\n const endContainer = range.endContainer;\n const endOffset = range.endOffset;\n\n // We need to position the pasteArea in the visible portion of the screen\n // to stop the browser auto-scrolling.\n let pasteArea: Element = createElement('DIV', {\n contenteditable: 'true',\n style: 'position:fixed; overflow:hidden; top:0; right:100%; width:1px; height:1px;',\n });\n body.appendChild(pasteArea);\n range.selectNodeContents(pasteArea);\n this.setSelection(range);\n\n // A setTimeout of 0 means this is added to the back of the\n // single javascript thread, so it will be executed after the\n // paste event.\n setTimeout(() => {\n try {\n // Get the pasted content and clean\n let html = '';\n let next: Element = pasteArea;\n let first: Node | null;\n\n // #88: Chrome can apparently split the paste area if certain\n // content is inserted; gather them all up.\n while ((pasteArea = next)) {\n next = pasteArea.nextSibling as Element;\n detach(pasteArea);\n // Safari and IE like putting extra divs around things.\n first = pasteArea.firstChild;\n if (\n first &&\n first === pasteArea.lastChild &&\n first instanceof HTMLDivElement\n ) {\n pasteArea = first;\n }\n html += pasteArea.innerHTML;\n }\n\n this.setSelection(\n createRange(\n startContainer,\n startOffset,\n endContainer,\n endOffset,\n ),\n );\n\n if (html) {\n this.insertHTML(html, true);\n }\n } catch (error) {\n this._config.didError(error);\n }\n }, 0);\n};\n\n// On Windows you can drag an drop text. We can't handle this ourselves, because\n// as far as I can see, there's no way to get the drop insertion point. So just\n// save an undo state and hope for the best.\nconst _onDrop = function (this: Squire, event: DragEvent): void {\n // it's possible for dataTransfer to be null, let's avoid it.\n if (!event.dataTransfer) {\n return;\n }\n const types = event.dataTransfer.types;\n let l = types.length;\n let hasPlain = false;\n let hasHTML = false;\n while (l--) {\n switch (types[l]) {\n case 'text/plain':\n hasPlain = true;\n break;\n case 'text/html':\n hasHTML = true;\n break;\n default:\n return;\n }\n }\n if (hasHTML || (hasPlain && this.saveUndoState)) {\n this.saveUndoState();\n }\n};\n\n// ---\n\nexport {\n extractRangeToClipboard,\n _onCut,\n _onCopy,\n _monitorShiftKey,\n _onPaste,\n _onDrop,\n};\n", "import type { Squire } from '../Editor';\n\n// ---\n\nconst Enter = (self: Squire, event: KeyboardEvent, range: Range): void => {\n event.preventDefault();\n self.splitBlock(event.shiftKey, range);\n};\n\n// ---\n\nexport { Enter };\n", "import { ZWS } from '../Constants';\nimport { getPreviousBlock } from '../node/Block';\nimport { isInline, isBlock } from '../node/Category';\nimport { fixCursor } from '../node/MergeSplit';\nimport { createElement, detach, getNearest } from '../node/Node';\nimport { moveRangeBoundariesDownTree } from '../range/Boundaries';\n\nimport type { Squire } from '../Editor';\n\n// ---\n\n// If you delete the content inside a span with a font styling, Webkit will\n// replace it with a tag (!). If you delete all the text inside a\n// link in Opera, it won't delete the link. Let's make things consistent. If\n// you delete all text inside an inline tag, remove the inline tag.\nconst afterDelete = (self: Squire, range?: Range): void => {\n try {\n if (!range) {\n range = self.getSelection();\n }\n let node = range!.startContainer;\n // Climb the tree from the focus point while we are inside an empty\n // inline element\n if (node instanceof Text) {\n node = node.parentNode!;\n }\n let parent = node;\n while (\n isInline(parent) &&\n (!parent.textContent || parent.textContent === ZWS)\n ) {\n node = parent;\n parent = node.parentNode!;\n }\n // If focused in empty inline element\n if (node !== parent) {\n // Move focus to just before empty inline(s)\n range!.setStart(\n parent,\n Array.from(parent.childNodes as NodeListOf).indexOf(node),\n );\n range!.collapse(true);\n // Remove empty inline(s)\n parent.removeChild(node);\n // Fix cursor in block\n if (!isBlock(parent)) {\n parent = getPreviousBlock(parent, self._root) || self._root;\n }\n fixCursor(parent);\n // Move cursor into text node\n moveRangeBoundariesDownTree(range!);\n }\n // If you delete the last character in the sole
in Chrome,\n // it removes the div and replaces it with just a
inside the\n // root. Detach the
; the _ensureBottomLine call will insert a new\n // block.\n if (\n node === self._root &&\n (node = node.firstChild!) &&\n node.nodeName === 'BR'\n ) {\n detach(node);\n }\n self._ensureBottomLine();\n self.setSelection(range);\n self._updatePath(range, true);\n } catch (error) {\n self._config.didError(error);\n }\n};\n\nconst detachUneditableNode = (node: Node, root: Element): void => {\n let parent: Node | null;\n while ((parent = node.parentNode)) {\n if (parent === root || (parent as HTMLElement).isContentEditable) {\n break;\n }\n node = parent;\n }\n detach(node);\n};\n\n// ---\n\nconst linkifyText = (self: Squire, textNode: Text, offset: number): void => {\n if (getNearest(textNode, self._root, 'A')) {\n return;\n }\n const data = textNode.data || '';\n const searchFrom =\n Math.max(\n data.lastIndexOf(' ', offset - 1),\n data.lastIndexOf('\u00A0', offset - 1),\n ) + 1;\n const searchText = data.slice(searchFrom, offset);\n const match = self.linkRegExp.exec(searchText);\n if (match) {\n // Record an undo point\n const selection = self.getSelection();\n self._docWasChanged();\n self._recordUndoState(selection);\n self._getRangeAndRemoveBookmark(selection);\n\n const index = searchFrom + match.index;\n const endIndex = index + match[0].length;\n const needsSelectionUpdate = selection.startContainer === textNode;\n const newSelectionOffset = selection.startOffset - endIndex;\n if (index) {\n textNode = textNode.splitText(index);\n }\n\n const defaultAttributes = self._config.tagAttributes.a;\n const link = createElement(\n 'A',\n Object.assign(\n {\n href: match[1]\n ? /^(?:ht|f)tps?:/i.test(match[1])\n ? match[1]\n : 'http://' + match[1]\n : 'mailto:' + match[0],\n },\n defaultAttributes,\n ),\n );\n link.textContent = data.slice(index, endIndex);\n textNode.parentNode!.insertBefore(link, textNode);\n textNode.data = data.slice(endIndex);\n\n if (needsSelectionUpdate) {\n selection.setStart(textNode, newSelectionOffset);\n selection.setEnd(textNode, newSelectionOffset);\n }\n self.setSelection(selection);\n }\n};\n\n// ---\n\nexport { afterDelete, detachUneditableNode, linkifyText };\n", "import type { Squire } from '../Editor';\nimport { getPreviousBlock } from '../node/Block';\nimport {\n fixContainer,\n mergeContainers,\n mergeWithBlock,\n} from '../node/MergeSplit';\nimport { getNearest } from '../node/Node';\nimport {\n getStartBlockOfRange,\n rangeDoesStartAtBlockBoundary,\n} from '../range/Block';\nimport { moveRangeBoundariesDownTree } from '../range/Boundaries';\nimport { deleteContentsOfRange } from '../range/InsertDelete';\nimport { afterDelete, detachUneditableNode } from './KeyHelpers';\n\n// ---\n\nconst Backspace = (self: Squire, event: KeyboardEvent, range: Range): void => {\n const root: Element = self._root;\n self._removeZWS();\n // Record undo checkpoint.\n self.saveUndoState(range);\n if (!range.collapsed) {\n // If not collapsed, delete contents\n event.preventDefault();\n deleteContentsOfRange(range, root);\n afterDelete(self, range);\n } else if (rangeDoesStartAtBlockBoundary(range, root)) {\n // If at beginning of block, merge with previous\n event.preventDefault();\n const startBlock = getStartBlockOfRange(range, root);\n if (!startBlock) {\n return;\n }\n let current = startBlock;\n // In case inline data has somehow got between blocks.\n fixContainer(current.parentNode!, root);\n // Now get previous block\n const previous = getPreviousBlock(current, root);\n // Must not be at the very beginning of the text area.\n if (previous) {\n // If not editable, just delete whole block.\n if (!(previous as HTMLElement).isContentEditable) {\n detachUneditableNode(previous, root);\n return;\n }\n // Otherwise merge.\n mergeWithBlock(previous, current, range, root);\n // If deleted line between containers, merge newly adjacent\n // containers.\n current = previous.parentNode as HTMLElement;\n while (current !== root && !current.nextSibling) {\n current = current.parentNode as HTMLElement;\n }\n if (\n current !== root &&\n (current = current.nextSibling as HTMLElement)\n ) {\n mergeContainers(current, root);\n }\n self.setSelection(range);\n // If at very beginning of text area, allow backspace\n // to break lists/blockquote.\n } else if (current) {\n if (\n getNearest(current, root, 'UL') ||\n getNearest(current, root, 'OL')\n ) {\n // Break list\n self.decreaseListLevel(range);\n return;\n } else if (getNearest(current, root, 'BLOCKQUOTE')) {\n // Break blockquote\n self.removeQuote(range);\n return;\n }\n self.setSelection(range);\n self._updatePath(range, true);\n }\n } else {\n // If deleting text inside a link that looks like a URL, delink.\n // This is to allow you to easily correct auto-linked text.\n moveRangeBoundariesDownTree(range);\n const text = range.startContainer;\n const offset = range.startOffset;\n const a = text.parentNode;\n if (\n text instanceof Text &&\n a instanceof HTMLAnchorElement &&\n offset &&\n a.href.includes(text.data)\n ) {\n text.deleteData(offset - 1, 1);\n self.setSelection(range);\n self.removeLink();\n event.preventDefault();\n } else {\n // Otherwise, leave to browser but check afterwards whether it has\n // left behind an empty inline tag.\n self.setSelection(range);\n setTimeout(() => {\n afterDelete(self);\n }, 0);\n }\n }\n};\n\n// ---\n\nexport { Backspace };\n", "import { getNextBlock } from '../node/Block';\nimport {\n fixContainer,\n mergeWithBlock,\n mergeContainers,\n} from '../node/MergeSplit';\nimport { detach } from '../node/Node';\nimport {\n rangeDoesEndAtBlockBoundary,\n getStartBlockOfRange,\n} from '../range/Block';\nimport {\n moveRangeBoundariesUpTree,\n moveRangeBoundariesDownTree,\n} from '../range/Boundaries';\nimport { deleteContentsOfRange } from '../range/InsertDelete';\nimport { afterDelete, detachUneditableNode } from './KeyHelpers';\n\nimport type { Squire } from '../Editor';\n\n// ---\n\nconst Delete = (self: Squire, event: KeyboardEvent, range: Range): void => {\n const root = self._root;\n let current: Node | null;\n let next: Node | null;\n let originalRange: Range;\n let cursorContainer: Node;\n let cursorOffset: number;\n let nodeAfterCursor: Node;\n self._removeZWS();\n // Record undo checkpoint.\n self.saveUndoState(range);\n // If not collapsed, delete contents\n if (!range.collapsed) {\n event.preventDefault();\n deleteContentsOfRange(range, root);\n afterDelete(self, range);\n // If at end of block, merge next into this block\n } else if (rangeDoesEndAtBlockBoundary(range, root)) {\n event.preventDefault();\n current = getStartBlockOfRange(range, root);\n if (!current) {\n return;\n }\n // In case inline data has somehow got between blocks.\n fixContainer(current.parentNode!, root);\n // Now get next block\n next = getNextBlock(current, root);\n // Must not be at the very end of the text area.\n if (next) {\n // If not editable, just delete whole block.\n if (!(next as HTMLElement).isContentEditable) {\n detachUneditableNode(next, root);\n return;\n }\n // Otherwise merge.\n mergeWithBlock(current, next, range, root);\n // If deleted line between containers, merge newly adjacent\n // containers.\n next = current.parentNode!;\n while (next !== root && !next.nextSibling) {\n next = next.parentNode!;\n }\n if (next !== root && (next = next.nextSibling)) {\n mergeContainers(next, root);\n }\n self.setSelection(range);\n self._updatePath(range, true);\n }\n // Otherwise, leave to browser but check afterwards whether it has\n // left behind an empty inline tag.\n } else {\n // But first check if the cursor is just before an IMG tag. If so,\n // delete it ourselves, because the browser won't if it is not\n // inline.\n originalRange = range.cloneRange();\n moveRangeBoundariesUpTree(range, root, root, root);\n cursorContainer = range.endContainer;\n cursorOffset = range.endOffset;\n if (cursorContainer instanceof Element) {\n nodeAfterCursor = cursorContainer.childNodes[cursorOffset];\n if (nodeAfterCursor && nodeAfterCursor.nodeName === 'IMG') {\n event.preventDefault();\n detach(nodeAfterCursor);\n moveRangeBoundariesDownTree(range);\n afterDelete(self, range);\n return;\n }\n }\n self.setSelection(originalRange);\n setTimeout(() => {\n afterDelete(self);\n }, 0);\n }\n};\n\n// ---\n\nexport { Delete };\n", "import {\n rangeDoesStartAtBlockBoundary,\n getStartBlockOfRange,\n} from '../range/Block';\nimport { getNearest } from '../node/Node';\n\nimport type { Squire } from '../Editor';\n\n// ---\n\nconst Tab = (self: Squire, event: KeyboardEvent, range: Range): void => {\n const root = self._root;\n self._removeZWS();\n // If no selection and at start of block\n if (range.collapsed && rangeDoesStartAtBlockBoundary(range, root)) {\n let node: Node = getStartBlockOfRange(range, root)!;\n // Iterate through the block's parents\n let parent: Node | null;\n while ((parent = node.parentNode)) {\n // If we find a UL or OL (so are in a list, node must be an LI)\n if (parent.nodeName === 'UL' || parent.nodeName === 'OL') {\n // Then increase the list level\n event.preventDefault();\n self.increaseListLevel(range);\n break;\n }\n node = parent;\n }\n }\n};\n\nconst ShiftTab = (self: Squire, event: KeyboardEvent, range: Range): void => {\n const root = self._root;\n self._removeZWS();\n // If no selection and at start of block\n if (range.collapsed && rangeDoesStartAtBlockBoundary(range, root)) {\n // Break list\n const node = range.startContainer;\n if (getNearest(node, root, 'UL') || getNearest(node, root, 'OL')) {\n event.preventDefault();\n self.decreaseListLevel(range);\n }\n }\n};\n\n// ---\n\nexport { Tab, ShiftTab };\n", "import { detach, getLength } from '../node/Node';\nimport { moveRangeBoundariesDownTree } from '../range/Boundaries';\nimport { deleteContentsOfRange } from '../range/InsertDelete';\n\nimport type { Squire } from '../Editor';\nimport { linkifyText } from './KeyHelpers';\nimport {\n getStartBlockOfRange,\n rangeDoesEndAtBlockBoundary,\n} from '../range/Block';\nimport { SHOW_TEXT, TreeIterator } from '../node/TreeIterator';\nimport { ZWS } from '../Constants';\n\n// ---\n\nconst Space = (self: Squire, event: KeyboardEvent, range: Range): void => {\n let node: Node | null;\n const root = self._root;\n self._recordUndoState(range);\n self._getRangeAndRemoveBookmark(range);\n\n // Delete the selection if not collapsed\n if (!range.collapsed) {\n deleteContentsOfRange(range, root);\n self._ensureBottomLine();\n self.setSelection(range);\n self._updatePath(range, true);\n } else if (rangeDoesEndAtBlockBoundary(range, root)) {\n const block = getStartBlockOfRange(range, root);\n if (block && block.nodeName !== 'PRE') {\n const text = block.textContent?.trimEnd().replace(ZWS, '');\n if (text === '*' || text === '1.') {\n event.preventDefault();\n self.insertPlainText(' ', false);\n self._docWasChanged();\n self.saveUndoState(range);\n const walker = new TreeIterator(block, SHOW_TEXT);\n let textNode: Text | null;\n while ((textNode = walker.nextNode())) {\n detach(textNode);\n }\n if (text === '*') {\n self.makeUnorderedList();\n } else {\n self.makeOrderedList();\n }\n return;\n }\n }\n }\n\n // If the cursor is at the end of a link (foo|) then move it\n // outside of the link (foo|) so that the space is not part of\n // the link text.\n node = range.endContainer;\n if (range.endOffset === getLength(node)) {\n do {\n if (node.nodeName === 'A') {\n range.setStartAfter(node);\n break;\n }\n } while (\n !node.nextSibling &&\n (node = node.parentNode) &&\n node !== root\n );\n }\n\n // Linkify text\n if (self._config.addLinks) {\n const linkRange = range.cloneRange();\n moveRangeBoundariesDownTree(linkRange);\n const textNode = linkRange.startContainer as Text;\n const offset = linkRange.startOffset;\n setTimeout(() => {\n linkifyText(self, textNode, offset);\n }, 0);\n }\n\n self.setSelection(range);\n};\n\n// ---\n\nexport { Space };\n", "import {\n isMac,\n isWin,\n isIOS,\n ctrlKey,\n supportsInputEvents,\n} from '../Constants';\nimport { deleteContentsOfRange } from '../range/InsertDelete';\nimport type { Squire } from '../Editor';\nimport { Enter } from './Enter';\nimport { Backspace } from './Backspace';\nimport { Delete } from './Delete';\nimport { ShiftTab, Tab } from './Tab';\nimport { Space } from './Space';\nimport { rangeDoesEndAtBlockBoundary } from '../range/Block';\nimport { moveRangeBoundariesDownTree } from '../range/Boundaries';\n\n// ---\n\nconst _onKey = function (this: Squire, event: KeyboardEvent): void {\n // Ignore key events where event.isComposing, to stop us from blatting\n // Kana-Kanji conversion\n if (event.defaultPrevented || event.isComposing) {\n return;\n }\n\n // We need to apply the Backspace/delete handlers regardless of\n // control key modifiers.\n let key = event.key;\n let modifiers = '';\n if (key !== 'Backspace' && key !== 'Delete') {\n if (event.altKey) {\n modifiers += 'Alt-';\n }\n if (event.ctrlKey) {\n modifiers += 'Ctrl-';\n }\n if (event.metaKey) {\n modifiers += 'Meta-';\n }\n if (event.shiftKey) {\n modifiers += 'Shift-';\n }\n }\n // However, on Windows, Shift-Delete is apparently \"cut\" (WTF right?), so\n // we want to let the browser handle Shift-Delete in this situation.\n if (isWin && event.shiftKey && key === 'Delete') {\n modifiers += 'Shift-';\n }\n key = modifiers + key;\n\n const range: Range = this.getSelection();\n if (this._keyHandlers[key]) {\n this._keyHandlers[key](this, event, range);\n } else if (\n !range.collapsed &&\n !event.ctrlKey &&\n !event.metaKey &&\n key.length === 1\n ) {\n // Record undo checkpoint.\n this.saveUndoState(range);\n // Delete the selection\n deleteContentsOfRange(range, this._root);\n this._ensureBottomLine();\n this.setSelection(range);\n this._updatePath(range, true);\n }\n};\n\n// ---\n\ntype KeyHandler = (self: Squire, event: KeyboardEvent, range: Range) => void;\n\nconst keyHandlers: Record = {\n 'Backspace': Backspace,\n 'Delete': Delete,\n 'Tab': Tab,\n 'Shift-Tab': ShiftTab,\n ' ': Space,\n 'ArrowLeft'(self: Squire): void {\n self._removeZWS();\n },\n 'ArrowRight'(self: Squire, event: KeyboardEvent, range: Range): void {\n self._removeZWS();\n // Allow right arrow to always break out of block.\n const root = self.getRoot();\n if (rangeDoesEndAtBlockBoundary(range, root)) {\n moveRangeBoundariesDownTree(range);\n let node: Node | null = range.endContainer;\n do {\n if (node.nodeName === 'CODE') {\n let next = node.nextSibling;\n if (!(next instanceof Text)) {\n const textNode = document.createTextNode('\u00A0'); // nbsp\n node.parentNode!.insertBefore(textNode, next);\n next = textNode;\n }\n range.setStart(next, 1);\n self.setSelection(range);\n event.preventDefault();\n break;\n }\n } while (\n !node.nextSibling &&\n (node = node.parentNode) &&\n node !== root\n );\n }\n },\n};\n\nif (!supportsInputEvents) {\n keyHandlers.Enter = Enter;\n keyHandlers['Shift-Enter'] = Enter;\n}\n\n// System standard for page up/down on Mac/iOS is to just scroll, not move the\n// cursor. On Linux/Windows, it should move the cursor, but some browsers don't\n// implement this natively. Override to support it.\nif (!isMac && !isIOS) {\n keyHandlers.PageUp = (self: Squire) => {\n self.moveCursorToStart();\n };\n keyHandlers.PageDown = (self: Squire) => {\n self.moveCursorToEnd();\n };\n}\n\n// ---\n\nconst mapKeyToFormat = (\n tag: string,\n remove?: { tag: string } | null,\n): KeyHandler => {\n remove = remove || null;\n return (self: Squire, event: Event) => {\n event.preventDefault();\n const range = self.getSelection();\n if (self.hasFormat(tag, null, range)) {\n self.changeFormat(null, { tag }, range);\n } else {\n self.changeFormat({ tag }, remove, range);\n }\n };\n};\n\nkeyHandlers[ctrlKey + 'b'] = mapKeyToFormat('B');\nkeyHandlers[ctrlKey + 'i'] = mapKeyToFormat('I');\nkeyHandlers[ctrlKey + 'u'] = mapKeyToFormat('U');\nkeyHandlers[ctrlKey + 'Shift-7'] = mapKeyToFormat('S');\nkeyHandlers[ctrlKey + 'Shift-5'] = mapKeyToFormat('SUB', { tag: 'SUP' });\nkeyHandlers[ctrlKey + 'Shift-6'] = mapKeyToFormat('SUP', { tag: 'SUB' });\n\nkeyHandlers[ctrlKey + 'Shift-8'] = (\n self: Squire,\n event: KeyboardEvent,\n): void => {\n event.preventDefault();\n const path = self.getPath();\n if (!/(?:^|>)UL/.test(path)) {\n self.makeUnorderedList();\n } else {\n self.removeList();\n }\n};\nkeyHandlers[ctrlKey + 'Shift-9'] = (\n self: Squire,\n event: KeyboardEvent,\n): void => {\n event.preventDefault();\n const path = self.getPath();\n if (!/(?:^|>)OL/.test(path)) {\n self.makeOrderedList();\n } else {\n self.removeList();\n }\n};\n\nkeyHandlers[ctrlKey + '['] = (self: Squire, event: KeyboardEvent): void => {\n event.preventDefault();\n const path = self.getPath();\n if (/(?:^|>)BLOCKQUOTE/.test(path) || !/(?:^|>)[OU]L/.test(path)) {\n self.decreaseQuoteLevel();\n } else {\n self.decreaseListLevel();\n }\n};\nkeyHandlers[ctrlKey + ']'] = (self: Squire, event: KeyboardEvent): void => {\n event.preventDefault();\n const path = self.getPath();\n if (/(?:^|>)BLOCKQUOTE/.test(path) || !/(?:^|>)[OU]L/.test(path)) {\n self.increaseQuoteLevel();\n } else {\n self.increaseListLevel();\n }\n};\n\nkeyHandlers[ctrlKey + 'd'] = (self: Squire, event: KeyboardEvent): void => {\n event.preventDefault();\n self.toggleCode();\n};\n\nkeyHandlers[ctrlKey + 'z'] = (self: Squire, event: KeyboardEvent): void => {\n event.preventDefault();\n self.undo();\n};\nkeyHandlers[ctrlKey + 'y'] = keyHandlers[ctrlKey + 'Shift-z'] = (\n self: Squire,\n event: KeyboardEvent,\n): void => {\n event.preventDefault();\n self.redo();\n};\n\nexport { _onKey, keyHandlers };\n", "import {\n TreeIterator,\n SHOW_TEXT,\n SHOW_ELEMENT_OR_TEXT,\n} from './node/TreeIterator';\nimport {\n createElement,\n detach,\n empty,\n getNearest,\n hasTagAttributes,\n replaceWith,\n} from './node/Node';\nimport {\n isLeaf,\n isInline,\n resetNodeCategoryCache,\n isContainer,\n isBlock,\n} from './node/Category';\nimport { isLineBreak, removeZWS } from './node/Whitespace';\nimport {\n moveRangeBoundariesDownTree,\n isNodeContainedInRange,\n moveRangeBoundaryOutOf,\n moveRangeBoundariesUpTree,\n} from './range/Boundaries';\nimport {\n createRange,\n deleteContentsOfRange,\n extractContentsOfRange,\n insertNodeInRange,\n insertTreeFragmentIntoRange,\n} from './range/InsertDelete';\nimport {\n fixContainer,\n fixCursor,\n mergeContainers,\n mergeInlines,\n split,\n} from './node/MergeSplit';\nimport { getBlockWalker, getNextBlock, isEmptyBlock } from './node/Block';\nimport { cleanTree, cleanupBRs, escapeHTML, removeEmptyInlines } from './Clean';\nimport { cantFocusEmptyTextNodes, isAndroid, ZWS } from './Constants';\nimport {\n expandRangeToBlockBoundaries,\n getEndBlockOfRange,\n getStartBlockOfRange,\n rangeDoesEndAtBlockBoundary,\n rangeDoesStartAtBlockBoundary,\n} from './range/Block';\nimport {\n _monitorShiftKey,\n _onCopy,\n _onCut,\n _onDrop,\n _onPaste,\n} from './Clipboard';\nimport { keyHandlers, _onKey } from './keyboard/KeyHandlers';\nimport { linkifyText } from './keyboard/KeyHelpers';\nimport { getTextContentsOfRange } from './range/Contents';\n\ndeclare const DOMPurify: any;\n\n// ---\n\ntype EventHandler = { handleEvent: (e: Event) => void } | ((e: Event) => void);\n\ntype KeyHandlerFunction = (x: Squire, y: KeyboardEvent, z: Range) => void;\n\ntype TagAttributes = {\n [key: string]: { [key: string]: string };\n};\n\ninterface SquireConfig {\n blockTag: string;\n blockAttributes: null | Record;\n tagAttributes: TagAttributes;\n classNames: {\n color: string;\n fontFamily: string;\n fontSize: string;\n highlight: string;\n };\n undo: {\n documentSizeThreshold: number;\n undoLimit: number;\n };\n addLinks: boolean;\n willCutCopy: null | ((html: string) => string);\n toPlainText: null | ((html: string) => string);\n sanitizeToDOMFragment: (html: string, editor: Squire) => DocumentFragment;\n didError: (x: any) => void;\n}\n\n// ---\n\nclass Squire {\n _root: HTMLElement;\n _config: SquireConfig;\n\n _isFocused: boolean;\n _lastSelection: Range;\n _willRestoreSelection: boolean;\n _mayHaveZWS: boolean;\n\n _lastAnchorNode: Node | null;\n _lastFocusNode: Node | null;\n _path: string;\n\n _events: Map>;\n\n _undoIndex: number;\n _undoStack: Array;\n _undoStackLength: number;\n _isInUndoState: boolean;\n _ignoreChange: boolean;\n _ignoreAllChanges: boolean;\n\n _isShiftDown: boolean;\n _keyHandlers: Record;\n\n _mutation: MutationObserver;\n\n constructor(root: HTMLElement, config?: Partial) {\n this._root = root;\n\n this._config = this._makeConfig(config);\n\n this._isFocused = false;\n this._lastSelection = createRange(root, 0);\n this._willRestoreSelection = false;\n this._mayHaveZWS = false;\n\n this._lastAnchorNode = null;\n this._lastFocusNode = null;\n this._path = '';\n\n this._events = new Map();\n\n this._undoIndex = -1;\n this._undoStack = [];\n this._undoStackLength = 0;\n this._isInUndoState = false;\n this._ignoreChange = false;\n this._ignoreAllChanges = false;\n\n // Add event listeners\n this.addEventListener('selectionchange', this._updatePathOnEvent);\n\n // On blur, restore focus except if the user taps or clicks to focus a\n // specific point. Can't actually use click event because focus happens\n // before click, so use mousedown/touchstart\n this.addEventListener('blur', this._enableRestoreSelection);\n this.addEventListener('mousedown', this._disableRestoreSelection);\n this.addEventListener('touchstart', this._disableRestoreSelection);\n this.addEventListener('focus', this._restoreSelection);\n\n // Clipboard support\n this._isShiftDown = false;\n this.addEventListener('cut', _onCut as (e: Event) => void);\n this.addEventListener('copy', _onCopy as (e: Event) => void);\n this.addEventListener('paste', _onPaste as (e: Event) => void);\n this.addEventListener('drop', _onDrop as (e: Event) => void);\n this.addEventListener(\n 'keydown',\n _monitorShiftKey as (e: Event) => void,\n );\n this.addEventListener('keyup', _monitorShiftKey as (e: Event) => void);\n\n // Keyboard support\n this.addEventListener('keydown', _onKey as (e: Event) => void);\n this._keyHandlers = Object.create(keyHandlers);\n\n const mutation = new MutationObserver(() => this._docWasChanged());\n mutation.observe(root, {\n childList: true,\n attributes: true,\n characterData: true,\n subtree: true,\n });\n this._mutation = mutation;\n\n // Make it editable\n root.setAttribute('contenteditable', 'true');\n\n // Modern browsers let you override their default content editable\n // handling!\n this.addEventListener(\n 'beforeinput',\n this._beforeInput as (e: Event) => void,\n );\n\n this.setHTML('');\n }\n\n destroy(): void {\n this._events.forEach((_, type) => {\n this.removeEventListener(type);\n });\n\n this._mutation.disconnect();\n\n this._undoIndex = -1;\n this._undoStack = [];\n this._undoStackLength = 0;\n }\n\n _makeConfig(userConfig?: object): SquireConfig {\n const config = {\n blockTag: 'DIV',\n blockAttributes: null,\n tagAttributes: {},\n classNames: {\n color: 'color',\n fontFamily: 'font',\n fontSize: 'size',\n highlight: 'highlight',\n },\n undo: {\n documentSizeThreshold: -1, // -1 means no threshold\n undoLimit: -1, // -1 means no limit\n },\n addLinks: true,\n willCutCopy: null,\n toPlainText: null,\n sanitizeToDOMFragment: (\n html: string,\n /* editor: Squire, */\n ): DocumentFragment => {\n const frag = DOMPurify.sanitize(html, {\n ALLOW_UNKNOWN_PROTOCOLS: true,\n WHOLE_DOCUMENT: false,\n RETURN_DOM: true,\n RETURN_DOM_FRAGMENT: true,\n FORCE_BODY: false,\n });\n return frag\n ? document.importNode(frag, true)\n : document.createDocumentFragment();\n },\n didError: (error: any): void => console.log(error),\n };\n if (userConfig) {\n Object.assign(config, userConfig);\n config.blockTag = config.blockTag.toUpperCase();\n }\n\n return config;\n }\n\n setKeyHandler(key: string, fn: KeyHandlerFunction) {\n this._keyHandlers[key] = fn;\n return this;\n }\n\n _beforeInput(event: InputEvent): void {\n switch (event.inputType) {\n case 'insertText':\n // Generally we let the browser handle text insertion, as it\n // does so fine. However, the Samsung keyboard on Android with\n // the Grammarly extension goes batshit crazy for some reason\n // and will try to disastrously rewrite the whole data, without\n // the user even doing anything (it can happen on first load\n // before the user types anything). Fortunately we can detect\n // this by looking for a new line in the data and if we see it,\n // stop it by preventing default.\n // 30-11-2023 Update: The fix for Grammarly bug prevents pasting\n // text directly from the keyboard on Android if the text to be\n // inserted contains \\n, as pasting from the keyboard does not\n // fire a true paste event. The Grammarly bug seems to have been\n // fixed in Samsung keyboard as of v5.6.10.4, but leaving the\n // fix in place for now, as the bug is particularly destructive.\n if (isAndroid && event.data && event.data.includes('\\n')) {\n event.preventDefault();\n }\n break;\n case 'insertLineBreak':\n event.preventDefault();\n this.splitBlock(true);\n break;\n case 'insertParagraph':\n event.preventDefault();\n this.splitBlock(false);\n break;\n case 'insertOrderedList':\n event.preventDefault();\n this.makeOrderedList();\n break;\n case 'insertUnoderedList':\n event.preventDefault();\n this.makeUnorderedList();\n break;\n case 'historyUndo':\n event.preventDefault();\n this.undo();\n break;\n case 'historyRedo':\n event.preventDefault();\n this.redo();\n break;\n case 'formatBold':\n event.preventDefault();\n this.bold();\n break;\n case 'formaItalic':\n event.preventDefault();\n this.italic();\n break;\n case 'formatUnderline':\n event.preventDefault();\n this.underline();\n break;\n case 'formatStrikeThrough':\n event.preventDefault();\n this.strikethrough();\n break;\n case 'formatSuperscript':\n event.preventDefault();\n this.superscript();\n break;\n case 'formatSubscript':\n event.preventDefault();\n this.subscript();\n break;\n case 'formatJustifyFull':\n case 'formatJustifyCenter':\n case 'formatJustifyRight':\n case 'formatJustifyLeft': {\n event.preventDefault();\n let alignment = event.inputType.slice(13).toLowerCase();\n if (alignment === 'full') {\n alignment = 'justify';\n }\n this.setTextAlignment(alignment);\n break;\n }\n case 'formatRemove':\n event.preventDefault();\n this.removeAllFormatting();\n break;\n case 'formatSetBlockTextDirection': {\n event.preventDefault();\n let dir = event.data;\n if (dir === 'null') {\n dir = null;\n }\n this.setTextDirection(dir);\n break;\n }\n case 'formatBackColor':\n event.preventDefault();\n this.setHighlightColor(event.data);\n break;\n case 'formatFontColor':\n event.preventDefault();\n this.setTextColor(event.data);\n break;\n case 'formatFontName':\n event.preventDefault();\n this.setFontFace(event.data);\n break;\n }\n }\n\n // --- Events\n\n handleEvent(event: Event): void {\n this.fireEvent(event.type, event);\n }\n\n fireEvent(type: string, detail?: Event | object): Squire {\n let handlers = this._events.get(type);\n // UI code, especially modal views, may be monitoring for focus events\n // and immediately removing focus. In certain conditions, this can\n // cause the focus event to fire after the blur event, which can cause\n // an infinite loop. So we detect whether we're actually\n // focused/blurred before firing.\n if (/^(?:focus|blur)/.test(type)) {\n const isFocused = this._root === document.activeElement;\n if (type === 'focus') {\n if (!isFocused || this._isFocused) {\n return this;\n }\n this._isFocused = true;\n } else {\n if (isFocused || !this._isFocused) {\n return this;\n }\n this._isFocused = false;\n }\n }\n if (handlers) {\n const event: Event =\n detail instanceof Event\n ? detail\n : new CustomEvent(type, {\n detail,\n });\n // Clone handlers array, so any handlers added/removed do not\n // affect it.\n handlers = handlers.slice();\n for (const handler of handlers) {\n try {\n if ('handleEvent' in handler) {\n handler.handleEvent(event);\n } else {\n handler.call(this, event);\n }\n } catch (error) {\n this._config.didError(error);\n }\n }\n }\n return this;\n }\n\n /**\n * Subscribing to these events won't automatically add a listener to the\n * document node, since these events are fired in a custom manner by the\n * editor code.\n */\n customEvents = new Set([\n 'pathChange',\n 'select',\n 'input',\n 'pasteImage',\n 'undoStateChange',\n ]);\n\n addEventListener(type: string, fn: EventHandler): Squire {\n let handlers = this._events.get(type);\n let target: Document | HTMLElement = this._root;\n if (!handlers) {\n handlers = [];\n this._events.set(type, handlers);\n if (!this.customEvents.has(type)) {\n if (type === 'selectionchange') {\n target = document;\n }\n target.addEventListener(type, this, true);\n }\n }\n handlers.push(fn);\n return this;\n }\n\n removeEventListener(type: string, fn?: EventHandler): Squire {\n const handlers = this._events.get(type);\n let target: Document | HTMLElement = this._root;\n if (handlers) {\n if (fn) {\n let l = handlers.length;\n while (l--) {\n if (handlers[l] === fn) {\n handlers.splice(l, 1);\n }\n }\n } else {\n handlers.length = 0;\n }\n if (!handlers.length) {\n this._events.delete(type);\n if (!this.customEvents.has(type)) {\n if (type === 'selectionchange') {\n target = document;\n }\n target.removeEventListener(type, this, true);\n }\n }\n }\n return this;\n }\n\n // --- Focus\n\n focus(): Squire {\n this._root.focus({ preventScroll: true });\n return this;\n }\n\n blur(): Squire {\n this._root.blur();\n return this;\n }\n\n // --- Selection and bookmarking\n\n _enableRestoreSelection(): void {\n this._willRestoreSelection = true;\n }\n\n _disableRestoreSelection(): void {\n this._willRestoreSelection = false;\n }\n\n _restoreSelection() {\n if (this._willRestoreSelection) {\n this.setSelection(this._lastSelection);\n }\n }\n\n // ---\n\n _removeZWS(): void {\n if (!this._mayHaveZWS) {\n return;\n }\n removeZWS(this._root);\n this._mayHaveZWS = false;\n }\n\n // ---\n\n startSelectionId = 'squire-selection-start';\n endSelectionId = 'squire-selection-end';\n\n _saveRangeToBookmark(range: Range): void {\n let startNode = createElement('INPUT', {\n id: this.startSelectionId,\n type: 'hidden',\n });\n let endNode = createElement('INPUT', {\n id: this.endSelectionId,\n type: 'hidden',\n });\n let temp: HTMLElement;\n\n insertNodeInRange(range, startNode);\n range.collapse(false);\n insertNodeInRange(range, endNode);\n\n // In a collapsed range, the start is sometimes inserted after the end!\n if (\n startNode.compareDocumentPosition(endNode) &\n Node.DOCUMENT_POSITION_PRECEDING\n ) {\n startNode.id = this.endSelectionId;\n endNode.id = this.startSelectionId;\n temp = startNode;\n startNode = endNode;\n endNode = temp;\n }\n\n range.setStartAfter(startNode);\n range.setEndBefore(endNode);\n }\n\n _getRangeAndRemoveBookmark(range?: Range): Range | null {\n const root = this._root;\n const start = root.querySelector('#' + this.startSelectionId);\n const end = root.querySelector('#' + this.endSelectionId);\n\n if (start && end) {\n let startContainer: Node = start.parentNode!;\n let endContainer: Node = end.parentNode!;\n const startOffset = Array.from(startContainer.childNodes).indexOf(\n start,\n );\n let endOffset = Array.from(endContainer.childNodes).indexOf(end);\n\n if (startContainer === endContainer) {\n endOffset -= 1;\n }\n\n start.remove();\n end.remove();\n\n if (!range) {\n range = document.createRange();\n }\n range.setStart(startContainer, startOffset);\n range.setEnd(endContainer, endOffset);\n\n // Merge any text nodes we split\n mergeInlines(startContainer, range);\n if (startContainer !== endContainer) {\n mergeInlines(endContainer, range);\n }\n\n // If we didn't split a text node, we should move into any adjacent\n // text node to current selection point\n if (range.collapsed) {\n startContainer = range.startContainer;\n if (startContainer instanceof Text) {\n endContainer = startContainer.childNodes[range.startOffset];\n if (!endContainer || !(endContainer instanceof Text)) {\n endContainer =\n startContainer.childNodes[range.startOffset - 1];\n }\n if (endContainer && endContainer instanceof Text) {\n range.setStart(endContainer, 0);\n range.collapse(true);\n }\n }\n }\n }\n return range || null;\n }\n\n getSelection(): Range {\n const selection = window.getSelection();\n const root = this._root;\n let range: Range | null = null;\n // If not focused, always rely on cached selection; another function may\n // have set it but the DOM is not modified until focus again\n if (this._isFocused && selection && selection.rangeCount) {\n range = selection.getRangeAt(0).cloneRange();\n const startContainer = range.startContainer;\n const endContainer = range.endContainer;\n // FF can return the selection as being inside an . WTF?\n if (startContainer && isLeaf(startContainer)) {\n range.setStartBefore(startContainer);\n }\n if (endContainer && isLeaf(endContainer)) {\n range.setEndBefore(endContainer);\n }\n }\n if (range && root.contains(range.commonAncestorContainer)) {\n this._lastSelection = range;\n } else {\n range = this._lastSelection;\n // Check the editor is in the live document; if not, the range has\n // probably been rewritten by the browser and is bogus\n if (!document.contains(range.commonAncestorContainer)) {\n range = null;\n }\n }\n if (!range) {\n range = createRange(root.firstElementChild || root, 0);\n }\n return range;\n }\n\n setSelection(range: Range): Squire {\n this._lastSelection = range;\n // If we're setting selection, that automatically, and synchronously,\n // triggers a focus event. So just store the selection and mark it as\n // needing restore on focus.\n if (!this._isFocused) {\n this._enableRestoreSelection();\n } else {\n const selection = window.getSelection();\n if (selection) {\n if ('setBaseAndExtent' in Selection.prototype) {\n selection.setBaseAndExtent(\n range.startContainer,\n range.startOffset,\n range.endContainer,\n range.endOffset,\n );\n } else {\n selection.removeAllRanges();\n selection.addRange(range);\n }\n }\n }\n return this;\n }\n\n // ---\n\n _moveCursorTo(toStart: boolean): Squire {\n const root = this._root;\n const range = createRange(root, toStart ? 0 : root.childNodes.length);\n moveRangeBoundariesDownTree(range);\n this.setSelection(range);\n return this;\n }\n\n moveCursorToStart(): Squire {\n return this._moveCursorTo(true);\n }\n\n moveCursorToEnd(): Squire {\n return this._moveCursorTo(false);\n }\n\n // ---\n\n getCursorPosition(): DOMRect {\n const range = this.getSelection();\n let rect = range.getBoundingClientRect();\n // If the range is outside of the viewport, some browsers at least\n // will return 0 for all the values; need to get a DOM node to find\n // the position instead.\n if (rect && !rect.top) {\n this._ignoreChange = true;\n const node = createElement('SPAN');\n node.textContent = ZWS;\n insertNodeInRange(range, node);\n rect = node.getBoundingClientRect();\n const parent = node.parentNode!;\n parent.removeChild(node);\n mergeInlines(parent, range);\n }\n return rect;\n }\n\n // --- Path\n\n getPath(): string {\n return this._path;\n }\n\n _updatePathOnEvent(): void {\n if (this._isFocused) {\n this._updatePath(this.getSelection());\n }\n }\n\n _updatePath(range: Range, force?: boolean): void {\n const anchor = range.startContainer;\n const focus = range.endContainer;\n let newPath: string;\n if (\n force ||\n anchor !== this._lastAnchorNode ||\n focus !== this._lastFocusNode\n ) {\n this._lastAnchorNode = anchor;\n this._lastFocusNode = focus;\n newPath =\n anchor && focus\n ? anchor === focus\n ? this._getPath(focus)\n : '(selection)'\n : '';\n if (this._path !== newPath) {\n this._path = newPath;\n this.fireEvent('pathChange', {\n path: newPath,\n });\n }\n }\n this.fireEvent(range.collapsed ? 'cursor' : 'select', {\n range: range,\n });\n }\n\n _getPath(node: Node) {\n const root = this._root;\n const config = this._config;\n let path = '';\n if (node && node !== root) {\n const parent = node.parentNode;\n path = parent ? this._getPath(parent) : '';\n if (node instanceof HTMLElement) {\n const id = node.id;\n const classList = node.classList;\n const classNames = Array.from(classList).sort();\n const dir = node.dir;\n const styleNames = config.classNames;\n path += (path ? '>' : '') + node.nodeName;\n if (id) {\n path += '#' + id;\n }\n if (classNames.length) {\n path += '.';\n path += classNames.join('.');\n }\n if (dir) {\n path += '[dir=' + dir + ']';\n }\n if (classList.contains(styleNames.highlight)) {\n path +=\n '[backgroundColor=' +\n node.style.backgroundColor.replace(/ /g, '') +\n ']';\n }\n if (classList.contains(styleNames.color)) {\n path +=\n '[color=' + node.style.color.replace(/ /g, '') + ']';\n }\n if (classList.contains(styleNames.fontFamily)) {\n path +=\n '[fontFamily=' +\n node.style.fontFamily.replace(/ /g, '') +\n ']';\n }\n if (classList.contains(styleNames.fontSize)) {\n path += '[fontSize=' + node.style.fontSize + ']';\n }\n }\n }\n return path;\n }\n\n // --- History\n\n modifyDocument(modificationFn: () => void): Squire {\n const mutation = this._mutation;\n if (mutation) {\n if (mutation.takeRecords().length) {\n this._docWasChanged();\n }\n mutation.disconnect();\n }\n\n this._ignoreAllChanges = true;\n modificationFn();\n this._ignoreAllChanges = false;\n\n if (mutation) {\n mutation.observe(this._root, {\n childList: true,\n attributes: true,\n characterData: true,\n subtree: true,\n });\n this._ignoreChange = false;\n }\n\n return this;\n }\n\n _docWasChanged(): void {\n resetNodeCategoryCache();\n this._mayHaveZWS = true;\n if (this._ignoreAllChanges) {\n return;\n }\n\n if (this._ignoreChange) {\n this._ignoreChange = false;\n return;\n }\n if (this._isInUndoState) {\n this._isInUndoState = false;\n this.fireEvent('undoStateChange', {\n canUndo: true,\n canRedo: false,\n });\n }\n this.fireEvent('input');\n }\n\n /**\n * Leaves bookmark.\n */\n _recordUndoState(range: Range, replace?: boolean): Squire {\n const isInUndoState = this._isInUndoState;\n if (!isInUndoState || replace) {\n // Advance pointer to new position\n let undoIndex = this._undoIndex + 1;\n const undoStack = this._undoStack;\n const undoConfig = this._config.undo;\n const undoThreshold = undoConfig.documentSizeThreshold;\n const undoLimit = undoConfig.undoLimit;\n\n // Truncate stack if longer (i.e. if has been previously undone)\n if (undoIndex < this._undoStackLength) {\n undoStack.length = this._undoStackLength = undoIndex;\n }\n\n // Add bookmark\n if (range) {\n this._saveRangeToBookmark(range);\n }\n\n // Don't record if we're already in an undo state\n if (isInUndoState) {\n return this;\n }\n\n // Get data\n const html = this._getRawHTML();\n\n // If this document is above the configured size threshold,\n // limit the number of saved undo states.\n // Threshold is in bytes, JS uses 2 bytes per character\n if (replace) {\n undoIndex -= 1;\n }\n if (undoThreshold > -1 && html.length * 2 > undoThreshold) {\n if (undoLimit > -1 && undoIndex > undoLimit) {\n undoStack.splice(0, undoIndex - undoLimit);\n undoIndex = undoLimit;\n this._undoStackLength = undoLimit;\n }\n }\n\n // Save data\n undoStack[undoIndex] = html;\n this._undoIndex = undoIndex;\n this._undoStackLength += 1;\n this._isInUndoState = true;\n }\n return this;\n }\n\n saveUndoState(range?: Range): Squire {\n if (!range) {\n range = this.getSelection();\n }\n this._recordUndoState(range, this._isInUndoState);\n this._getRangeAndRemoveBookmark(range);\n\n return this;\n }\n\n undo(): Squire {\n // Sanity check: must not be at beginning of the history stack\n if (this._undoIndex !== 0 || !this._isInUndoState) {\n // Make sure any changes since last checkpoint are saved.\n this._recordUndoState(this.getSelection(), false);\n this._undoIndex -= 1;\n this._setRawHTML(this._undoStack[this._undoIndex]);\n const range = this._getRangeAndRemoveBookmark();\n if (range) {\n this.setSelection(range);\n }\n this._isInUndoState = true;\n this.fireEvent('undoStateChange', {\n canUndo: this._undoIndex !== 0,\n canRedo: true,\n });\n this.fireEvent('input');\n }\n return this.focus();\n }\n\n redo(): Squire {\n // Sanity check: must not be at end of stack and must be in an undo\n // state.\n const undoIndex = this._undoIndex;\n const undoStackLength = this._undoStackLength;\n if (undoIndex + 1 < undoStackLength && this._isInUndoState) {\n this._undoIndex += 1;\n this._setRawHTML(this._undoStack[this._undoIndex]);\n const range = this._getRangeAndRemoveBookmark();\n if (range) {\n this.setSelection(range);\n }\n this.fireEvent('undoStateChange', {\n canUndo: true,\n canRedo: undoIndex + 2 < undoStackLength,\n });\n this.fireEvent('input');\n }\n return this.focus();\n }\n\n // --- Get and set data\n\n getRoot(): HTMLElement {\n return this._root;\n }\n\n _getRawHTML(): string {\n return this._root.innerHTML;\n }\n\n _setRawHTML(html: string): Squire {\n const root = this._root;\n root.innerHTML = html;\n\n let node: Element | null = root;\n const child = node.firstChild;\n if (!child || child.nodeName === 'BR') {\n const block = this.createDefaultBlock();\n if (child) {\n node.replaceChild(block, child);\n } else {\n node.appendChild(block);\n }\n } else {\n while ((node = getNextBlock(node, root))) {\n fixCursor(node);\n }\n }\n\n this._ignoreChange = true;\n\n return this;\n }\n\n getHTML(withBookmark?: boolean): string {\n let range: Range | undefined;\n if (withBookmark) {\n range = this.getSelection();\n this._saveRangeToBookmark(range);\n }\n const html = this._getRawHTML().replace(/\\u200B/g, '');\n if (withBookmark) {\n this._getRangeAndRemoveBookmark(range);\n }\n return html;\n }\n\n setHTML(html: string): Squire {\n // Parse HTML into DOM tree\n const frag = this._config.sanitizeToDOMFragment(html, this);\n const root = this._root;\n\n // Fixup DOM tree\n cleanTree(frag, this._config);\n cleanupBRs(frag, root, false);\n fixContainer(frag, root);\n\n // Fix cursor\n let node: DocumentFragment | HTMLElement | null = frag;\n let child = node.firstChild;\n if (!child || child.nodeName === 'BR') {\n const block = this.createDefaultBlock();\n if (child) {\n node.replaceChild(block, child);\n } else {\n node.appendChild(block);\n }\n } else {\n while ((node = getNextBlock(node, root))) {\n fixCursor(node);\n }\n }\n\n // Don't fire an input event\n this._ignoreChange = true;\n\n // Remove existing root children and insert new content\n while ((child = root.lastChild)) {\n root.removeChild(child);\n }\n root.appendChild(frag);\n\n // Reset the undo stack\n this._undoIndex = -1;\n this._undoStack.length = 0;\n this._undoStackLength = 0;\n this._isInUndoState = false;\n\n // Record undo state\n const range =\n this._getRangeAndRemoveBookmark() ||\n createRange(root.firstElementChild || root, 0);\n this.saveUndoState(range);\n\n // Set inital selection\n this.setSelection(range);\n this._updatePath(range, true);\n\n return this;\n }\n\n /**\n * Insert HTML at the cursor location. If the selection is not collapsed\n * insertTreeFragmentIntoRange will delete the selection so that it is\n * replaced by the html being inserted.\n */\n insertHTML(html: string, isPaste?: boolean): Squire {\n // Parse\n const config = this._config;\n let frag = config.sanitizeToDOMFragment(html, this);\n\n // Record undo checkpoint\n const range = this.getSelection();\n this.saveUndoState(range);\n\n try {\n const root = this._root;\n\n if (config.addLinks) {\n this.addDetectedLinks(frag, frag);\n }\n cleanTree(frag, this._config);\n cleanupBRs(frag, root, false);\n removeEmptyInlines(frag);\n frag.normalize();\n\n let node: HTMLElement | DocumentFragment | null = frag;\n while ((node = getNextBlock(node, frag))) {\n fixCursor(node);\n }\n\n let doInsert = true;\n if (isPaste) {\n const event = new CustomEvent('willPaste', {\n cancelable: true,\n detail: {\n fragment: frag,\n },\n });\n this.fireEvent('willPaste', event);\n frag = event.detail.fragment;\n doInsert = !event.defaultPrevented;\n }\n\n if (doInsert) {\n insertTreeFragmentIntoRange(range, frag, root);\n range.collapse(false);\n\n // After inserting the fragment, check whether the cursor is\n // inside an element and if so if there is an equivalent\n // cursor position after the element. If there is, move it\n // there.\n moveRangeBoundaryOutOf(range, 'A', root);\n\n this._ensureBottomLine();\n }\n\n this.setSelection(range);\n this._updatePath(range, true);\n // Safari sometimes loses focus after paste. Weird.\n if (isPaste) {\n this.focus();\n }\n } catch (error) {\n this._config.didError(error);\n }\n return this;\n }\n\n insertElement(el: Element, range?: Range): Squire {\n if (!range) {\n range = this.getSelection();\n }\n range.collapse(true);\n if (isInline(el)) {\n insertNodeInRange(range, el);\n range.setStartAfter(el);\n } else {\n // Get containing block node.\n const root = this._root;\n const startNode: HTMLElement | null = getStartBlockOfRange(\n range,\n root,\n );\n let splitNode: Element | Node = startNode || root;\n\n let nodeAfterSplit: Node | null = null;\n // While at end of container node, move up DOM tree.\n while (splitNode !== root && !splitNode.nextSibling) {\n splitNode = splitNode.parentNode!;\n }\n // If in the middle of a container node, split up to root.\n if (splitNode !== root) {\n const parent = splitNode.parentNode!;\n nodeAfterSplit = split(\n parent,\n splitNode.nextSibling,\n root,\n root,\n ) as Node;\n }\n\n // If the startNode was empty remove it so that we don't end up\n // with two blank lines.\n if (startNode && isEmptyBlock(startNode)) {\n detach(startNode);\n }\n\n // Insert element and blank line.\n root.insertBefore(el, nodeAfterSplit);\n const blankLine = this.createDefaultBlock();\n root.insertBefore(blankLine, nodeAfterSplit);\n\n // Move cursor to blank line after inserted element.\n range.setStart(blankLine, 0);\n range.setEnd(blankLine, 0);\n moveRangeBoundariesDownTree(range);\n }\n this.focus();\n this.setSelection(range);\n this._updatePath(range);\n\n return this;\n }\n\n insertImage(\n src: string,\n attributes: Record,\n ): HTMLImageElement {\n const img = createElement(\n 'IMG',\n Object.assign(\n {\n src: src,\n },\n attributes,\n ),\n ) as HTMLImageElement;\n this.insertElement(img);\n return img;\n }\n\n insertPlainText(plainText: string, isPaste: boolean): Squire {\n const range = this.getSelection();\n if (\n range.collapsed &&\n getNearest(range.startContainer, this._root, 'PRE')\n ) {\n const startContainer: Node = range.startContainer;\n let offset = range.startOffset;\n let textNode: Text;\n if (!startContainer || !(startContainer instanceof Text)) {\n const text = document.createTextNode('');\n startContainer.insertBefore(\n text,\n startContainer.childNodes[offset],\n );\n textNode = text;\n offset = 0;\n } else {\n textNode = startContainer;\n }\n let doInsert = true;\n if (isPaste) {\n const event = new CustomEvent('willPaste', {\n cancelable: true,\n detail: {\n text: plainText,\n },\n });\n this.fireEvent('willPaste', event);\n plainText = event.detail.text;\n doInsert = !event.defaultPrevented;\n }\n\n if (doInsert) {\n textNode.insertData(offset, plainText);\n range.setStart(textNode, offset + plainText.length);\n range.collapse(true);\n }\n this.setSelection(range);\n return this;\n }\n const lines = plainText.split('\\n');\n const config = this._config;\n const tag = config.blockTag;\n const attributes = config.blockAttributes;\n const closeBlock = '';\n let openBlock = '<' + tag;\n\n for (const attr in attributes) {\n openBlock += ' ' + attr + '=\"' + escapeHTML(attributes[attr]) + '\"';\n }\n openBlock += '>';\n\n for (let i = 0, l = lines.length; i < l; i += 1) {\n let line = lines[i];\n line = escapeHTML(line).replace(/ (?=(?: |$))/g, ' ');\n // We don't wrap the first line in the block, so if it gets inserted\n // into a blank line it keeps that line's formatting.\n // Wrap each line in
\n if (i) {\n line = openBlock + (line || '
') + closeBlock;\n }\n lines[i] = line;\n }\n return this.insertHTML(lines.join(''), isPaste);\n }\n\n getSelectedText(range?: Range): string {\n return getTextContentsOfRange(range || this.getSelection());\n }\n\n // --- Inline formatting\n\n /**\n * Extracts the font-family and font-size (if any) of the element\n * holding the cursor. If there's a selection, returns an empty object.\n */\n getFontInfo(range?: Range): Record {\n const fontInfo = {\n color: undefined,\n backgroundColor: undefined,\n fontFamily: undefined,\n fontSize: undefined,\n } as Record;\n\n if (!range) {\n range = this.getSelection();\n }\n\n let seenAttributes = 0;\n let element: Node | null = range.commonAncestorContainer;\n if (range.collapsed || element instanceof Text) {\n if (element instanceof Text) {\n element = element.parentNode!;\n }\n while (seenAttributes < 4 && element) {\n const style = (element as HTMLElement).style;\n if (style) {\n const color = style.color;\n if (!fontInfo.color && color) {\n fontInfo.color = color;\n seenAttributes += 1;\n }\n const backgroundColor = style.backgroundColor;\n if (!fontInfo.backgroundColor && backgroundColor) {\n fontInfo.backgroundColor = backgroundColor;\n seenAttributes += 1;\n }\n const fontFamily = style.fontFamily;\n if (!fontInfo.fontFamily && fontFamily) {\n fontInfo.fontFamily = fontFamily;\n seenAttributes += 1;\n }\n const fontSize = style.fontSize;\n if (!fontInfo.fontSize && fontSize) {\n fontInfo.fontSize = fontSize;\n seenAttributes += 1;\n }\n }\n element = element.parentNode;\n }\n }\n return fontInfo;\n }\n\n /**\n * Looks for matching tag and attributes, so won't work if \n * instead of etc.\n */\n hasFormat(\n tag: string,\n attributes?: Record | null,\n range?: Range,\n ): boolean {\n // 1. Normalise the arguments and get selection\n tag = tag.toUpperCase();\n if (!attributes) {\n attributes = {};\n }\n if (!range) {\n range = this.getSelection();\n }\n\n // Move range up one level in the DOM tree if at the edge of a text\n // node, so we don't consider it included when it's not really.\n if (\n !range.collapsed &&\n range.startContainer instanceof Text &&\n range.startOffset === range.startContainer.length &&\n range.startContainer.nextSibling\n ) {\n range.setStartBefore(range.startContainer.nextSibling);\n }\n if (\n !range.collapsed &&\n range.endContainer instanceof Text &&\n range.endOffset === 0 &&\n range.endContainer.previousSibling\n ) {\n range.setEndAfter(range.endContainer.previousSibling);\n }\n\n // If the common ancestor is inside the tag we require, we definitely\n // have the format.\n const root = this._root;\n const common = range.commonAncestorContainer;\n if (getNearest(common, root, tag, attributes)) {\n return true;\n }\n\n // If common ancestor is a text node and doesn't have the format, we\n // definitely don't have it.\n if (common instanceof Text) {\n return false;\n }\n\n // Otherwise, check each text node at least partially contained within\n // the selection and make sure all of them have the format we want.\n const walker = new TreeIterator(common, SHOW_TEXT, (node) => {\n return isNodeContainedInRange(range!, node, true);\n });\n\n let seenNode = false;\n let node: Node | null;\n while ((node = walker.nextNode())) {\n if (!getNearest(node, root, tag, attributes)) {\n return false;\n }\n seenNode = true;\n }\n\n return seenNode;\n }\n\n changeFormat(\n add: { tag: string; attributes?: Record } | null,\n remove?: { tag: string; attributes?: Record } | null,\n range?: Range,\n partial?: boolean,\n ): Squire {\n // Normalise the arguments and get selection\n if (!range) {\n range = this.getSelection();\n }\n\n // Save undo checkpoint\n this.saveUndoState(range);\n\n if (remove) {\n range = this._removeFormat(\n remove.tag.toUpperCase(),\n remove.attributes || {},\n range,\n partial,\n );\n }\n if (add) {\n range = this._addFormat(\n add.tag.toUpperCase(),\n add.attributes || {},\n range,\n );\n }\n\n this.setSelection(range);\n this._updatePath(range, true);\n\n return this.focus();\n }\n\n _addFormat(\n tag: string,\n attributes: Record | null,\n range: Range,\n ): Range {\n // If the range is collapsed we simply insert the node by wrapping\n // it round the range and focus it.\n const root = this._root;\n if (range.collapsed) {\n const el = fixCursor(createElement(tag, attributes));\n insertNodeInRange(range, el);\n const focusNode = el.firstChild || el;\n // Focus after the ZWS if present\n const focusOffset =\n focusNode instanceof Text ? focusNode.length : 0;\n range.setStart(focusNode, focusOffset);\n range.collapse(true);\n\n // Clean up any previous formats that may have been set on this\n // block that are unused.\n let block = el;\n while (isInline(block)) {\n block = block.parentNode!;\n }\n removeZWS(block, el);\n // Otherwise we find all the textnodes in the range (splitting\n // partially selected nodes) and if they're not already formatted\n // correctly we wrap them in the appropriate tag.\n } else {\n // Create an iterator to walk over all the text nodes under this\n // ancestor which are in the range and not already formatted\n // correctly.\n //\n // In Blink/WebKit, empty blocks may have no text nodes, just a\n //
. Therefore we wrap this in the tag as well, as this will\n // then cause it to apply when the user types something in the\n // block, which is presumably what was intended.\n //\n // IMG tags are included because we may want to create a link around\n // them, and adding other styles is harmless.\n const walker = new TreeIterator(\n range.commonAncestorContainer,\n SHOW_ELEMENT_OR_TEXT,\n (node: Node) => {\n return (\n (node instanceof Text ||\n node.nodeName === 'BR' ||\n node.nodeName === 'IMG') &&\n isNodeContainedInRange(range, node, true)\n );\n },\n );\n\n // Start at the beginning node of the range and iterate through\n // all the nodes in the range that need formatting.\n let { startContainer, startOffset, endContainer, endOffset } =\n range;\n\n // Make sure we start with a valid node.\n walker.currentNode = startContainer;\n if (\n (!(startContainer instanceof Element) &&\n !(startContainer instanceof Text)) ||\n !walker.filter(startContainer)\n ) {\n const next = walker.nextNode();\n // If there are no interesting nodes in the selection, abort\n if (!next) {\n return range;\n }\n startContainer = next;\n startOffset = 0;\n }\n\n do {\n let node = walker.currentNode;\n const needsFormat = !getNearest(node, root, tag, attributes);\n if (needsFormat) {\n //
can never be a container node, so must have a text\n // node if node == (end|start)Container\n if (\n node === endContainer &&\n (node as Text).length > endOffset\n ) {\n (node as Text).splitText(endOffset);\n }\n if (node === startContainer && startOffset) {\n node = (node as Text).splitText(startOffset);\n if (endContainer === startContainer) {\n endContainer = node;\n endOffset -= startOffset;\n } else if (endContainer === startContainer.parentNode) {\n endOffset += 1;\n }\n startContainer = node;\n startOffset = 0;\n }\n const el = createElement(tag, attributes);\n replaceWith(node, el);\n el.appendChild(node);\n }\n } while (walker.nextNode());\n\n // Now set the selection to as it was before\n range = createRange(\n startContainer,\n startOffset,\n endContainer,\n endOffset,\n );\n }\n return range;\n }\n\n _removeFormat(\n tag: string,\n attributes: Record,\n range: Range,\n partial?: boolean,\n ): Range {\n // Add bookmark\n this._saveRangeToBookmark(range);\n\n // We need a node in the selection to break the surrounding\n // formatted text.\n let fixer: Node | Text | null | undefined;\n if (range.collapsed) {\n if (cantFocusEmptyTextNodes) {\n fixer = document.createTextNode(ZWS);\n } else {\n fixer = document.createTextNode('');\n }\n insertNodeInRange(range, fixer!);\n }\n\n // Find block-level ancestor of selection\n let root = range.commonAncestorContainer;\n while (isInline(root)) {\n root = root.parentNode!;\n }\n\n // Find text nodes inside formatTags that are not in selection and\n // add an extra tag with the same formatting.\n const startContainer = range.startContainer;\n const startOffset = range.startOffset;\n const endContainer = range.endContainer;\n const endOffset = range.endOffset;\n const toWrap: [Node, Node][] = [];\n const examineNode = (node: Node, exemplar: Node) => {\n // If the node is completely contained by the range then\n // we're going to remove all formatting so ignore it.\n if (isNodeContainedInRange(range, node, false)) {\n return;\n }\n\n let child: Node;\n let next: Node;\n\n // If not at least partially contained, wrap entire contents\n // in a clone of the tag we're removing and we're done.\n if (!isNodeContainedInRange(range, node, true)) {\n // Ignore bookmarks and empty text nodes\n if (\n !(node instanceof HTMLInputElement) &&\n (!(node instanceof Text) || node.data)\n ) {\n toWrap.push([exemplar, node]);\n }\n return;\n }\n\n // Split any partially selected text nodes.\n if (node instanceof Text) {\n if (node === endContainer && endOffset !== node.length) {\n toWrap.push([exemplar, node.splitText(endOffset)]);\n }\n if (node === startContainer && startOffset) {\n node.splitText(startOffset);\n toWrap.push([exemplar, node]);\n }\n } else {\n // If not a text node, recurse onto all children.\n // Beware, the tree may be rewritten with each call\n // to examineNode, hence find the next sibling first.\n for (child = node.firstChild!; child; child = next) {\n next = child.nextSibling!;\n examineNode(child, exemplar);\n }\n }\n };\n const formatTags = Array.from(\n (root as Element).getElementsByTagName(tag),\n ).filter((el: Node): boolean => {\n return (\n isNodeContainedInRange(range, el, true) &&\n hasTagAttributes(el, tag, attributes)\n );\n });\n\n if (!partial) {\n formatTags.forEach((node: Node) => {\n examineNode(node, node);\n });\n }\n\n // Now wrap unselected nodes in the tag\n toWrap.forEach(([el, node]) => {\n el = el.cloneNode(false);\n replaceWith(node, el);\n el.appendChild(node);\n });\n // and remove old formatting tags.\n formatTags.forEach((el: Element) => {\n replaceWith(el, empty(el));\n });\n\n if (cantFocusEmptyTextNodes && fixer) {\n // Clean up any previous ZWS in this block. They are not needed,\n // and this works around a Chrome bug where it doesn't render the\n // text in some situations with multiple ZWS(!)\n fixer = fixer.parentNode;\n let block = fixer;\n while (block && isInline(block)) {\n block = block.parentNode;\n }\n if (block) {\n removeZWS(block, fixer);\n }\n }\n\n // Merge adjacent inlines:\n this._getRangeAndRemoveBookmark(range);\n if (fixer) {\n range.collapse(false);\n }\n mergeInlines(root, range);\n\n return range;\n }\n\n // ---\n\n bold(): Squire {\n return this.changeFormat({ tag: 'B' });\n }\n\n removeBold(): Squire {\n return this.changeFormat(null, { tag: 'B' });\n }\n\n italic(): Squire {\n return this.changeFormat({ tag: 'I' });\n }\n\n removeItalic(): Squire {\n return this.changeFormat(null, { tag: 'I' });\n }\n\n underline(): Squire {\n return this.changeFormat({ tag: 'U' });\n }\n\n removeUnderline(): Squire {\n return this.changeFormat(null, { tag: 'U' });\n }\n\n strikethrough(): Squire {\n return this.changeFormat({ tag: 'S' });\n }\n\n removeStrikethrough(): Squire {\n return this.changeFormat(null, { tag: 'S' });\n }\n\n subscript(): Squire {\n return this.changeFormat({ tag: 'SUB' }, { tag: 'SUP' });\n }\n\n removeSubscript(): Squire {\n return this.changeFormat(null, { tag: 'SUB' });\n }\n\n superscript(): Squire {\n return this.changeFormat({ tag: 'SUP' }, { tag: 'SUB' });\n }\n\n removeSuperscript(): Squire {\n return this.changeFormat(null, { tag: 'SUP' });\n }\n\n // ---\n\n makeLink(url: string, attributes?: Record): Squire {\n const range = this.getSelection();\n if (range.collapsed) {\n let protocolEnd = url.indexOf(':') + 1;\n if (protocolEnd) {\n while (url[protocolEnd] === '/') {\n protocolEnd += 1;\n }\n }\n insertNodeInRange(\n range,\n document.createTextNode(url.slice(protocolEnd)),\n );\n }\n attributes = Object.assign(\n {\n href: url,\n },\n this._config.tagAttributes.a,\n attributes,\n );\n\n return this.changeFormat(\n {\n tag: 'A',\n attributes: attributes as Record,\n },\n {\n tag: 'A',\n },\n range,\n );\n }\n\n removeLink(): Squire {\n return this.changeFormat(\n null,\n {\n tag: 'A',\n },\n this.getSelection(),\n true,\n );\n }\n\n /*\n linkRegExp = new RegExp(\n // Only look on boundaries\n '\\\\b(?:' +\n // Capture group 1: URLs\n '(' +\n // Add links to URLS\n // Starts with:\n '(?:' +\n // http(s):// or ftp://\n '(?:ht|f)tps?:\\\\/\\\\/' +\n // or\n '|' +\n // www.\n 'www\\\\d{0,3}[.]' +\n // or\n '|' +\n // foo90.com/\n '[a-z0-9][a-z0-9.\\\\-]*[.][a-z]{2,}\\\\/' +\n ')' +\n // Then we get one or more:\n '(?:' +\n // Run of non-spaces, non ()<>\n '[^\\\\s()<>]+' +\n // or\n '|' +\n // balanced parentheses (one level deep only)\n '\\\\([^\\\\s()<>]+\\\\)' +\n ')+' +\n // And we finish with\n '(?:' +\n // Not a space or punctuation character\n '[^\\\\s?&`!()\\\\[\\\\]{};:\\'\".,<>\u00AB\u00BB\u201C\u201D\u2018\u2019]' +\n // or\n '|' +\n // Balanced parentheses.\n '\\\\([^\\\\s()<>]+\\\\)' +\n ')' +\n // Capture group 2: Emails\n ')|(' +\n // Add links to emails\n '[\\\\w\\\\-.%+]+@(?:[\\\\w\\\\-]+\\\\.)+[a-z]{2,}\\\\b' +\n // Allow query parameters in the mailto: style\n '(?:' +\n '[?][^&?\\\\s]+=[^\\\\s?&`!()\\\\[\\\\]{};:\\'\".,<>\u00AB\u00BB\u201C\u201D\u2018\u2019]+' +\n '(?:&[^&?\\\\s]+=[^\\\\s?&`!()\\\\[\\\\]{};:\\'\".,<>\u00AB\u00BB\u201C\u201D\u2018\u2019]+)*' +\n ')?' +\n '))',\n 'i'\n );\n */\n linkRegExp =\n /\\b(?:((?:(?:ht|f)tps?:\\/\\/|www\\d{0,3}[.]|[a-z0-9][a-z0-9.\\-]*[.][a-z]{2,}\\/)(?:[^\\s()<>]+|\\([^\\s()<>]+\\))+(?:[^\\s?&`!()\\[\\]{};:'\".,<>\u00AB\u00BB\u201C\u201D\u2018\u2019]|\\([^\\s()<>]+\\)))|([\\w\\-.%+]+@(?:[\\w\\-]+\\.)+[a-z]{2,}\\b(?:[?][^&?\\s]+=[^\\s?&`!()\\[\\]{};:'\".,<>\u00AB\u00BB\u201C\u201D\u2018\u2019]+(?:&[^&?\\s]+=[^\\s?&`!()\\[\\]{};:'\".,<>\u00AB\u00BB\u201C\u201D\u2018\u2019]+)*)?))/i;\n\n addDetectedLinks(\n searchInNode: DocumentFragment | Node,\n root?: DocumentFragment | HTMLElement,\n ): Squire {\n const walker = new TreeIterator(\n searchInNode,\n SHOW_TEXT,\n (node) => !getNearest(node, root || this._root, 'A'),\n );\n const linkRegExp = this.linkRegExp;\n const defaultAttributes = this._config.tagAttributes.a;\n let node: Text | null;\n while ((node = walker.nextNode())) {\n const parent = node.parentNode!;\n let data = node.data;\n let match: RegExpExecArray | null;\n while ((match = linkRegExp.exec(data))) {\n const index = match.index;\n const endIndex = index + match[0].length;\n if (index) {\n parent.insertBefore(\n document.createTextNode(data.slice(0, index)),\n node,\n );\n }\n const child = createElement(\n 'A',\n Object.assign(\n {\n href: match[1]\n ? /^(?:ht|f)tps?:/i.test(match[1])\n ? match[1]\n : 'http://' + match[1]\n : 'mailto:' + match[0],\n },\n defaultAttributes,\n ),\n );\n child.textContent = data.slice(index, endIndex);\n parent.insertBefore(child, node);\n node.data = data = data.slice(endIndex);\n }\n }\n return this;\n }\n\n // ---\n\n setFontFace(name: string | null): Squire {\n const className = this._config.classNames.fontFamily;\n return this.changeFormat(\n name\n ? {\n tag: 'SPAN',\n attributes: {\n class: className,\n style: 'font-family: ' + name + ', sans-serif;',\n },\n }\n : null,\n {\n tag: 'SPAN',\n attributes: { class: className },\n },\n );\n }\n\n setFontSize(size: string | null): Squire {\n const className = this._config.classNames.fontSize;\n return this.changeFormat(\n size\n ? {\n tag: 'SPAN',\n attributes: {\n class: className,\n style:\n 'font-size: ' +\n (typeof size === 'number' ? size + 'px' : size),\n },\n }\n : null,\n {\n tag: 'SPAN',\n attributes: { class: className },\n },\n );\n }\n\n setTextColor(color: string | null): Squire {\n const className = this._config.classNames.color;\n return this.changeFormat(\n color\n ? {\n tag: 'SPAN',\n attributes: {\n class: className,\n style: 'color:' + color,\n },\n }\n : null,\n {\n tag: 'SPAN',\n attributes: { class: className },\n },\n );\n }\n\n setHighlightColor(color: string | null): Squire {\n const className = this._config.classNames.highlight;\n return this.changeFormat(\n color\n ? {\n tag: 'SPAN',\n attributes: {\n class: className,\n style: 'background-color:' + color,\n },\n }\n : null,\n {\n tag: 'SPAN',\n attributes: { class: className },\n },\n );\n }\n\n // --- Block formatting\n\n _ensureBottomLine(): void {\n const root = this._root;\n const last = root.lastElementChild;\n if (\n !last ||\n last.nodeName !== this._config.blockTag ||\n !isBlock(last)\n ) {\n root.appendChild(this.createDefaultBlock());\n }\n }\n\n createDefaultBlock(children?: Node[]): HTMLElement {\n const config = this._config;\n return fixCursor(\n createElement(config.blockTag, config.blockAttributes, children),\n ) as HTMLElement;\n }\n\n tagAfterSplit: Record = {\n DT: 'DD',\n DD: 'DT',\n LI: 'LI',\n PRE: 'PRE',\n };\n\n splitBlock(lineBreakOnly: boolean, range?: Range): Squire {\n if (!range) {\n range = this.getSelection();\n }\n const root = this._root;\n let block: Node | Element | null;\n let parent: Node | null;\n let node: Node;\n let nodeAfterSplit: Node;\n\n // Save undo checkpoint and remove any zws so we don't think there's\n // content in an empty block.\n this._recordUndoState(range);\n this._removeZWS();\n this._getRangeAndRemoveBookmark(range);\n\n // Selected text is overwritten, therefore delete the contents\n // to collapse selection.\n if (!range.collapsed) {\n deleteContentsOfRange(range, root);\n }\n\n // Linkify text\n if (this._config.addLinks) {\n moveRangeBoundariesDownTree(range);\n const textNode = range.startContainer as Text;\n const offset = range.startOffset;\n setTimeout(() => {\n linkifyText(this, textNode, offset);\n }, 0);\n }\n\n block = getStartBlockOfRange(range, root);\n\n // Inside a PRE, insert literal newline, unless on blank line.\n if (block && (parent = getNearest(block, root, 'PRE'))) {\n moveRangeBoundariesDownTree(range);\n node = range.startContainer;\n const offset = range.startOffset;\n if (!(node instanceof Text)) {\n node = document.createTextNode('');\n parent.insertBefore(node, parent.firstChild);\n }\n // If blank line: split and insert default block\n if (\n !lineBreakOnly &&\n node instanceof Text &&\n (node.data.charAt(offset - 1) === '\\n' ||\n rangeDoesStartAtBlockBoundary(range, root)) &&\n (node.data.charAt(offset) === '\\n' ||\n rangeDoesEndAtBlockBoundary(range, root))\n ) {\n node.deleteData(offset && offset - 1, offset ? 2 : 1);\n nodeAfterSplit = split(\n node,\n offset && offset - 1,\n root,\n root,\n ) as Node;\n node = nodeAfterSplit.previousSibling!;\n if (!node.textContent) {\n detach(node);\n }\n node = this.createDefaultBlock();\n nodeAfterSplit.parentNode!.insertBefore(node, nodeAfterSplit);\n if (!nodeAfterSplit.textContent) {\n detach(nodeAfterSplit);\n }\n range.setStart(node, 0);\n } else {\n (node as Text).insertData(offset, '\\n');\n fixCursor(parent);\n // Firefox bug: if you set the selection in the text node after\n // the new line, it draws the cursor before the line break still\n // but if you set the selection to the equivalent position\n // in the parent, it works.\n if ((node as Text).length === offset + 1) {\n range.setStartAfter(node);\n } else {\n range.setStart(node, offset + 1);\n }\n }\n range.collapse(true);\n this.setSelection(range);\n this._updatePath(range, true);\n this._docWasChanged();\n return this;\n }\n\n // If this is a malformed bit of document or in a table;\n // just play it safe and insert a
.\n if (!block || lineBreakOnly || /^T[HD]$/.test(block.nodeName)) {\n // If inside an
, move focus out\n moveRangeBoundaryOutOf(range, 'A', root);\n insertNodeInRange(range, createElement('BR'));\n range.collapse(false);\n this.setSelection(range);\n this._updatePath(range, true);\n return this;\n }\n\n // If in a list, we'll split the LI instead.\n if ((parent = getNearest(block, root, 'LI'))) {\n block = parent;\n }\n\n if (isEmptyBlock(block as Element)) {\n if (\n getNearest(block, root, 'UL') ||\n getNearest(block, root, 'OL')\n ) {\n // Break list\n this.decreaseListLevel(range);\n return this;\n // Break blockquote\n } else if (getNearest(block, root, 'BLOCKQUOTE')) {\n this.removeQuote(range);\n return this;\n }\n }\n\n // Otherwise, split at cursor point.\n node = range.startContainer;\n const offset = range.startOffset;\n let splitTag = this.tagAfterSplit[block.nodeName];\n nodeAfterSplit = split(\n node,\n offset,\n block.parentNode!,\n this._root,\n ) as Node;\n\n const config = this._config;\n let splitProperties: Record | null = null;\n if (!splitTag) {\n splitTag = config.blockTag;\n splitProperties = config.blockAttributes;\n }\n\n // Make sure the new node is the correct type.\n if (!hasTagAttributes(nodeAfterSplit, splitTag, splitProperties)) {\n block = createElement(splitTag, splitProperties);\n if ((nodeAfterSplit as HTMLElement).dir) {\n (block as HTMLElement).dir = (\n nodeAfterSplit as HTMLElement\n ).dir;\n }\n replaceWith(nodeAfterSplit, block);\n block.appendChild(empty(nodeAfterSplit));\n nodeAfterSplit = block;\n }\n\n // Clean up any empty inlines if we hit enter at the beginning of the\n // block\n removeZWS(block);\n removeEmptyInlines(block);\n fixCursor(block);\n\n // Focus cursor\n // If there's a / etc. at the beginning of the split\n // make sure we focus inside it.\n while (nodeAfterSplit instanceof Element) {\n let child = nodeAfterSplit.firstChild;\n let next;\n\n // Don't continue links over a block break; unlikely to be the\n // desired outcome.\n if (\n nodeAfterSplit.nodeName === 'A' &&\n (!nodeAfterSplit.textContent ||\n nodeAfterSplit.textContent === ZWS)\n ) {\n child = document.createTextNode('') as Text;\n replaceWith(nodeAfterSplit, child);\n nodeAfterSplit = child;\n break;\n }\n\n while (child && child instanceof Text && !child.data) {\n next = child.nextSibling;\n if (!next || next.nodeName === 'BR') {\n break;\n }\n detach(child);\n child = next;\n }\n\n // 'BR's essentially don't count; they're a browser hack.\n // If you try to select the contents of a 'BR', FF will not let\n // you type anything!\n if (!child || child.nodeName === 'BR' || child instanceof Text) {\n break;\n }\n nodeAfterSplit = child;\n }\n range = createRange(nodeAfterSplit, 0);\n this.setSelection(range);\n this._updatePath(range, true);\n\n return this;\n }\n\n forEachBlock(\n fn: (el: HTMLElement) => any,\n mutates: boolean,\n range?: Range,\n ): Squire {\n if (!range) {\n range = this.getSelection();\n }\n\n // Save undo checkpoint\n if (mutates) {\n this.saveUndoState(range);\n }\n\n const root = this._root;\n let start = getStartBlockOfRange(range, root);\n const end = getEndBlockOfRange(range, root);\n if (start && end) {\n do {\n if (fn(start) || start === end) {\n break;\n }\n } while ((start = getNextBlock(start, root)));\n }\n\n if (mutates) {\n this.setSelection(range);\n // Path may have changed\n this._updatePath(range, true);\n }\n return this;\n }\n\n modifyBlocks(modify: (x: DocumentFragment) => Node, range?: Range): Squire {\n if (!range) {\n range = this.getSelection();\n }\n\n // 1. Save undo checkpoint and bookmark selection\n this._recordUndoState(range, this._isInUndoState);\n\n // 2. Expand range to block boundaries\n const root = this._root;\n expandRangeToBlockBoundaries(range, root);\n\n // 3. Remove range.\n moveRangeBoundariesUpTree(range, root, root, root);\n const frag = extractContentsOfRange(range, root, root);\n\n // 4. Modify tree of fragment and reinsert.\n if (!range.collapsed) {\n // After extracting contents, the range edges will still be at the\n // level we began the spilt. We want to insert directly in the\n // root, so move the range up there.\n let node = range.endContainer;\n if (node === root) {\n range.collapse(false);\n } else {\n while (node.parentNode !== root) {\n node = node.parentNode!;\n }\n range.setStartBefore(node);\n range.collapse(true);\n }\n }\n insertNodeInRange(range, modify.call(this, frag));\n\n // 5. Merge containers at edges\n if (range.endOffset < range.endContainer.childNodes.length) {\n mergeContainers(\n range.endContainer.childNodes[range.endOffset],\n root,\n );\n }\n mergeContainers(\n range.startContainer.childNodes[range.startOffset],\n root,\n );\n\n // 6. Restore selection\n this._getRangeAndRemoveBookmark(range);\n this.setSelection(range);\n this._updatePath(range, true);\n\n return this;\n }\n\n // ---\n\n setTextAlignment(alignment: string): Squire {\n this.forEachBlock((block: HTMLElement) => {\n const className = block.className\n .split(/\\s+/)\n .filter((klass) => {\n return !!klass && !/^align/.test(klass);\n })\n .join(' ');\n if (alignment) {\n block.className = className + ' align-' + alignment;\n block.style.textAlign = alignment;\n } else {\n block.className = className;\n block.style.textAlign = '';\n }\n }, true);\n return this.focus();\n }\n\n setTextDirection(direction: string | null): Squire {\n this.forEachBlock((block: HTMLElement) => {\n if (direction) {\n block.dir = direction;\n } else {\n block.removeAttribute('dir');\n }\n }, true);\n return this.focus();\n }\n\n // ---\n\n _getListSelection(\n range: Range,\n root: Element,\n ): [Node, Node | null, Node | null] | null {\n let list: Node | null = range.commonAncestorContainer;\n let startLi: Node | null = range.startContainer;\n let endLi: Node | null = range.endContainer;\n while (list && list !== root && !/^[OU]L$/.test(list.nodeName)) {\n list = list.parentNode;\n }\n if (!list || list === root) {\n return null;\n }\n if (startLi === list) {\n startLi = startLi.childNodes[range.startOffset];\n }\n if (endLi === list) {\n endLi = endLi.childNodes[range.endOffset];\n }\n while (startLi && startLi.parentNode !== list) {\n startLi = startLi.parentNode;\n }\n while (endLi && endLi.parentNode !== list) {\n endLi = endLi.parentNode;\n }\n return [list, startLi, endLi];\n }\n\n increaseListLevel(range?: Range) {\n if (!range) {\n range = this.getSelection();\n }\n\n // Get start+end li in single common ancestor\n const root = this._root;\n const listSelection = this._getListSelection(range, root);\n if (!listSelection) {\n return this.focus();\n }\n // eslint-disable-next-line prefer-const\n let [list, startLi, endLi] = listSelection;\n if (!startLi || startLi === list.firstChild) {\n return this.focus();\n }\n\n // Save undo checkpoint and bookmark selection\n this._recordUndoState(range, this._isInUndoState);\n\n // Increase list depth\n const type = list.nodeName;\n let newParent = startLi.previousSibling!;\n let listAttrs: Record | null;\n let next: Node | null;\n if (newParent.nodeName !== type) {\n listAttrs = this._config.tagAttributes[type.toLowerCase()];\n newParent = createElement(type, listAttrs);\n list.insertBefore(newParent, startLi);\n }\n do {\n next = startLi === endLi ? null : startLi.nextSibling;\n newParent.appendChild(startLi);\n } while ((startLi = next));\n next = newParent.nextSibling;\n if (next) {\n mergeContainers(next, root);\n }\n\n // Restore selection\n this._getRangeAndRemoveBookmark(range);\n this.setSelection(range);\n this._updatePath(range, true);\n\n return this.focus();\n }\n\n decreaseListLevel(range?: Range) {\n if (!range) {\n range = this.getSelection();\n }\n\n const root = this._root;\n const listSelection = this._getListSelection(range, root);\n if (!listSelection) {\n return this.focus();\n }\n\n // eslint-disable-next-line prefer-const\n let [list, startLi, endLi] = listSelection;\n if (!startLi) {\n startLi = list.firstChild;\n }\n if (!endLi) {\n endLi = list.lastChild!;\n }\n\n // Save undo checkpoint and bookmark selection\n this._recordUndoState(range, this._isInUndoState);\n\n let next: Node | null;\n let insertBefore: Node | null = null;\n if (startLi) {\n // Find the new parent list node\n let newParent = list.parentNode!;\n\n // Split list if necessary\n insertBefore = !endLi.nextSibling\n ? list.nextSibling\n : (split(list, endLi.nextSibling, newParent, root) as Node);\n\n if (newParent !== root && newParent.nodeName === 'LI') {\n newParent = newParent.parentNode!;\n while (insertBefore) {\n next = insertBefore.nextSibling;\n endLi.appendChild(insertBefore);\n insertBefore = next;\n }\n insertBefore = list.parentNode!.nextSibling;\n }\n\n const makeNotList = !/^[OU]L$/.test(newParent.nodeName);\n do {\n next = startLi === endLi ? null : startLi.nextSibling;\n list.removeChild(startLi);\n if (makeNotList && startLi.nodeName === 'LI') {\n startLi = this.createDefaultBlock([empty(startLi)]);\n }\n newParent.insertBefore(startLi!, insertBefore);\n } while ((startLi = next));\n }\n\n if (!list.firstChild) {\n detach(list);\n }\n\n if (insertBefore) {\n mergeContainers(insertBefore, root);\n }\n\n // Restore selection\n this._getRangeAndRemoveBookmark(range);\n this.setSelection(range);\n this._updatePath(range, true);\n\n return this.focus();\n }\n\n _makeList(frag: DocumentFragment, type: string): DocumentFragment {\n const walker = getBlockWalker(frag, this._root);\n const tagAttributes = this._config.tagAttributes;\n const listAttrs = tagAttributes[type.toLowerCase()];\n const listItemAttrs = tagAttributes.li;\n let node: Node | null;\n while ((node = walker.nextNode())) {\n if (node.parentNode! instanceof HTMLLIElement) {\n node = node.parentNode!;\n walker.currentNode = node.lastChild!;\n }\n if (!(node instanceof HTMLLIElement)) {\n const newLi = createElement('LI', listItemAttrs);\n if ((node as HTMLElement).dir) {\n newLi.dir = (node as HTMLElement).dir;\n }\n\n // Have we replaced the previous block with a new