diff --git a/.editorconfig b/.editorconfig index 0d028a6f..102bd7ae 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,6 @@ insert_final_newline = true charset = utf-8 indent_style = space indent_size = 4 + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bcc31e19..2f8819e8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,20 @@ +before_script: + - npm ci --ignore-scripts --force + +build: + stage: build + script: npm run build + only: + - merge_requests + +lint: + stage: test + script: npm run lint + only: + - merge_requests + test: stage: test - script: npm ci --ignore-scripts --force && npm run test + script: npm run test only: - merge_requests diff --git a/dist/squire-raw.js b/dist/squire-raw.js index b6165025..751b4111 100644 --- a/dist/squire-raw.js +++ b/dist/squire-raw.js @@ -338,11 +338,13 @@ if (!child || isLeaf(child)) { if (startOffset) { child = startContainer.childNodes[startOffset - 1]; - let prev = child.previousSibling; - while (child instanceof Text && !child.length && prev && prev instanceof Text) { - child.remove(); - child = prev; - continue; + const prev = child.previousSibling; + if (prev && prev instanceof Text) { + while (child instanceof Text && !child.length) { + child.remove(); + child = prev; + continue; + } } if (child instanceof Text) { startContainer = child; @@ -466,18 +468,14 @@ return node; }; var fixContainer = (container, root) => { - const children = container.childNodes; let wrapper = null; - for (let i = 0, l = children.length; i < l; i += 1) { - const child = children[i]; + [...container.childNodes].forEach((child) => { const isBR = child.nodeName === "BR"; if (!isBR && isInline(child)) { if (!wrapper) { wrapper = createElement("DIV"); } wrapper.appendChild(child); - i -= 1; - l -= 1; } else if (isBR || wrapper) { if (!wrapper) { wrapper = createElement("DIV"); @@ -487,15 +485,13 @@ container.replaceChild(wrapper, child); } else { container.insertBefore(wrapper, child); - i += 1; - l += 1; } wrapper = null; } if (isContainer(child)) { fixContainer(child, root); } - } + }); if (wrapper) { container.appendChild(fixCursor(wrapper)); } diff --git a/dist/squire-raw.mjs b/dist/squire-raw.mjs index 4d75107a..5f5ef1ce 100644 --- a/dist/squire-raw.mjs +++ b/dist/squire-raw.mjs @@ -340,11 +340,13 @@ var moveRangeBoundariesDownTree = (range) => { if (!child || isLeaf(child)) { if (startOffset) { child = startContainer.childNodes[startOffset - 1]; - let prev = child.previousSibling; - while (child instanceof Text && !child.length && prev && prev instanceof Text) { - child.remove(); - child = prev; - continue; + const prev = child.previousSibling; + if (prev && prev instanceof Text) { + while (child instanceof Text && !child.length) { + child.remove(); + child = prev; + continue; + } } if (child instanceof Text) { startContainer = child; @@ -468,18 +470,14 @@ var fixCursor = (node) => { return node; }; var fixContainer = (container, root) => { - const children = container.childNodes; let wrapper = null; - for (let i = 0, l = children.length; i < l; i += 1) { - const child = children[i]; + [...container.childNodes].forEach((child) => { const isBR = child.nodeName === "BR"; if (!isBR && isInline(child)) { if (!wrapper) { wrapper = createElement("DIV"); } wrapper.appendChild(child); - i -= 1; - l -= 1; } else if (isBR || wrapper) { if (!wrapper) { wrapper = createElement("DIV"); @@ -489,15 +487,13 @@ var fixContainer = (container, root) => { container.replaceChild(wrapper, child); } else { container.insertBefore(wrapper, child); - i += 1; - l += 1; } wrapper = null; } if (isContainer(child)) { fixContainer(child, root); } - } + }); if (wrapper) { container.appendChild(fixCursor(wrapper)); } diff --git a/dist/squire.js b/dist/squire.js index bbf64b16..566e5003 100644 --- a/dist/squire.js +++ b/dist/squire.js @@ -1,4 +1,4 @@ -"use strict";(()=>{var ot=()=>!0,_=class{constructor(t,e,n){this.root=t,this.currentNode=t,this.nodeType=e,this.filter=n||ot}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 W="\u200B",V=navigator.userAgent,ce=/Mac OS X/.test(V),de=/Windows NT/.test(V),Ce=/iP(?:ad|hone|od)/.test(V)||ce&&!!navigator.maxTouchPoints,Me=/Android/.test(V),Fe=/Gecko\//.test(V),se=/Edge\//.test(V),st=!se&&/WebKit\//.test(V),y=ce||Ce?"Meta-":"Ctrl-",re=st,He="onbeforeinput"in document&&"inputType"in new InputEvent("input"),w=/[^ \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,_e=1,Pe=2,Ue=3,fe=new WeakMap,qe=()=>{fe=new WeakMap},I=i=>ct.has(i.nodeName),ve=i=>{switch(i.nodeType){case 3:return _e;case 1:case 11:if(fe.has(i))return fe.get(i);break;default:return dt}let t;return Array.from(i.childNodes).every(N)?at.test(i.nodeName)?t=_e:t=Pe:t=Ue,fe.set(i,t),t},N=i=>ve(i)===_e,H=i=>ve(i)===Pe,j=i=>ve(i)===Ue;var m=(i,t,e)=>{let n=document.createElement(i);if(t instanceof Array&&(e=t,t=null),t)for(let o in t){let s=t[o];s!==void 0&&n.setAttribute(o,s)}return e&&e.forEach(o=>n.appendChild(o)),n},xe=(i,t)=>I(i)||i.nodeType!==t.nodeType||i.nodeName!==t.nodeName?!1:i instanceof HTMLElement&&t instanceof HTMLElement?i.nodeName!=="A"&&i.className===t.className&&i.style.cssText===t.style.cssText:!0,ue=(i,t,e)=>{if(i.nodeName!==t)return!1;for(let n in e)if(!("getAttribute"in i)||i.getAttribute(n)!==e[n])return!1;return!0},g=(i,t,e,n)=>{for(;i&&i!==t;){if(ue(i,e,n))return i;i=i.parentNode}return null},he=(i,t)=>{let e=i.childNodes;for(;t&&i instanceof Element;)i=e[t-1],e=i.childNodes,t=e.length;return i},Le=(i,t)=>{let e=i;if(e instanceof Element){let n=e.childNodes;if(ti instanceof Element||i instanceof DocumentFragment?i.childNodes.length:i instanceof CharacterData?i.length:0,S=i=>{let t=document.createDocumentFragment(),e=i.firstChild;for(;e;)t.appendChild(e),e=i.firstChild;return t},E=i=>{let t=i.parentNode;return t&&t.removeChild(i),i},L=(i,t)=>{let e=i.parentNode;e&&e.replaceChild(t,i)};var ft=i=>i instanceof Element?i.nodeName==="BR":w.test(i.data),Y=(i,t)=>{let e=i.parentNode;for(;N(e);)e=e.parentNode;let n=new _(e,5,ft);return n.currentNode=i,!!n.nextNode()||t&&!n.previousNode()},me=(i,t)=>{let e=new _(i,4),n,o;for(;n=e.nextNode();)for(;(o=n.data.indexOf(W))>-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(o,1)};var ut=0,ht=1,mt=2,pt=3,K=(i,t,e)=>{let n=document.createRange();if(n.selectNode(t),e){let o=i.compareBoundaryPoints(pt,n)>-1,s=i.compareBoundaryPoints(ht,n)<1;return!o&&!s}else{let o=i.compareBoundaryPoints(ut,n)<1,s=i.compareBoundaryPoints(mt,n)>-1;return o&&s}},C=i=>{let{startContainer:t,startOffset:e,endContainer:n,endOffset:o}=i;for(;!(t instanceof Text);){let s=t.childNodes[e];if(!s||I(s)){if(e){s=t.childNodes[e-1];let r=s.previousSibling;for(;s instanceof Text&&!s.length&&r&&r instanceof Text;)s.remove(),s=r;s instanceof Text&&(t=s,e=s.data.length)}break}t=s,e=0}if(o)for(;!(n instanceof Text);){let s=n.childNodes[o-1];if(!s||I(s)){if(s&&s.nodeName==="BR"&&!Y(s,!1)){o-=1;continue}break}n=s,o=k(n)}else for(;!(n instanceof Text);){let s=n.firstChild;if(!s||I(s))break;n=s}i.setStart(t,e),i.setEnd(n,o)},P=(i,t,e,n)=>{let o=i.startContainer,s=i.startOffset,r=i.endContainer,l=i.endOffset,a;for(t||(t=i.commonAncestorContainer),e||(e=t);!s&&o!==t&&o!==n;)a=o.parentNode,s=Array.from(a.childNodes).indexOf(o),o=a;for(;!(r===e||r===n||(r.nodeType!==3&&r.childNodes[l]&&r.childNodes[l].nodeName==="BR"&&!Y(r.childNodes[l],!1)&&(l+=1),l!==k(r)));)a=r.parentNode,l=Array.from(a.childNodes).indexOf(r)+1,r=a;i.setStart(o,s),i.setEnd(r,l)},Re=(i,t,e)=>{let n=g(i.endContainer,e,t);if(n&&(n=n.parentNode)){let o=i.cloneRange();P(o,n,n,e),o.endContainer===n&&(i.setStart(o.endContainer,o.endOffset),i.setEnd(o.endContainer,o.endOffset))}return i};var b=i=>{let t=null;if(i instanceof Text)return i;if(N(i)){let e=i.firstChild;if(re)for(;e&&e instanceof Text&&!e.data;)i.removeChild(e),e=i.firstChild;e||(re?t=document.createTextNode(W):t=document.createTextNode(""))}else if(i instanceof Element&&!i.querySelector("BR")){t=m("BR");let e=i,n;for(;(n=e.lastElementChild)&&!N(n);)e=n}if(t)try{i.appendChild(t)}catch(e){}return i},B=(i,t)=>{let e=i.childNodes,n=null;for(let o=0,s=e.length;o{if(i instanceof Text&&i!==e){if(typeof t!="number")throw new Error("Offset must be a number to split text node!");if(!i.parentNode)throw new Error("Cannot split text node with no parent!");return D(i.parentNode,i.splitText(t),e,n)}let o=typeof t=="number"?t{let e=i.childNodes,n=e.length,o=[];for(;n--;){let s=e[n],r=n?e[n-1]:null;if(r&&N(s)&&xe(s,r))t.startContainer===s&&(t.startContainer=r,t.startOffset+=k(r)),t.endContainer===s&&(t.endContainer=r,t.endOffset+=k(r)),t.startContainer===i&&(t.startOffset>n?t.startOffset-=1:t.startOffset===n&&(t.startContainer=r,t.startOffset=k(r))),t.endContainer===i&&(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):o.push(S(s));else if(s instanceof Element){let l;for(;l=o.pop();)s.appendChild(l);We(s,t)}}},J=(i,t)=>{let e=i instanceof Text?i.parentNode:i;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)}},$=(i,t,e,n)=>{let o=t,s,r;for(;(s=o.parentNode)&&s!==n&&s instanceof Element&&s.childNodes.length===1;)o=s;E(o),r=i.childNodes.length;let l=i.lastChild;l&&l.nodeName==="BR"&&(i.removeChild(l),r-=1),i.appendChild(S(t)),e.setStart(i,r),e.collapse(!0),J(i,e)},M=(i,t)=>{let e=i.previousSibling,n=i.firstChild,o=i.nodeName==="LI";if(!(o&&(!n||!/^[OU]L$/.test(n.nodeName)))){if(e&&xe(e,i)){if(!j(e))if(o){let r=m("DIV");r.appendChild(S(e)),e.appendChild(r)}else return;E(i);let s=!j(i);e.appendChild(S(i)),s&&B(e,t),n&&M(n,t)}else if(o){let s=m("DIV");i.insertBefore(s,n),b(s)}}};var Ke={"font-weight":{regexp:/^bold|^700/i,replace(){return m("B")}},"font-style":{regexp:/^italic/i,replace(){return m("I")}},"font-family":{regexp:w,replace(i,t){return m("SPAN",{class:i.fontFamily,style:"font-family:"+t})}},"font-size":{regexp:w,replace(i,t){return m("SPAN",{class:i.fontSize,style:"font-size:"+t})}},"text-decoration":{regexp:/^underline/i,replace(){return m("U")}}},Nt=(i,t,e)=>{let n=i.style,o,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===i.nodeName&&d.className===i.className)continue;s||(s=d),o&&o.appendChild(d),o=d,i.style.removeProperty(r)}}return s&&o&&(o.appendChild(S(i)),i.style.cssText?i.appendChild(s):L(i,s)),o||i},pe=i=>(t,e)=>{let n=m(i),o=t.attributes;for(let s=0,r=o.length;s{let n=i,o=n.face,s=n.size,r=n.color,l=e.classNames,a,d,c,f,u;return o&&(a=m("SPAN",{class:l.fontFamily,style:"font-family:"+o}),u=a,f=a),s&&(d=m("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=m("SPAN",{class:l.color,style:"color:"+r}),u||(u=c),f&&f.appendChild(c),f=c),(!u||!f)&&(u=f=m("SPAN")),t.replaceChild(u,n),f.appendChild(S(n)),f},TT:(i,t,e)=>{let n=m("SPAN",{class:e.classNames.fontFamily,style:'font-family:menlo,consolas,"courier new",monospace'});return t.replaceChild(n,i),n.appendChild(S(i)),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=(i,t,e)=>{let n=i.childNodes,o=i;for(;N(o);)o=o.parentNode;let s=new _(o,5);for(let r=0,l=n.length;r{let t=i.childNodes,e=t.length;for(;e--;){let n=t[e];n instanceof Element&&!I(n)?(ge(n),N(n)&&!n.firstChild&&i.removeChild(n)):n instanceof Text&&!n.data&&i.removeChild(n)}},ee=(i,t,e)=>{let n=i.querySelectorAll("BR"),o=[],s=n.length;for(let r=0;ri.split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""");var le=(i,t)=>{let e=new _(t,1,H);return e.currentNode=i,e},z=(i,t)=>{let e=le(i,t).previousNode();return e!==t?e:null},U=(i,t)=>{let e=le(i,t).nextNode();return e!==t?e:null},ae=i=>!i.textContent&&!i.querySelector("IMG");var R=(i,t)=>{let e=i.startContainer,n;if(N(e))n=z(e,t);else if(e!==t&&e instanceof HTMLElement&&H(e))n=e;else{let o=he(e,i.startOffset);n=U(o,t)}return n&&K(i,n,!0)?n:null},G=(i,t)=>{let e=i.endContainer,n;if(N(e))n=z(e,t);else if(e!==t&&e instanceof HTMLElement&&H(e))n=e;else{let o=Le(e,i.endOffset);if(!o||!t.contains(o)){o=t;let s;for(;s=o.lastChild;)o=s}n=z(o,t)}return n&&K(i,n,!0)?n:null},ze=i=>i instanceof Text?w.test(i.data):i.nodeName==="IMG",Q=(i,t)=>{let e=i.startContainer,n=i.startOffset,o;if(e instanceof Text){if(n)return!1;o=e}else if(o=Le(e,n),o&&!t.contains(o)&&(o=null),!o&&(o=he(e,n),o instanceof Text&&o.length))return!1;let s=R(i,t);if(!s)return!1;let r=new _(s,5,ze);return r.currentNode=o,!r.previousNode()},ie=(i,t)=>{let e=i.endContainer,n=i.endOffset,o;if(e instanceof Text){let l=e.data.length;if(l&&n{let e=R(i,t),n=G(i,t),o;e&&n&&(o=e.parentNode,i.setStart(o,Array.from(o.childNodes).indexOf(e)),o=n.parentNode,i.setEnd(o,Array.from(o.childNodes).indexOf(n)+1))};function X(i,t,e,n){let o=document.createRange();return o.setStart(i,t),e&&typeof n=="number"?o.setEnd(e,n):o.setEnd(i,t),o}var Z=(i,t)=>{let{startContainer:e,startOffset:n,endContainer:o,endOffset:s}=i,r;if(e instanceof Text){let a=e.parentNode;if(r=a.childNodes,n===e.length)n=Array.from(r).indexOf(e)+1,i.collapsed&&(o=a,s=n);else{if(n){let d=e.splitText(n);o===e?(s-=n,o=d):o===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===o&&(s+=r.length-l),i.setStart(e,n),i.setEnd(o,s)},ye=(i,t,e)=>{let n=document.createDocumentFragment();if(i.collapsed)return n;t||(t=i.commonAncestorContainer),t instanceof Text&&(t=t.parentNode);let o=i.startContainer,s=i.startOffset,r=D(i.endContainer,i.endOffset,t,e),l=0,a=D(o,s,t,e);for(;a&&a!==r;){let d=a.nextSibling;n.appendChild(a),a=d}return o instanceof Text&&r instanceof Text&&(o.appendData(r.data),E(r),r=o,l=s),i.setStart(o,s),r?i.setEnd(r,l):i.setEnd(t,t.childNodes.length),b(t),n},Ge=(i,t,e)=>{i.currentNode=e;let n;for(;n=i[t]();){if(n instanceof Text||I(n))return n;if(!N(n))return null}return null},F=(i,t)=>{let e=R(i,t),n=G(i,t),o=e!==n;e&&n&&(C(i),P(i,e,n,t));let s=ye(i,null,t);C(i),o&&(n=G(i,t),e&&n&&e!==n&&$(e,n,i,t)),e&&b(e);let r=t.firstChild;(!r||r.nodeName==="BR")&&(b(t),t.firstChild&&i.selectNodeContents(t.firstChild)),i.collapse(!0);let l=i.startContainer,a=i.startOffset,d=new _(t,5),c=l,f=a;(!(c instanceof Text)||f===c.data.length)&&(c=Ge(d,"nextNode",c),f=0);let u=l,p=a-1;(!(u instanceof Text)||p===-1)&&(u=Ge(d,"previousPONode",c||(l instanceof Text?l:l.childNodes[a]||l)),u instanceof Text&&(p=u.data.length));let h=null,T=0;return c instanceof Text&&c.data.charAt(f)===" "&&Q(i,t)?(h=c,T=f):u instanceof Text&&u.data.charAt(p)===" "&&(c instanceof Text&&c.data.charAt(f)===" "||ie(i,t))&&(h=u,T=p),h&&h.replaceData(T,1,"\xA0"),i.setStart(l,a),i.collapse(!0),s},Ze=(i,t,e)=>{let n=t.firstChild&&N(t.firstChild),o;for(B(t,e),o=t;o=U(o,e);)b(o);i.collapsed||F(i,e),C(i),i.collapse(!1);let s=g(i.endContainer,e,"BLOCKQUOTE")||e,r=R(i,e),l=null,a=U(t,t),d=!n&&!!r&&ae(r);if(r&&a&&!d&&!g(a,t,"PRE")&&!g(a,t,"TABLE")){P(i,r,r,e),i.collapse(!0);let c=i.endContainer,f=i.endOffset;if(ee(r,e,!1),N(c)){let u=D(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();o=c.childNodes[f];)l.appendChild(o);$(c,a,i,e),f=Array.from(c.parentNode.childNodes).indexOf(c)+1,c=c.parentNode,i.setEnd(c,f)}if(k(t)){d&&r&&(i.setEndBefore(r),i.collapse(!1),E(r)),P(i,s,s,e);let c=D(i.endContainer,i.endOffset,s,e),f=c?c.previousSibling:s.lastChild;s.insertBefore(t,c),c?i.setEndBefore(c):i.setEnd(s,k(s)),r=G(i,e),C(i);let u=i.endContainer,p=i.endOffset;c&&j(c)&&M(c,e),c=f&&f.nextSibling,c&&j(c)&&M(c,e),i.setEnd(u,p)}if(l&&r){let c=i.cloneRange();$(r,l,c,e),i.setEnd(c.endContainer,c.endOffset)}C(i)};var Be=Array.prototype.indexOf,_t=(i,t,e,n,o,s)=>{let r=i.clipboardData,l=document.body,a=m("DIV"),d,c;t.childNodes.length===1&&t.childNodes[0]instanceof Text?(c=t.childNodes[0].data.replace(/ /g," "),s=!0):(a.appendChild(t),d=a.innerHTML,n&&(d=n(d))),c!==void 0||(o&&d!==void 0?c=o(d):(ee(a,e,!0),a.setAttribute("style","position:fixed;overflow:hidden;bottom:100%;right:100%;"),l.appendChild(a),c=a.innerText||a.textContent,c=c.replace(/ /g," "),l.removeChild(a))),de&&(c=c.replace(/\r?\n/g,`\r +"use strict";(()=>{var ot=()=>!0,_=class{constructor(t,e,n){this.root=t,this.currentNode=t,this.nodeType=e,this.filter=n||ot}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 W="\u200B",V=navigator.userAgent,ce=/Mac OS X/.test(V),de=/Windows NT/.test(V),Ce=/iP(?:ad|hone|od)/.test(V)||ce&&!!navigator.maxTouchPoints,Me=/Android/.test(V),Fe=/Gecko\//.test(V),se=/Edge\//.test(V),st=!se&&/WebKit\//.test(V),y=ce||Ce?"Meta-":"Ctrl-",re=st,He="onbeforeinput"in document&&"inputType"in new InputEvent("input"),w=/[^ \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,_e=1,Pe=2,Ue=3,fe=new WeakMap,qe=()=>{fe=new WeakMap},I=i=>ct.has(i.nodeName),ve=i=>{switch(i.nodeType){case 3:return _e;case 1:case 11:if(fe.has(i))return fe.get(i);break;default:return dt}let t;return Array.from(i.childNodes).every(N)?at.test(i.nodeName)?t=_e:t=Pe:t=Ue,fe.set(i,t),t},N=i=>ve(i)===_e,H=i=>ve(i)===Pe,j=i=>ve(i)===Ue;var m=(i,t,e)=>{let n=document.createElement(i);if(t instanceof Array&&(e=t,t=null),t)for(let o in t){let s=t[o];s!==void 0&&n.setAttribute(o,s)}return e&&e.forEach(o=>n.appendChild(o)),n},xe=(i,t)=>I(i)||i.nodeType!==t.nodeType||i.nodeName!==t.nodeName?!1:i instanceof HTMLElement&&t instanceof HTMLElement?i.nodeName!=="A"&&i.className===t.className&&i.style.cssText===t.style.cssText:!0,ue=(i,t,e)=>{if(i.nodeName!==t)return!1;for(let n in e)if(!("getAttribute"in i)||i.getAttribute(n)!==e[n])return!1;return!0},g=(i,t,e,n)=>{for(;i&&i!==t;){if(ue(i,e,n))return i;i=i.parentNode}return null},he=(i,t)=>{let e=i.childNodes;for(;t&&i instanceof Element;)i=e[t-1],e=i.childNodes,t=e.length;return i},Le=(i,t)=>{let e=i;if(e instanceof Element){let n=e.childNodes;if(ti instanceof Element||i instanceof DocumentFragment?i.childNodes.length:i instanceof CharacterData?i.length:0,S=i=>{let t=document.createDocumentFragment(),e=i.firstChild;for(;e;)t.appendChild(e),e=i.firstChild;return t},E=i=>{let t=i.parentNode;return t&&t.removeChild(i),i},L=(i,t)=>{let e=i.parentNode;e&&e.replaceChild(t,i)};var ft=i=>i instanceof Element?i.nodeName==="BR":w.test(i.data),Y=(i,t)=>{let e=i.parentNode;for(;N(e);)e=e.parentNode;let n=new _(e,5,ft);return n.currentNode=i,!!n.nextNode()||t&&!n.previousNode()},me=(i,t)=>{let e=new _(i,4),n,o;for(;n=e.nextNode();)for(;(o=n.data.indexOf(W))>-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(o,1)};var ut=0,ht=1,mt=2,pt=3,K=(i,t,e)=>{let n=document.createRange();if(n.selectNode(t),e){let o=i.compareBoundaryPoints(pt,n)>-1,s=i.compareBoundaryPoints(ht,n)<1;return!o&&!s}else{let o=i.compareBoundaryPoints(ut,n)<1,s=i.compareBoundaryPoints(mt,n)>-1;return o&&s}},C=i=>{let{startContainer:t,startOffset:e,endContainer:n,endOffset:o}=i;for(;!(t instanceof Text);){let s=t.childNodes[e];if(!s||I(s)){if(e){s=t.childNodes[e-1];let r=s.previousSibling;if(r&&r instanceof Text)for(;s instanceof Text&&!s.length;)s.remove(),s=r;s instanceof Text&&(t=s,e=s.data.length)}break}t=s,e=0}if(o)for(;!(n instanceof Text);){let s=n.childNodes[o-1];if(!s||I(s)){if(s&&s.nodeName==="BR"&&!Y(s,!1)){o-=1;continue}break}n=s,o=k(n)}else for(;!(n instanceof Text);){let s=n.firstChild;if(!s||I(s))break;n=s}i.setStart(t,e),i.setEnd(n,o)},P=(i,t,e,n)=>{let o=i.startContainer,s=i.startOffset,r=i.endContainer,l=i.endOffset,a;for(t||(t=i.commonAncestorContainer),e||(e=t);!s&&o!==t&&o!==n;)a=o.parentNode,s=Array.from(a.childNodes).indexOf(o),o=a;for(;!(r===e||r===n||(r.nodeType!==3&&r.childNodes[l]&&r.childNodes[l].nodeName==="BR"&&!Y(r.childNodes[l],!1)&&(l+=1),l!==k(r)));)a=r.parentNode,l=Array.from(a.childNodes).indexOf(r)+1,r=a;i.setStart(o,s),i.setEnd(r,l)},Re=(i,t,e)=>{let n=g(i.endContainer,e,t);if(n&&(n=n.parentNode)){let o=i.cloneRange();P(o,n,n,e),o.endContainer===n&&(i.setStart(o.endContainer,o.endOffset),i.setEnd(o.endContainer,o.endOffset))}return i};var b=i=>{let t=null;if(i instanceof Text)return i;if(N(i)){let e=i.firstChild;if(re)for(;e&&e instanceof Text&&!e.data;)i.removeChild(e),e=i.firstChild;e||(re?t=document.createTextNode(W):t=document.createTextNode(""))}else if(i instanceof Element&&!i.querySelector("BR")){t=m("BR");let e=i,n;for(;(n=e.lastElementChild)&&!N(n);)e=n}if(t)try{i.appendChild(t)}catch(e){}return i},B=(i,t)=>{let e=null;return[...i.childNodes].forEach(n=>{let o=n.nodeName==="BR";!o&&N(n)?(e||(e=m("DIV")),e.appendChild(n)):(o||e)&&(e||(e=m("DIV")),b(e),o?i.replaceChild(e,n):i.insertBefore(e,n),e=null),j(n)&&B(n,t)}),e&&i.appendChild(b(e)),i},D=(i,t,e,n)=>{if(i instanceof Text&&i!==e){if(typeof t!="number")throw new Error("Offset must be a number to split text node!");if(!i.parentNode)throw new Error("Cannot split text node with no parent!");return D(i.parentNode,i.splitText(t),e,n)}let o=typeof t=="number"?t{let e=i.childNodes,n=e.length,o=[];for(;n--;){let s=e[n],r=n?e[n-1]:null;if(r&&N(s)&&xe(s,r))t.startContainer===s&&(t.startContainer=r,t.startOffset+=k(r)),t.endContainer===s&&(t.endContainer=r,t.endOffset+=k(r)),t.startContainer===i&&(t.startOffset>n?t.startOffset-=1:t.startOffset===n&&(t.startContainer=r,t.startOffset=k(r))),t.endContainer===i&&(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):o.push(S(s));else if(s instanceof Element){let l;for(;l=o.pop();)s.appendChild(l);We(s,t)}}},J=(i,t)=>{let e=i instanceof Text?i.parentNode:i;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)}},$=(i,t,e,n)=>{let o=t,s,r;for(;(s=o.parentNode)&&s!==n&&s instanceof Element&&s.childNodes.length===1;)o=s;E(o),r=i.childNodes.length;let l=i.lastChild;l&&l.nodeName==="BR"&&(i.removeChild(l),r-=1),i.appendChild(S(t)),e.setStart(i,r),e.collapse(!0),J(i,e)},M=(i,t)=>{let e=i.previousSibling,n=i.firstChild,o=i.nodeName==="LI";if(!(o&&(!n||!/^[OU]L$/.test(n.nodeName)))){if(e&&xe(e,i)){if(!j(e))if(o){let r=m("DIV");r.appendChild(S(e)),e.appendChild(r)}else return;E(i);let s=!j(i);e.appendChild(S(i)),s&&B(e,t),n&&M(n,t)}else if(o){let s=m("DIV");i.insertBefore(s,n),b(s)}}};var Ke={"font-weight":{regexp:/^bold|^700/i,replace(){return m("B")}},"font-style":{regexp:/^italic/i,replace(){return m("I")}},"font-family":{regexp:w,replace(i,t){return m("SPAN",{class:i.fontFamily,style:"font-family:"+t})}},"font-size":{regexp:w,replace(i,t){return m("SPAN",{class:i.fontSize,style:"font-size:"+t})}},"text-decoration":{regexp:/^underline/i,replace(){return m("U")}}},Nt=(i,t,e)=>{let n=i.style,o,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===i.nodeName&&d.className===i.className)continue;s||(s=d),o&&o.appendChild(d),o=d,i.style.removeProperty(r)}}return s&&o&&(o.appendChild(S(i)),i.style.cssText?i.appendChild(s):L(i,s)),o||i},pe=i=>(t,e)=>{let n=m(i),o=t.attributes;for(let s=0,r=o.length;s{let n=i,o=n.face,s=n.size,r=n.color,l=e.classNames,a,d,c,f,u;return o&&(a=m("SPAN",{class:l.fontFamily,style:"font-family:"+o}),u=a,f=a),s&&(d=m("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=m("SPAN",{class:l.color,style:"color:"+r}),u||(u=c),f&&f.appendChild(c),f=c),(!u||!f)&&(u=f=m("SPAN")),t.replaceChild(u,n),f.appendChild(S(n)),f},TT:(i,t,e)=>{let n=m("SPAN",{class:e.classNames.fontFamily,style:'font-family:menlo,consolas,"courier new",monospace'});return t.replaceChild(n,i),n.appendChild(S(i)),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=(i,t,e)=>{let n=i.childNodes,o=i;for(;N(o);)o=o.parentNode;let s=new _(o,5);for(let r=0,l=n.length;r{let t=i.childNodes,e=t.length;for(;e--;){let n=t[e];n instanceof Element&&!I(n)?(ge(n),N(n)&&!n.firstChild&&i.removeChild(n)):n instanceof Text&&!n.data&&i.removeChild(n)}},ee=(i,t,e)=>{let n=i.querySelectorAll("BR"),o=[],s=n.length;for(let r=0;ri.split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""");var le=(i,t)=>{let e=new _(t,1,H);return e.currentNode=i,e},z=(i,t)=>{let e=le(i,t).previousNode();return e!==t?e:null},U=(i,t)=>{let e=le(i,t).nextNode();return e!==t?e:null},ae=i=>!i.textContent&&!i.querySelector("IMG");var R=(i,t)=>{let e=i.startContainer,n;if(N(e))n=z(e,t);else if(e!==t&&e instanceof HTMLElement&&H(e))n=e;else{let o=he(e,i.startOffset);n=U(o,t)}return n&&K(i,n,!0)?n:null},G=(i,t)=>{let e=i.endContainer,n;if(N(e))n=z(e,t);else if(e!==t&&e instanceof HTMLElement&&H(e))n=e;else{let o=Le(e,i.endOffset);if(!o||!t.contains(o)){o=t;let s;for(;s=o.lastChild;)o=s}n=z(o,t)}return n&&K(i,n,!0)?n:null},ze=i=>i instanceof Text?w.test(i.data):i.nodeName==="IMG",Q=(i,t)=>{let e=i.startContainer,n=i.startOffset,o;if(e instanceof Text){if(n)return!1;o=e}else if(o=Le(e,n),o&&!t.contains(o)&&(o=null),!o&&(o=he(e,n),o instanceof Text&&o.length))return!1;let s=R(i,t);if(!s)return!1;let r=new _(s,5,ze);return r.currentNode=o,!r.previousNode()},ie=(i,t)=>{let e=i.endContainer,n=i.endOffset,o;if(e instanceof Text){let l=e.data.length;if(l&&n{let e=R(i,t),n=G(i,t),o;e&&n&&(o=e.parentNode,i.setStart(o,Array.from(o.childNodes).indexOf(e)),o=n.parentNode,i.setEnd(o,Array.from(o.childNodes).indexOf(n)+1))};function X(i,t,e,n){let o=document.createRange();return o.setStart(i,t),e&&typeof n=="number"?o.setEnd(e,n):o.setEnd(i,t),o}var Z=(i,t)=>{let{startContainer:e,startOffset:n,endContainer:o,endOffset:s}=i,r;if(e instanceof Text){let a=e.parentNode;if(r=a.childNodes,n===e.length)n=Array.from(r).indexOf(e)+1,i.collapsed&&(o=a,s=n);else{if(n){let d=e.splitText(n);o===e?(s-=n,o=d):o===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===o&&(s+=r.length-l),i.setStart(e,n),i.setEnd(o,s)},ye=(i,t,e)=>{let n=document.createDocumentFragment();if(i.collapsed)return n;t||(t=i.commonAncestorContainer),t instanceof Text&&(t=t.parentNode);let o=i.startContainer,s=i.startOffset,r=D(i.endContainer,i.endOffset,t,e),l=0,a=D(o,s,t,e);for(;a&&a!==r;){let d=a.nextSibling;n.appendChild(a),a=d}return o instanceof Text&&r instanceof Text&&(o.appendData(r.data),E(r),r=o,l=s),i.setStart(o,s),r?i.setEnd(r,l):i.setEnd(t,t.childNodes.length),b(t),n},Ge=(i,t,e)=>{i.currentNode=e;let n;for(;n=i[t]();){if(n instanceof Text||I(n))return n;if(!N(n))return null}return null},F=(i,t)=>{let e=R(i,t),n=G(i,t),o=e!==n;e&&n&&(C(i),P(i,e,n,t));let s=ye(i,null,t);C(i),o&&(n=G(i,t),e&&n&&e!==n&&$(e,n,i,t)),e&&b(e);let r=t.firstChild;(!r||r.nodeName==="BR")&&(b(t),t.firstChild&&i.selectNodeContents(t.firstChild)),i.collapse(!0);let l=i.startContainer,a=i.startOffset,d=new _(t,5),c=l,f=a;(!(c instanceof Text)||f===c.data.length)&&(c=Ge(d,"nextNode",c),f=0);let u=l,p=a-1;(!(u instanceof Text)||p===-1)&&(u=Ge(d,"previousPONode",c||(l instanceof Text?l:l.childNodes[a]||l)),u instanceof Text&&(p=u.data.length));let h=null,T=0;return c instanceof Text&&c.data.charAt(f)===" "&&Q(i,t)?(h=c,T=f):u instanceof Text&&u.data.charAt(p)===" "&&(c instanceof Text&&c.data.charAt(f)===" "||ie(i,t))&&(h=u,T=p),h&&h.replaceData(T,1,"\xA0"),i.setStart(l,a),i.collapse(!0),s},Ze=(i,t,e)=>{let n=t.firstChild&&N(t.firstChild),o;for(B(t,e),o=t;o=U(o,e);)b(o);i.collapsed||F(i,e),C(i),i.collapse(!1);let s=g(i.endContainer,e,"BLOCKQUOTE")||e,r=R(i,e),l=null,a=U(t,t),d=!n&&!!r&&ae(r);if(r&&a&&!d&&!g(a,t,"PRE")&&!g(a,t,"TABLE")){P(i,r,r,e),i.collapse(!0);let c=i.endContainer,f=i.endOffset;if(ee(r,e,!1),N(c)){let u=D(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();o=c.childNodes[f];)l.appendChild(o);$(c,a,i,e),f=Array.from(c.parentNode.childNodes).indexOf(c)+1,c=c.parentNode,i.setEnd(c,f)}if(k(t)){d&&r&&(i.setEndBefore(r),i.collapse(!1),E(r)),P(i,s,s,e);let c=D(i.endContainer,i.endOffset,s,e),f=c?c.previousSibling:s.lastChild;s.insertBefore(t,c),c?i.setEndBefore(c):i.setEnd(s,k(s)),r=G(i,e),C(i);let u=i.endContainer,p=i.endOffset;c&&j(c)&&M(c,e),c=f&&f.nextSibling,c&&j(c)&&M(c,e),i.setEnd(u,p)}if(l&&r){let c=i.cloneRange();$(r,l,c,e),i.setEnd(c.endContainer,c.endOffset)}C(i)};var Be=Array.prototype.indexOf,_t=(i,t,e,n,o,s)=>{let r=i.clipboardData,l=document.body,a=m("DIV"),d,c;t.childNodes.length===1&&t.childNodes[0]instanceof Text?(c=t.childNodes[0].data.replace(/ /g," "),s=!0):(a.appendChild(t),d=a.innerHTML,n&&(d=n(d))),c!==void 0||(o&&d!==void 0?c=o(d):(ee(a,e,!0),a.setAttribute("style","position:fixed;overflow:hidden;bottom:100%;right:100%;"),l.appendChild(a),c=a.innerText||a.textContent,c=c.replace(/ /g," "),l.removeChild(a))),de&&(c=c.replace(/\r?\n/g,`\r `)),!s&&d&&c!==d&&r.setData("text/html",d),r.setData("text/plain",c),i.preventDefault()},je=(i,t,e,n,o,s,r)=>{if(!se&&i.clipboardData){let l=R(t,e),a=G(t,e),d=e;l===a&&(l!=null&&l.contains(t.commonAncestorContainer))&&(d=l);let c;n?c=F(t,e):(t=t.cloneRange(),C(t),P(t,d,d,e),c=t.cloneContents());let f=t.commonAncestorContainer;for(f instanceof Text&&(f=f.parentNode);f&&f!==d;){let u=f.cloneNode(!1);u.appendChild(c),c=u,f=f.parentNode}return _t(i,c,e,o,s,r),!0}return!1},Qe=function(i){let t=this.getSelection(),e=this._root;if(t.collapsed){i.preventDefault();return}this.saveUndoState(t),je(i,t,e,!0,this._config.willCutCopy,null,!1)||setTimeout(()=>{try{this._ensureBottomLine()}catch(o){this._config.didError(o)}},0),this.setSelection(t)},Xe=function(i){je(i,this.getSelection(),this._root,!1,this._config.willCutCopy,null,!1)},Ae=function(i){this._isShiftDown=i.shiftKey},$e=function(i){let t=i.clipboardData,e=t==null?void 0:t.items,n=this._isShiftDown,o=!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"?o=!0:/^image\/.*/.test(A)&&(s=!0)}if(s&&!(o&&l)){i.preventDefault(),this.fireEvent("pasteImage",{clipboardData:t});return}if(!se){i.preventDefault(),l&&(!n||!r)?l.getAsString(O=>{this.insertHTML(O,!0)}):r&&r.getAsString(O=>{let A=!1,we=this.getSelection();if(!we.collapsed&&w.test(we.toString())){let Ie=this.linkRegExp.exec(O);A=!!Ie&&Ie[0].length===O.length}A?this.makeLink(O):this.insertPlainText(O,!0)});return}}let a=t==null?void 0:t.types;if(!se&&a&&(Be.call(a,"text/html")>-1||!Fe&&Be.call(a,"text/plain")>-1&&Be.call(a,"text/rtf")<0)){i.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,p=c.endContainer,h=c.endOffset,T=m("DIV",{contenteditable:"true",style:"position:fixed; overflow:hidden; top:0; right:100%; width:1px; height:1px;"});d.appendChild(T),c.selectNodeContents(T),this.setSelection(c),setTimeout(()=>{try{let v="",O=T,A;for(;T=O;)O=T.nextSibling,E(T),A=T.firstChild,A&&A===T.lastChild&&A instanceof HTMLDivElement&&(T=A),v+=T.innerHTML;this.setSelection(X(f,u,p,h)),v&&this.insertHTML(v,!0)}catch(v){this._config.didError(v)}},0)},Ve=function(i){if(!i.dataTransfer)return;let t=i.dataTransfer.types,e=t.length,n=!1,o=!1;for(;e--;)switch(t[e]){case"text/plain":n=!0;break;case"text/html":o=!0;break;default:return}(o||n&&this.saveUndoState)&&this.saveUndoState()};var De=(i,t,e)=>{t.preventDefault(),i.splitBlock(t.shiftKey,e)};var te=(i,t)=>{try{t||(t=i.getSelection());let e=t.startContainer;e instanceof Text&&(e=e.parentNode);let n=e;for(;N(n)&&(!n.textContent||n.textContent===W);)e=n,n=e.parentNode;e!==n&&(t.setStart(n,Array.from(n.childNodes).indexOf(e)),t.collapse(!0),n.removeChild(e),H(n)||(n=z(n,i._root)||i._root),b(n),C(t)),e===i._root&&(e=e.firstChild)&&e.nodeName==="BR"&&E(e),i._ensureBottomLine(),i.setSelection(t),i._updatePath(t,!0)}catch(e){i._config.didError(e)}},Se=(i,t)=>{let e;for(;(e=i.parentNode)&&!(e===t||e.isContentEditable);)i=e;E(i)},Ee=(i,t,e)=>{if(g(t,i._root,"A"))return;let n=t.data||"",o=Math.max(n.lastIndexOf(" ",e-1),n.lastIndexOf("\xA0",e-1))+1,s=n.slice(o,e),r=i.linkRegExp.exec(s);if(r){let l=i.getSelection();i._docWasChanged(),i._recordUndoState(l),i._getRangeAndRemoveBookmark(l);let a=o+r.index,d=a+r[0].length;a&&(t=t.splitText(a));let c=i._config.tagAttributes.a,f=m("A",Object.assign({href:r[1]?/^(?:ht|f)tps?:/i.test(r[1])?r[1]:"http://"+r[1]:"mailto:"+r[0]},c));f.textContent=n.slice(a,d),t.parentNode.insertBefore(f,t);let u=l.startOffset;if(t.data=n.slice(d),l.startContainer===t){let p=u-d;l.setStart(t,p),l.setEnd(t,p)}i.setSelection(l)}};var Ye=(i,t,e)=>{let n=i._root;if(i._removeZWS(),i.saveUndoState(e),!e.collapsed)t.preventDefault(),F(e,n),te(i,e);else if(Q(e,n)){t.preventDefault();let o=R(e,n);if(!o)return;let s=o;B(s.parentNode,n);let r=z(s,n);if(r){if(!r.isContentEditable){Se(r,n);return}for($(r,s,e,n),s=r.parentNode;s!==n&&!s.nextSibling;)s=s.parentNode;s!==n&&(s=s.nextSibling)&&M(s,n),i.setSelection(e)}else if(s){if(g(s,n,"UL")||g(s,n,"OL")){i.decreaseListLevel(e);return}else if(g(s,n,"BLOCKQUOTE")){i.removeQuote(e);return}i.setSelection(e),i._updatePath(e,!0)}}else{C(e);let o=e.startContainer,s=e.startOffset,r=o.parentNode;o instanceof Text&&r instanceof HTMLAnchorElement&&s&&r.href.includes(o.data)?(o.deleteData(s-1,1),i.setSelection(e),i.removeLink()):(i.setSelection(e),setTimeout(()=>{te(i)},0))}};var Je=(i,t,e)=>{let n=i._root,o,s,r,l,a,d;if(i._removeZWS(),i.saveUndoState(e),!e.collapsed)t.preventDefault(),F(e,n),te(i,e);else if(ie(e,n)){if(t.preventDefault(),o=R(e,n),!o)return;if(B(o.parentNode,n),s=U(o,n),s){if(!s.isContentEditable){Se(s,n);return}for($(o,s,e,n),s=o.parentNode;s!==n&&!s.nextSibling;)s=s.parentNode;s!==n&&(s=s.nextSibling)&&M(s,n),i.setSelection(e),i._updatePath(e,!0)}}else{if(r=e.cloneRange(),P(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),C(e),te(i,e);return}i.setSelection(r),setTimeout(()=>{te(i)},0)}};var et=(i,t,e)=>{let n=i._root;if(i._removeZWS(),e.collapsed&&Q(e,n)){let o=R(e,n),s;for(;s=o.parentNode;){if(s.nodeName==="UL"||s.nodeName==="OL"){t.preventDefault(),i.increaseListLevel(e);break}o=s}}},tt=(i,t,e)=>{let n=i._root;if(i._removeZWS(),e.collapsed&&Q(e,n)){let o=e.startContainer;(g(o,n,"UL")||g(o,n,"OL"))&&(t.preventDefault(),i.decreaseListLevel(e))}};var nt=(i,t,e)=>{let n,o=i._root;if(i._recordUndoState(e),i._getRangeAndRemoveBookmark(e),e.collapsed||(F(e,o),i._ensureBottomLine(),i.setSelection(e),i._updatePath(e,!0)),n=e.endContainer,e.endOffset===k(n))do if(n.nodeName==="A"){e.setStartAfter(n);break}while(!n.nextSibling&&(n=n.parentNode)&&n!==o);if(i._config.addLinks){let s=e.cloneRange();C(s);let r=s.startContainer,l=s.startOffset;setTimeout(()=>{Ee(i,r,l)},0)}i.setSelection(e)};var bt={8:"Backspace",9:"Tab",13:"Enter",27:"Escape",32:"Space",33:"PageUp",34:"PageDown",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",46:"Delete",191:"/",219:"[",220:"\\",221:"]"},it=function(i){let t=i.keyCode,e=bt[t],n="",o=this.getSelection();i.defaultPrevented||(e||(e=String.fromCharCode(t).toLowerCase(),/^[A-Za-z0-9]$/.test(e)||(e="")),111{i.moveCursorToStart()},x.PageDown=i=>{i.moveCursorToEnd()});var oe=(i,t)=>(t=t||null,(e,n)=>{n.preventDefault();let o=e.getSelection();e.hasFormat(i,null,o)?e.changeFormat(null,{tag:i},o):e.changeFormat({tag:i},t,o)});x[y+"b"]=oe("B");x[y+"i"]=oe("I");x[y+"u"]=oe("U");x[y+"Shift-7"]=oe("S");x[y+"Shift-5"]=oe("SUB",{tag:"SUP"});x[y+"Shift-6"]=oe("SUP",{tag:"SUB"});x[y+"Shift-8"]=(i,t)=>{t.preventDefault();let e=i.getPath();/(?:^|>)UL/.test(e)?i.removeList():i.makeUnorderedList()};x[y+"Shift-9"]=(i,t)=>{t.preventDefault();let e=i.getPath();/(?:^|>)OL/.test(e)?i.removeList():i.makeOrderedList()};x[y+"["]=(i,t)=>{t.preventDefault();let e=i.getPath();/(?:^|>)BLOCKQUOTE/.test(e)||!/(?:^|>)[OU]L/.test(e)?i.decreaseQuoteLevel():i.decreaseListLevel()};x[y+"]"]=(i,t)=>{t.preventDefault();let e=i.getPath();/(?:^|>)BLOCKQUOTE/.test(e)||!/(?:^|>)[OU]L/.test(e)?i.increaseQuoteLevel():i.increaseListLevel()};x[y+"d"]=(i,t)=>{t.preventDefault(),i.toggleCode()};x[y+"z"]=(i,t)=>{t.preventDefault(),i.undo()};x[y+"y"]=x[y+"Shift-z"]=(i,t)=>{t.preventDefault(),i.redo()};var Te=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=X(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",it),this._keyHandlers=Object.create(x);let n=new MutationObserver(()=>this._docWasChanged());n.observe(t,{childList:!0,attributes:!0,characterData:!0,subtree:!0}),this._mutation=n,t.setAttribute("contenteditable","true");try{document.execCommand("enableObjectResizing",!1,"false"),document.execCommand("enableInlineTableEditing",!1,"false")}catch(o){}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,sanitizeToDOMFragment:n=>{let o=DOMPurify.sanitize(n,{ALLOW_UNKNOWN_PROTOCOLS:!0,WHOLE_DOCUMENT:!1,RETURN_DOM:!0,RETURN_DOM_FRAGMENT:!0,FORCE_BODY:!1});return o?document.importNode(o,!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":Me&&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 o=this._root===document.activeElement;if(t==="focus"){if(!o||this._isFocused)return this;this._isFocused=!0}else{if(o||!this._isFocused)return this;this._isFocused=!1}}if(n){let o=e instanceof Event?e:new CustomEvent(t,{detail:e});n=n.slice();for(let s of n)try{"handleEvent"in s?s.handleEvent(o):s.call(this,o)}catch(r){this._config.didError(r)}}return this}addEventListener(t,e){let n=this._events.get(t),o=this._root;return n||(n=[],this._events.set(t,n),this.customEvents.has(t)||(t==="selectionchange"&&(o=document),o.addEventListener(t,this,!0))),n.push(e),this}removeEventListener(t,e){let n=this._events.get(t),o=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"&&(o=document),o.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&&(me(this._root),this._mayHaveZWS=!1)}_saveRangeToBookmark(t){let e=m("INPUT",{id:this.startSelectionId,type:"hidden"}),n=m("INPUT",{id:this.endSelectionId,type:"hidden"}),o;Z(t,e),t.collapse(!1),Z(t,n),e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING&&(e.id=this.endSelectionId,n.id=this.startSelectionId,o=e,e=n,n=o),t.setStartAfter(e),t.setEndBefore(n)}_getRangeAndRemoveBookmark(t){let e=this._root,n=e.querySelector("#"+this.startSelectionId),o=e.querySelector("#"+this.endSelectionId);if(n&&o){let s=n.parentNode,r=o.parentNode,l=Array.from(s.childNodes).indexOf(n),a=Array.from(r.childNodes).indexOf(o);s===r&&(a-=1),n.remove(),o.remove(),t||(t=document.createRange()),t.setStart(s,l),t.setEnd(r,a),J(s,t),s!==r&&J(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 o=n.startContainer,s=n.endContainer;o&&I(o)&&n.setStartBefore(o),s&&I(s)&&n.setEndBefore(s)}return n&&e.contains(n.commonAncestorContainer)?this._lastSelection=n:(n=this._lastSelection,document.contains(n.commonAncestorContainer)||(n=null)),n||(n=X(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=X(e,t?0:e.childNodes.length);return C(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=m("SPAN");n.textContent=W,Z(t,n),e=n.getBoundingClientRect();let o=n.parentNode;o.removeChild(n),J(o,t)}return e}getPath(){return this._path}_updatePathOnEvent(){this._isFocused&&this._updatePath(this.getSelection())}_updatePath(t,e){let n=t.startContainer,o=t.endContainer,s;(e||n!==this._lastAnchorNode||o!==this._lastFocusNode)&&(this._lastAnchorNode=n,this._lastFocusNode=o,s=n&&o?n===o?this._getPath(o):"(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,o="";if(t&&t!==e){let s=t.parentNode;if(o=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;o+=(o?">":"")+t.nodeName,r&&(o+="#"+r),a.length&&(o+=".",o+=a.join(".")),d&&(o+="[dir="+d+"]"),l.contains(c.highlight)&&(o+="[backgroundColor="+t.style.backgroundColor.replace(/ /g,"")+"]"),l.contains(c.color)&&(o+="[color="+t.style.color.replace(/ /g,"")+"]"),l.contains(c.fontFamily)&&(o+="[fontFamily="+t.style.fontFamily.replace(/ /g,"")+"]"),l.contains(c.fontSize)&&(o+="[fontSize="+t.style.fontSize+"]")}}return o}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){if(!this._isInUndoState||e){let n=this._undoIndex,o=this._undoStack,s=this._config.undo,r=s.documentSizeThreshold,l=s.undoLimit;e||(n+=1),n-1&&a.length*2>r&&l>-1&&n>l&&(o.splice(0,n-l),n=l,this._undoStackLength=l),o[n]=a,this._undoIndex=n,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}redo(){let t=this._undoIndex,e=this._undoStackLength;if(t+1",d="<"+r;for(let c in l)d+=" "+c+'="'+Oe(l[c])+'"';d+=">";for(let c=0,f=o.length;c")+a),o[c]=u}return this.insertHTML(o.join(""),e)}getSelectedText(){let t=this.getSelection();if(t.collapsed)return"";let e=t.startContainer,n=t.endContainer,o=new _(t.commonAncestorContainer,5,d=>K(t,d,!0));o.currentNode=e;let s=e,r="",l=!1,a;for((!(s instanceof Element)&&!(s instanceof Text)||!o.filter(s))&&(s=o.nextNode());s;)s instanceof Text?(a=s.data,a&&/\S/.test(a)&&(s===n&&(a=a.slice(0,t.endOffset)),s===e&&(a=a.slice(t.startOffset)),r+=a,l=!0)):(s.nodeName==="BR"||l&&!N(s))&&(r+=` diff --git a/dist/squire.js.map b/dist/squire.js.map index 7be16cee..1a4a6e8a 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/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): 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 let prev = child.previousSibling;\n // If we have an empty text node next to another text node,\n // just skip and remove it.\n while (\n child instanceof Text &&\n !child.length &&\n prev &&\n prev instanceof Text\n ) {\n child.remove();\n child = prev;\n continue;\n }\n if (child instanceof Text) {\n startContainer = child;\n startOffset = child.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 (node instanceof Element && !node.querySelector('BR')) {\n fixer = createElement('BR');\n let parent: Element = node;\n let child: Element | null;\n while ((child = parent.lastElementChild) && !isInline(child)) {\n parent = child;\n }\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 const children = container.childNodes;\n let wrapper: HTMLElement | null = null;\n for (let i = 0, l = children.length; i < l; i += 1) {\n const child = children[i];\n const isBR = child.nodeName === 'BR';\n if (!isBR && isInline(child)) {\n if (!wrapper) {\n wrapper = createElement('DIV');\n }\n wrapper.appendChild(child);\n i -= 1;\n l -= 1;\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 i += 1;\n l += 1;\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 { 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 if (startOffset) {\n return false;\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\n if (endContainer instanceof Text) {\n const length = endContainer.data.length;\n if (length && endOffset < length) {\n return false;\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 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 { cleanupBRs } from './Clean';\nimport { 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';\n\n// ---\n\nconst indexOf = Array.prototype.indexOf;\n\n// The (non-standard but supported enough) innerText property is based on the\n// render tree in Firefox and possibly other browsers, so we must insert the\n// DOM node into the document to ensure the text part is correct.\nconst setClipboardData = (\n event: ClipboardEvent,\n contents: Node,\n root: HTMLElement,\n toCleanHTML: null | ((html: string) => string),\n toPlainText: null | ((html: string) => string),\n plainTextOnly: boolean,\n): void => {\n const clipboardData = event.clipboardData!;\n const body = document.body;\n const node = createElement('DIV') as HTMLDivElement;\n let html: string | undefined;\n let text: string | undefined;\n\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 node.appendChild(contents);\n html = node.innerHTML;\n if (toCleanHTML) {\n html = toCleanHTML(html);\n }\n }\n\n if (text !== undefined) {\n // Do nothing; we were copying plain text to start\n } else if (toPlainText && html !== undefined) {\n text = toPlainText(html);\n } else {\n // Firefox will add an extra new line for BRs at the end of block when\n // calculating innerText, even though they don't actually affect\n // display, so we need to remove them first.\n cleanupBRs(node, root, true);\n node.setAttribute(\n 'style',\n 'position:fixed;overflow:hidden;bottom:100%;right:100%;',\n );\n body.appendChild(node);\n text = node.innerText || node.textContent!;\n // Replace nbsp with regular space\n // eslint-disable-next-line no-irregular-whitespace\n text = text.replace(/\u00A0/g, ' ');\n body.removeChild(node);\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 if (!plainTextOnly && html && text !== html) {\n clipboardData.setData('text/html', html);\n }\n clipboardData.setData('text/plain', text);\n event.preventDefault();\n};\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 if (!isLegacyEdge && event.clipboardData) {\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 // 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 // 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 // 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 // Set clipboard data\n setClipboardData(\n event,\n contents,\n root,\n toCleanHTML,\n toPlainText,\n plainTextOnly,\n );\n return true;\n }\n return false;\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 null,\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 null,\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 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\n const startOffset = selection.startOffset;\n textNode.data = data.slice(endIndex);\n if (selection.startContainer === textNode) {\n const newOffset = startOffset - endIndex;\n selection.setStart(textNode, newOffset);\n selection.setEnd(textNode, newOffset);\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 } 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 { getLength } from '../node/Node';\nimport { moveRangeBoundariesDownTree } from '../range/Boundaries';\nimport { deleteContentsOfRange } from '../range/InsertDelete';\n\nimport type { Squire } from '../Editor';\nimport { linkifyText } from './KeyHelpers';\n\n// ---\n\nconst Space = (self: Squire, _: 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 }\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';\n\n// ---\n\nconst keys: Record = {\n 8: 'Backspace',\n 9: 'Tab',\n 13: 'Enter',\n 27: 'Escape',\n 32: 'Space',\n 33: 'PageUp',\n 34: 'PageDown',\n 37: 'ArrowLeft',\n 38: 'ArrowUp',\n 39: 'ArrowRight',\n 40: 'ArrowDown',\n 46: 'Delete',\n 191: '/',\n 219: '[',\n 220: '\\\\',\n 221: ']',\n};\n\n// Ref: http://unixpapa.com/js/key.html\nconst _onKey = function (this: Squire, event: KeyboardEvent): void {\n const code = event.keyCode;\n let key = keys[code];\n let modifiers = '';\n const range: Range = this.getSelection();\n\n if (event.defaultPrevented) {\n return;\n }\n\n if (!key) {\n key = String.fromCharCode(code).toLowerCase();\n // Only reliable for letters and numbers\n if (!/^[A-Za-z0-9]$/.test(key)) {\n key = '';\n }\n }\n\n // Function keys\n if (111 < code && code < 124) {\n key = 'F' + (code - 111);\n }\n\n // We need to apply the Backspace/delete handlers regardless of\n // control key 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\n key = modifiers + key;\n\n if (this._keyHandlers[key]) {\n this._keyHandlers[key](this, event, range);\n } else if (\n !range.collapsed &&\n // !event.isComposing stops us from blatting Kana-Kanji conversion in\n // Safari\n !event.isComposing &&\n !event.ctrlKey &&\n !event.metaKey &&\n (event.key || 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': Space,\n 'ArrowLeft'(self: Squire): void {\n self._removeZWS();\n },\n 'ArrowRight'(self: Squire): void {\n self._removeZWS();\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';\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 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?: object) {\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 // Remove Firefox's built-in controls\n try {\n document.execCommand('enableObjectResizing', false, 'false');\n document.execCommand('enableInlineTableEditing', false, 'false');\n } catch (_) {}\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 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: number, 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 Grammerly 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 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 // Don't record if we're already in an undo state\n if (!this._isInUndoState || replace) {\n // Advance pointer to new position\n let undoIndex = this._undoIndex;\n const undoStack = this._undoStack;\n const undoConfig = this._config.undo;\n const undoThreshold = undoConfig.documentSizeThreshold;\n const undoLimit = undoConfig.undoLimit;\n\n if (!replace) {\n undoIndex += 1;\n }\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 // Get data\n if (range) {\n this._saveRangeToBookmark(range);\n }\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 (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;\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;\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 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 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(): string {\n const range = this.getSelection();\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\n return textContent;\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 | 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 // 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