BVB Source Codes

Squire Show squire-raw.js Source code

Return Download Squire: download squire-raw.js Source code - Download Squire Source code - Type:.js
  1. /* Copyright 漏 2011-2015 by Neil Jenkins. MIT Licensed. */
  2.  
  3. ( function ( doc, undefined ) {
  4.  
  5. "use strict";
  6.  
  7. var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING
  8. var ELEMENT_NODE = 1;                // Node.ELEMENT_NODE;
  9. var TEXT_NODE = 3;                   // Node.TEXT_NODE;
  10. var DOCUMENT_NODE = 9;               // Node.DOCUMENT_NODE;
  11. var DOCUMENT_FRAGMENT_NODE = 11;     // Node.DOCUMENT_FRAGMENT_NODE;
  12. var SHOW_ELEMENT = 1;                // NodeFilter.SHOW_ELEMENT;
  13. var SHOW_TEXT = 4;                   // NodeFilter.SHOW_TEXT;
  14.  
  15. var START_TO_START = 0; // Range.START_TO_START
  16. var START_TO_END = 1;   // Range.START_TO_END
  17. var END_TO_END = 2;     // Range.END_TO_END
  18. var END_TO_START = 3;   // Range.END_TO_START
  19.  
  20. var HIGHLIGHT_CLASS = 'highlight';
  21. var COLOUR_CLASS = 'colour';
  22. var FONT_FAMILY_CLASS = 'font';
  23. var FONT_SIZE_CLASS = 'size';
  24.  
  25. var ZWS = '\u200B';
  26.  
  27. var win = doc.defaultView;
  28.  
  29. var ua = navigator.userAgent;
  30.  
  31. var isAndroid = /Android/.test( ua );
  32. var isIOS = /iP(?:ad|hone|od)/.test( ua );
  33. var isMac = /Mac OS X/.test( ua );
  34. var isWin = /Windows NT/.test( ua );
  35.  
  36. var isGecko = /Gecko\//.test( ua );
  37. var isIElt11 = /Trident\/[456]\./.test( ua );
  38. var isPresto = !!win.opera;
  39. var isEdge = /Edge\//.test( ua );
  40. var isWebKit = !isEdge && /WebKit\//.test( ua );
  41. var isIE = /Trident\/[4567]\./.test( ua );
  42.  
  43. var ctrlKey = isMac ? 'meta-' : 'ctrl-';
  44.  
  45. var useTextFixer = isIElt11 || isPresto;
  46. var cantFocusEmptyTextNodes = isIElt11 || isWebKit;
  47. var losesSelectionOnBlur = isIElt11;
  48.  
  49. var canObserveMutations = typeof MutationObserver !== 'undefined';
  50. var canWeakMap = typeof WeakMap !== 'undefined';
  51.  
  52. // Use [^ \t\r\n] instead of \S so that nbsp does not count as white-space
  53. var notWS = /[^ \t\r\n]/;
  54.  
  55. var indexOf = Array.prototype.indexOf;
  56.  
  57. // Polyfill for FF3.5
  58. if ( !Object.create ) {
  59.     Object.create = function ( proto ) {
  60.         var F = function () {};
  61.         F.prototype = proto;
  62.         return new F();
  63.     };
  64. }
  65.  
  66. /*
  67.     Native TreeWalker is buggy in IE and Opera:
  68.     * IE9/10 sometimes throw errors when calling TreeWalker#nextNode or
  69.       TreeWalker#previousNode. No way to feature detect this.
  70.     * Some versions of Opera have a bug in TreeWalker#previousNode which makes
  71.       it skip to the wrong node.
  72.  
  73.     Rather than risk further bugs, it's easiest just to implement our own
  74.     (subset) of the spec in all browsers.
  75. */
  76.  
  77. var typeToBitArray = {
  78.     // ELEMENT_NODE
  79.     1: 1,
  80.     // ATTRIBUTE_NODE
  81.     2: 2,
  82.     // TEXT_NODE
  83.     3: 4,
  84.     // COMMENT_NODE
  85.     8: 128,
  86.     // DOCUMENT_NODE
  87.     9: 256,
  88.     // DOCUMENT_FRAGMENT_NODE
  89.     11: 1024
  90. };
  91.  
  92. function TreeWalker ( root, nodeType, filter ) {
  93.     this.root = this.currentNode = root;
  94.     this.nodeType = nodeType;
  95.     this.filter = filter;
  96. }
  97.  
  98. TreeWalker.prototype.nextNode = function () {
  99.     var current = this.currentNode,
  100.         root = this.root,
  101.         nodeType = this.nodeType,
  102.         filter = this.filter,
  103.         node;
  104.     while ( true ) {
  105.         node = current.firstChild;
  106.         while ( !node && current ) {
  107.             if ( current === root ) {
  108.                 break;
  109.             }
  110.             node = current.nextSibling;
  111.             if ( !node ) { current = current.parentNode; }
  112.         }
  113.         if ( !node ) {
  114.             return null;
  115.         }
  116.         if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
  117.                 filter( node ) ) {
  118.             this.currentNode = node;
  119.             return node;
  120.         }
  121.         current = node;
  122.     }
  123. };
  124.  
  125. TreeWalker.prototype.previousNode = function () {
  126.     var current = this.currentNode,
  127.         root = this.root,
  128.         nodeType = this.nodeType,
  129.         filter = this.filter,
  130.         node;
  131.     while ( true ) {
  132.         if ( current === root ) {
  133.             return null;
  134.         }
  135.         node = current.previousSibling;
  136.         if ( node ) {
  137.             while ( current = node.lastChild ) {
  138.                 node = current;
  139.             }
  140.         } else {
  141.             node = current.parentNode;
  142.         }
  143.         if ( !node ) {
  144.             return null;
  145.         }
  146.         if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
  147.                 filter( node ) ) {
  148.             this.currentNode = node;
  149.             return node;
  150.         }
  151.         current = node;
  152.     }
  153. };
  154.  
  155. // Previous node in post-order.
  156. TreeWalker.prototype.previousPONode = function () {
  157.     var current = this.currentNode,
  158.         root = this.root,
  159.         nodeType = this.nodeType,
  160.         filter = this.filter,
  161.         node;
  162.     while ( true ) {
  163.         node = current.lastChild;
  164.         while ( !node && current ) {
  165.             if ( current === root ) {
  166.                 break;
  167.             }
  168.             node = current.previousSibling;
  169.             if ( !node ) { current = current.parentNode; }
  170.         }
  171.         if ( !node ) {
  172.             return null;
  173.         }
  174.         if ( ( typeToBitArray[ node.nodeType ] & nodeType ) &&
  175.                 filter( node ) ) {
  176.             this.currentNode = node;
  177.             return node;
  178.         }
  179.         current = node;
  180.     }
  181. };
  182.  
  183. var inlineNodeNames  = /^(?:#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)$/;
  184.  
  185. var leafNodeNames = {
  186.     BR: 1,
  187.     HR: 1,
  188.     IFRAME: 1,
  189.     IMG: 1,
  190.     INPUT: 1
  191. };
  192.  
  193. function every ( nodeList, fn ) {
  194.     var l = nodeList.length;
  195.     while ( l-- ) {
  196.         if ( !fn( nodeList[l] ) ) {
  197.             return false;
  198.         }
  199.     }
  200.     return true;
  201. }
  202.  
  203. // ---
  204.  
  205. var UNKNOWN = 0;
  206. var INLINE = 1;
  207. var BLOCK = 2;
  208. var CONTAINER = 3;
  209.  
  210. var nodeCategoryCache = canWeakMap ? new WeakMap() : null;
  211.  
  212. function isLeaf ( node ) {
  213.     return node.nodeType === ELEMENT_NODE && !!leafNodeNames[ node.nodeName ];
  214. }
  215. function getNodeCategory ( node ) {
  216.     switch ( node.nodeType ) {
  217.     case TEXT_NODE:
  218.         return INLINE;
  219.     case ELEMENT_NODE:
  220.     case DOCUMENT_FRAGMENT_NODE:
  221.         if ( canWeakMap && nodeCategoryCache.has( node ) ) {
  222.             return nodeCategoryCache.get( node );
  223.         }
  224.         break;
  225.     default:
  226.         return UNKNOWN;
  227.     }
  228.  
  229.     var nodeCategory;
  230.     if ( !every( node.childNodes, isInline ) ) {
  231.         // Malformed HTML can have block tags inside inline tags. Need to treat
  232.         // these as containers rather than inline. See #239.
  233.         nodeCategory = CONTAINER;
  234.     } else if ( inlineNodeNames.test( node.nodeName ) ) {
  235.         nodeCategory = INLINE;
  236.     } else {
  237.         nodeCategory = BLOCK;
  238.     }
  239.     if ( canWeakMap ) {
  240.         nodeCategoryCache.set( node, nodeCategory );
  241.     }
  242.     return nodeCategory;
  243. }
  244. function isInline ( node ) {
  245.     return getNodeCategory( node ) === INLINE;
  246. }
  247. function isBlock ( node ) {
  248.     return getNodeCategory( node ) === BLOCK;
  249. }
  250. function isContainer ( node ) {
  251.     return getNodeCategory( node ) === CONTAINER;
  252. }
  253.  
  254. function getBlockWalker ( node, root ) {
  255.     var walker = new TreeWalker( root, SHOW_ELEMENT, isBlock );
  256.     walker.currentNode = node;
  257.     return walker;
  258. }
  259. function getPreviousBlock ( node, root ) {
  260.     node = getBlockWalker( node, root ).previousNode();
  261.     return node !== root ? node : null;
  262. }
  263. function getNextBlock ( node, root ) {
  264.     node = getBlockWalker( node, root ).nextNode();
  265.     return node !== root ? node : null;
  266. }
  267.  
  268. function areAlike ( node, node2 ) {
  269.     return !isLeaf( node ) && (
  270.         node.nodeType === node2.nodeType &&
  271.         node.nodeName === node2.nodeName &&
  272.         node.nodeName !== 'A' &&
  273.         node.className === node2.className &&
  274.         ( ( !node.style && !node2.style ) ||
  275.           node.style.cssText === node2.style.cssText )
  276.     );
  277. }
  278. function hasTagAttributes ( node, tag, attributes ) {
  279.     if ( node.nodeName !== tag ) {
  280.         return false;
  281.     }
  282.     for ( var attr in attributes ) {
  283.         if ( node.getAttribute( attr ) !== attributes[ attr ] ) {
  284.             return false;
  285.         }
  286.     }
  287.     return true;
  288. }
  289. function getNearest ( node, root, tag, attributes ) {
  290.     while ( node && node !== root ) {
  291.         if ( hasTagAttributes( node, tag, attributes ) ) {
  292.             return node;
  293.         }
  294.         node = node.parentNode;
  295.     }
  296.     return null;
  297. }
  298. function isOrContains ( parent, node ) {
  299.     while ( node ) {
  300.         if ( node === parent ) {
  301.             return true;
  302.         }
  303.         node = node.parentNode;
  304.     }
  305.     return false;
  306. }
  307.  
  308. function getPath ( node, root ) {
  309.     var path = '';
  310.     var id, className, classNames, dir;
  311.     if ( node && node !== root ) {
  312.         path = getPath( node.parentNode, root );
  313.         if ( node.nodeType === ELEMENT_NODE ) {
  314.             path += ( path ? '>' : '' ) + node.nodeName;
  315.             if ( id = node.id ) {
  316.                 path += '#' + id;
  317.             }
  318.             if ( className = node.className.trim() ) {
  319.                 classNames = className.split( /\s\s*/ );
  320.                 classNames.sort();
  321.                 path += '.';
  322.                 path += classNames.join( '.' );
  323.             }
  324.             if ( dir = node.dir ) {
  325.                 path += '[dir=' + dir + ']';
  326.             }
  327.             if ( classNames ) {
  328.                 if ( indexOf.call( classNames, HIGHLIGHT_CLASS ) > -1 ) {
  329.                     path += '[backgroundColor=' +
  330.                         node.style.backgroundColor.replace( / /g,'' ) + ']';
  331.                 }
  332.                 if ( indexOf.call( classNames, COLOUR_CLASS ) > -1 ) {
  333.                     path += '[color=' +
  334.                         node.style.color.replace( / /g,'' ) + ']';
  335.                 }
  336.                 if ( indexOf.call( classNames, FONT_FAMILY_CLASS ) > -1 ) {
  337.                     path += '[fontFamily=' +
  338.                         node.style.fontFamily.replace( / /g,'' ) + ']';
  339.                 }
  340.                 if ( indexOf.call( classNames, FONT_SIZE_CLASS ) > -1 ) {
  341.                     path += '[fontSize=' + node.style.fontSize + ']';
  342.                 }
  343.             }
  344.         }
  345.     }
  346.     return path;
  347. }
  348.  
  349. function getLength ( node ) {
  350.     var nodeType = node.nodeType;
  351.     return nodeType === ELEMENT_NODE ?
  352.         node.childNodes.length : node.length || 0;
  353. }
  354.  
  355. function detach ( node ) {
  356.     var parent = node.parentNode;
  357.     if ( parent ) {
  358.         parent.removeChild( node );
  359.     }
  360.     return node;
  361. }
  362. function replaceWith ( node, node2 ) {
  363.     var parent = node.parentNode;
  364.     if ( parent ) {
  365.         parent.replaceChild( node2, node );
  366.     }
  367. }
  368. function empty ( node ) {
  369.     var frag = node.ownerDocument.createDocumentFragment(),
  370.         childNodes = node.childNodes,
  371.         l = childNodes ? childNodes.length : 0;
  372.     while ( l-- ) {
  373.         frag.appendChild( node.firstChild );
  374.     }
  375.     return frag;
  376. }
  377.  
  378. function createElement ( doc, tag, props, children ) {
  379.     var el = doc.createElement( tag ),
  380.         attr, value, i, l;
  381.     if ( props instanceof Array ) {
  382.         children = props;
  383.         props = null;
  384.     }
  385.     if ( props ) {
  386.         for ( attr in props ) {
  387.             value = props[ attr ];
  388.             if ( value !== undefined ) {
  389.                 el.setAttribute( attr, props[ attr ] );
  390.             }
  391.         }
  392.     }
  393.     if ( children ) {
  394.         for ( i = 0, l = children.length; i < l; i += 1 ) {
  395.             el.appendChild( children[i] );
  396.         }
  397.     }
  398.     return el;
  399. }
  400.  
  401. function fixCursor ( node, root ) {
  402.     // In Webkit and Gecko, block level elements are collapsed and
  403.     // unfocussable if they have no content. To remedy this, a <BR> must be
  404.     // inserted. In Opera and IE, we just need a textnode in order for the
  405.     // cursor to appear.
  406.     var self = root.__squire__;
  407.     var doc = node.ownerDocument;
  408.     var originalNode = node;
  409.     var fixer, child;
  410.  
  411.     if ( node === root ) {
  412.         if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) {
  413.             fixer = self.createDefaultBlock();
  414.             if ( child ) {
  415.                 node.replaceChild( fixer, child );
  416.             }
  417.             else {
  418.                 node.appendChild( fixer );
  419.             }
  420.             node = fixer;
  421.             fixer = null;
  422.         }
  423.     }
  424.  
  425.     if ( node.nodeType === TEXT_NODE ) {
  426.         return originalNode;
  427.     }
  428.  
  429.     if ( isInline( node ) ) {
  430.         child = node.firstChild;
  431.         while ( cantFocusEmptyTextNodes && child &&
  432.                 child.nodeType === TEXT_NODE && !child.data ) {
  433.             node.removeChild( child );
  434.             child = node.firstChild;
  435.         }
  436.         if ( !child ) {
  437.             if ( cantFocusEmptyTextNodes ) {
  438.                 fixer = doc.createTextNode( ZWS );
  439.                 self._didAddZWS();
  440.             } else {
  441.                 fixer = doc.createTextNode( '' );
  442.             }
  443.         }
  444.     } else {
  445.         if ( useTextFixer ) {
  446.             while ( node.nodeType !== TEXT_NODE && !isLeaf( node ) ) {
  447.                 child = node.firstChild;
  448.                 if ( !child ) {
  449.                     fixer = doc.createTextNode( '' );
  450.                     break;
  451.                 }
  452.                 node = child;
  453.             }
  454.             if ( node.nodeType === TEXT_NODE ) {
  455.                 // Opera will collapse the block element if it contains
  456.                 // just spaces (but not if it contains no data at all).
  457.                 if ( /^ +$/.test( node.data ) ) {
  458.                     node.data = '';
  459.                 }
  460.             } else if ( isLeaf( node ) ) {
  461.                 node.parentNode.insertBefore( doc.createTextNode( '' ), node );
  462.             }
  463.         }
  464.         else if ( !node.querySelector( 'BR' ) ) {
  465.             fixer = createElement( doc, 'BR' );
  466.             while ( ( child = node.lastElementChild ) && !isInline( child ) ) {
  467.                 node = child;
  468.             }
  469.         }
  470.     }
  471.     if ( fixer ) {
  472.         try {
  473.             node.appendChild( fixer );
  474.         } catch ( error ) {
  475.             self.didError({
  476.                 name: 'Squire: fixCursor 鈥撀? + error,
  477.                message: 'Parent: ' + node.nodeName + '/' + node.innerHTML +
  478.                    ' appendChild: ' + fixer.nodeName
  479.            });
  480.        }
  481.    }
  482.  
  483.    return originalNode;
  484. }
  485.  
  486. // Recursively examine container nodes and wrap any inline children.
  487. function fixContainer ( container, root ) {
  488.    var children = container.childNodes;
  489.    var doc = container.ownerDocument;
  490.    var wrapper = null;
  491.    var i, l, child, isBR;
  492.    var config = root.__squire__._config;
  493.  
  494.    for ( i = 0, l = children.length; i < l; i += 1 ) {
  495.        child = children[i];
  496.        isBR = child.nodeName === 'BR';
  497.        if ( !isBR && isInline( child ) ) {
  498.            if ( !wrapper ) {
  499.                 wrapper = createElement( doc,
  500.                    config.blockTag, config.blockAttributes );
  501.            }
  502.            wrapper.appendChild( child );
  503.            i -= 1;
  504.            l -= 1;
  505.        } else if ( isBR || wrapper ) {
  506.            if ( !wrapper ) {
  507.                wrapper = createElement( doc,
  508.                    config.blockTag, config.blockAttributes );
  509.            }
  510.            fixCursor( wrapper, root );
  511.            if ( isBR ) {
  512.                container.replaceChild( wrapper, child );
  513.            } else {
  514.                container.insertBefore( wrapper, child );
  515.                i += 1;
  516.                l += 1;
  517.            }
  518.            wrapper = null;
  519.        }
  520.        if ( isContainer( child ) ) {
  521.            fixContainer( child, root );
  522.        }
  523.    }
  524.    if ( wrapper ) {
  525.        container.appendChild( fixCursor( wrapper, root ) );
  526.    }
  527.    return container;
  528. }
  529.  
  530. function split ( node, offset, stopNode, root ) {
  531.    var nodeType = node.nodeType,
  532.        parent, clone, next;
  533.    if ( nodeType === TEXT_NODE && node !== stopNode ) {
  534.        return split(
  535.            node.parentNode, node.splitText( offset ), stopNode, root );
  536.    }
  537.    if ( nodeType === ELEMENT_NODE ) {
  538.        if ( typeof( offset ) === 'number' ) {
  539.            offset = offset < node.childNodes.length ?
  540.                node.childNodes[ offset ] : null;
  541.        }
  542.        if ( node === stopNode ) {
  543.            return offset;
  544.        }
  545.  
  546.        // Clone node without children
  547.        parent = node.parentNode;
  548.        clone = node.cloneNode( false );
  549.  
  550.        // Add right-hand siblings to the clone
  551.        while ( offset ) {
  552.            next = offset.nextSibling;
  553.            clone.appendChild( offset );
  554.            offset = next;
  555.        }
  556.  
  557.        // Maintain li numbering if inside a quote.
  558.        if ( node.nodeName === 'OL' &&
  559.                getNearest( node, root, 'BLOCKQUOTE' ) ) {
  560.            clone.start = ( +node.start || 1 ) + node.childNodes.length - 1;
  561.        }
  562.  
  563.        // DO NOT NORMALISE. This may undo the fixCursor() call
  564.        // of a node lower down the tree!
  565.  
  566.        // We need something in the element in order for the cursor to appear.
  567.        fixCursor( node, root );
  568.        fixCursor( clone, root );
  569.  
  570.        // Inject clone after original node
  571.        if ( next = node.nextSibling ) {
  572.            parent.insertBefore( clone, next );
  573.        } else {
  574.            parent.appendChild( clone );
  575.        }
  576.  
  577.        // Keep on splitting up the tree
  578.        return split( parent, clone, stopNode, root );
  579.    }
  580.    return offset;
  581. }
  582.  
  583. function _mergeInlines ( node, fakeRange ) {
  584.    var children = node.childNodes,
  585.        l = children.length,
  586.        frags = [],
  587.        child, prev, len;
  588.    while ( l-- ) {
  589.        child = children[l];
  590.        prev = l && children[ l - 1 ];
  591.        if ( l && isInline( child ) && areAlike( child, prev ) &&
  592.                !leafNodeNames[ child.nodeName ] ) {
  593.            if ( fakeRange.startContainer === child ) {
  594.                fakeRange.startContainer = prev;
  595.                fakeRange.startOffset += getLength( prev );
  596.            }
  597.            if ( fakeRange.endContainer === child ) {
  598.                fakeRange.endContainer = prev;
  599.                fakeRange.endOffset += getLength( prev );
  600.            }
  601.            if ( fakeRange.startContainer === node ) {
  602.                if ( fakeRange.startOffset > l ) {
  603.                    fakeRange.startOffset -= 1;
  604.                }
  605.                else if ( fakeRange.startOffset === l ) {
  606.                    fakeRange.startContainer = prev;
  607.                    fakeRange.startOffset = getLength( prev );
  608.                }
  609.            }
  610.            if ( fakeRange.endContainer === node ) {
  611.                if ( fakeRange.endOffset > l ) {
  612.                    fakeRange.endOffset -= 1;
  613.                }
  614.                else if ( fakeRange.endOffset === l ) {
  615.                    fakeRange.endContainer = prev;
  616.                    fakeRange.endOffset = getLength( prev );
  617.                }
  618.            }
  619.            detach( child );
  620.            if ( child.nodeType === TEXT_NODE ) {
  621.                prev.appendData( child.data );
  622.            }
  623.            else {
  624.                frags.push( empty( child ) );
  625.            }
  626.        }
  627.        else if ( child.nodeType === ELEMENT_NODE ) {
  628.            len = frags.length;
  629.            while ( len-- ) {
  630.                child.appendChild( frags.pop() );
  631.            }
  632.            _mergeInlines( child, fakeRange );
  633.        }
  634.    }
  635. }
  636.  
  637. function mergeInlines ( node, range ) {
  638.    if ( node.nodeType === TEXT_NODE ) {
  639.        node = node.parentNode;
  640.    }
  641.    if ( node.nodeType === ELEMENT_NODE ) {
  642.        var fakeRange = {
  643.            startContainer: range.startContainer,
  644.            startOffset: range.startOffset,
  645.            endContainer: range.endContainer,
  646.            endOffset: range.endOffset
  647.        };
  648.        _mergeInlines( node, fakeRange );
  649.        range.setStart( fakeRange.startContainer, fakeRange.startOffset );
  650.        range.setEnd( fakeRange.endContainer, fakeRange.endOffset );
  651.    }
  652. }
  653.  
  654. function mergeWithBlock ( block, next, range ) {
  655.    var container = next,
  656.        last, offset;
  657.    while ( container.parentNode.childNodes.length === 1 ) {
  658.        container = container.parentNode;
  659.    }
  660.    detach( container );
  661.  
  662.    offset = block.childNodes.length;
  663.  
  664.    // Remove extra <BR> fixer if present.
  665.    last = block.lastChild;
  666.    if ( last && last.nodeName === 'BR' ) {
  667.        block.removeChild( last );
  668.        offset -= 1;
  669.    }
  670.  
  671.    block.appendChild( empty( next ) );
  672.  
  673.    range.setStart( block, offset );
  674.    range.collapse( true );
  675.    mergeInlines( block, range );
  676.  
  677.    // Opera inserts a BR if you delete the last piece of text
  678.    // in a block-level element. Unfortunately, it then gets
  679.    // confused when setting the selection subsequently and
  680.    // refuses to accept the range that finishes just before the
  681.    // BR. Removing the BR fixes the bug.
  682.    // Steps to reproduce bug: Type "a-b-c" (where - is return)
  683.    // then backspace twice. The cursor goes to the top instead
  684.    // of after "b".
  685.    if ( isPresto && ( last = block.lastChild ) && last.nodeName === 'BR' ) {
  686.        block.removeChild( last );
  687.    }
  688. }
  689.  
  690. function mergeContainers ( node, root ) {
  691.    var prev = node.previousSibling,
  692.        first = node.firstChild,
  693.        doc = node.ownerDocument,
  694.        isListItem = ( node.nodeName === 'LI' ),
  695.        needsFix, block;
  696.  
  697.    // Do not merge LIs, unless it only contains a UL
  698.    if ( isListItem && ( !first || !/^[OU]L$/.test( first.nodeName ) ) ) {
  699.        return;
  700.    }
  701.  
  702.    if ( prev && areAlike( prev, node ) ) {
  703.        if ( !isContainer( prev ) ) {
  704.            if ( isListItem ) {
  705.                block = createElement( doc, 'DIV' );
  706.                block.appendChild( empty( prev ) );
  707.                prev.appendChild( block );
  708.            } else {
  709.                return;
  710.            }
  711.        }
  712.        detach( node );
  713.        needsFix = !isContainer( node );
  714.        prev.appendChild( empty( node ) );
  715.        if ( needsFix ) {
  716.            fixContainer( prev, root );
  717.        }
  718.        if ( first ) {
  719.            mergeContainers( first, root );
  720.        }
  721.    } else if ( isListItem ) {
  722.        prev = createElement( doc, 'DIV' );
  723.        node.insertBefore( prev, first );
  724.        fixCursor( prev, root );
  725.    }
  726. }
  727.  
  728. var getNodeBefore = function ( node, offset ) {
  729.    var children = node.childNodes;
  730.    while ( offset && node.nodeType === ELEMENT_NODE ) {
  731.        node = children[ offset - 1 ];
  732.        children = node.childNodes;
  733.        offset = children.length;
  734.    }
  735.    return node;
  736. };
  737.  
  738. var getNodeAfter = function ( node, offset ) {
  739.    if ( node.nodeType === ELEMENT_NODE ) {
  740.        var children = node.childNodes;
  741.        if ( offset < children.length ) {
  742.            node = children[ offset ];
  743.        } else {
  744.            while ( node && !node.nextSibling ) {
  745.                node = node.parentNode;
  746.            }
  747.            if ( node ) { node = node.nextSibling; }
  748.        }
  749.    }
  750.    return node;
  751. };
  752.  
  753. // ---
  754.  
  755. var insertNodeInRange = function ( range, node ) {
  756.    // Insert at start.
  757.    var startContainer = range.startContainer,
  758.        startOffset = range.startOffset,
  759.        endContainer = range.endContainer,
  760.        endOffset = range.endOffset,
  761.        parent, children, childCount, afterSplit;
  762.  
  763.    // If part way through a text node, split it.
  764.    if ( startContainer.nodeType === TEXT_NODE ) {
  765.        parent = startContainer.parentNode;
  766.        children = parent.childNodes;
  767.        if ( startOffset === startContainer.length ) {
  768.            startOffset = indexOf.call( children, startContainer ) + 1;
  769.            if ( range.collapsed ) {
  770.                endContainer = parent;
  771.                endOffset = startOffset;
  772.            }
  773.        } else {
  774.            if ( startOffset ) {
  775.                afterSplit = startContainer.splitText( startOffset );
  776.                if ( endContainer === startContainer ) {
  777.                    endOffset -= startOffset;
  778.                    endContainer = afterSplit;
  779.                }
  780.                else if ( endContainer === parent ) {
  781.                    endOffset += 1;
  782.                }
  783.                startContainer = afterSplit;
  784.            }
  785.            startOffset = indexOf.call( children, startContainer );
  786.        }
  787.        startContainer = parent;
  788.    } else {
  789.        children = startContainer.childNodes;
  790.    }
  791.  
  792.    childCount = children.length;
  793.  
  794.    if ( startOffset === childCount ) {
  795.        startContainer.appendChild( node );
  796.    } else {
  797.        startContainer.insertBefore( node, children[ startOffset ] );
  798.    }
  799.  
  800.    if ( startContainer === endContainer ) {
  801.        endOffset += children.length - childCount;
  802.    }
  803.  
  804.    range.setStart( startContainer, startOffset );
  805.    range.setEnd( endContainer, endOffset );
  806. };
  807.  
  808. var extractContentsOfRange = function ( range, common, root ) {
  809.    var startContainer = range.startContainer,
  810.        startOffset = range.startOffset,
  811.        endContainer = range.endContainer,
  812.        endOffset = range.endOffset;
  813.  
  814.    if ( !common ) {
  815.        common = range.commonAncestorContainer;
  816.    }
  817.  
  818.    if ( common.nodeType === TEXT_NODE ) {
  819.        common = common.parentNode;
  820.    }
  821.  
  822.    var endNode = split( endContainer, endOffset, common, root ),
  823.        startNode = split( startContainer, startOffset, common, root ),
  824.        frag = common.ownerDocument.createDocumentFragment(),
  825.        next, before, after;
  826.  
  827.    // End node will be null if at end of child nodes list.
  828.    while ( startNode !== endNode ) {
  829.        next = startNode.nextSibling;
  830.        frag.appendChild( startNode );
  831.        startNode = next;
  832.    }
  833.  
  834.    startContainer = common;
  835.    startOffset = endNode ?
  836.        indexOf.call( common.childNodes, endNode ) :
  837.        common.childNodes.length;
  838.  
  839.    // Merge text nodes if adjacent. IE10 in particular will not focus
  840.    // between two text nodes
  841.    after = common.childNodes[ startOffset ];
  842.    before = after && after.previousSibling;
  843.    if ( before &&
  844.            before.nodeType === TEXT_NODE &&
  845.            after.nodeType === TEXT_NODE ) {
  846.        startContainer = before;
  847.        startOffset = before.length;
  848.        before.appendData( after.data );
  849.        detach( after );
  850.    }
  851.  
  852.    range.setStart( startContainer, startOffset );
  853.    range.collapse( true );
  854.  
  855.    fixCursor( common, root );
  856.  
  857.    return frag;
  858. };
  859.  
  860. var deleteContentsOfRange = function ( range, root ) {
  861.    // Move boundaries up as much as possible to reduce need to split.
  862.    // But we need to check whether we've moved the boundary outside of a
  863.     // block. If so, the entire block will be removed, so we shouldn't merge
  864.     // later.
  865.     moveRangeBoundariesUpTree( range );
  866.  
  867.     var startBlock = range.startContainer,
  868.         endBlock = range.endContainer,
  869.         needsMerge = ( isInline( startBlock ) || isBlock( startBlock ) ) &&
  870.             ( isInline( endBlock ) || isBlock( endBlock ) );
  871.  
  872.     // Remove selected range
  873.     var frag = extractContentsOfRange( range, null, root );
  874.  
  875.     // Move boundaries back down tree so that they are inside the blocks.
  876.     // If we don't do this, the range may be collapsed to a point between
  877.     // two blocks, so get(Start|End)BlockOfRange will return null.
  878.     moveRangeBoundariesDownTree( range );
  879.  
  880.     // If we split into two different blocks, merge the blocks.
  881.     startBlock = getStartBlockOfRange( range, root );
  882.     if ( needsMerge ) {
  883.         endBlock = getEndBlockOfRange( range, root );
  884.         if ( startBlock && endBlock && startBlock !== endBlock ) {
  885.             mergeWithBlock( startBlock, endBlock, range );
  886.         }
  887.     }
  888.  
  889.     // Ensure block has necessary children
  890.     if ( startBlock ) {
  891.         fixCursor( startBlock, root );
  892.     }
  893.  
  894.     // Ensure root has a block-level element in it.
  895.     var child = root.firstChild;
  896.     if ( !child || child.nodeName === 'BR' ) {
  897.         fixCursor( root, root );
  898.         range.selectNodeContents( root.firstChild );
  899.     } else {
  900.         range.collapse( range.endContainer === root ? true : false );
  901.     }
  902.     return frag;
  903. };
  904.  
  905. // ---
  906.  
  907. var insertTreeFragmentIntoRange = function ( range, frag, root ) {
  908.     // Check if it's all inline content
  909.     var allInline = true,
  910.         children = frag.childNodes,
  911.         l = children.length;
  912.     while ( l-- ) {
  913.         if ( !isInline( children[l] ) ) {
  914.             allInline = false;
  915.             break;
  916.         }
  917.     }
  918.  
  919.     // Delete any selected content
  920.     if ( !range.collapsed ) {
  921.         deleteContentsOfRange( range, root );
  922.     }
  923.  
  924.     // Move range down into text nodes
  925.     moveRangeBoundariesDownTree( range );
  926.  
  927.     if ( allInline ) {
  928.         // If inline, just insert at the current position.
  929.         insertNodeInRange( range, frag );
  930.         if ( range.startContainer !== range.endContainer ) {
  931.             mergeInlines( range.endContainer, range );
  932.         }
  933.         mergeInlines( range.startContainer, range );
  934.         range.collapse( false );
  935.     } else {
  936.         // Otherwise...
  937.         // 1. Split up to blockquote (if a parent) or root
  938.         var splitPoint = range.startContainer,
  939.             nodeAfterSplit = split(
  940.                 splitPoint,
  941.                 range.startOffset,
  942.                 getNearest( splitPoint.parentNode, root, 'BLOCKQUOTE' ) || root,
  943.                 root
  944.             ),
  945.             nodeBeforeSplit = nodeAfterSplit.previousSibling,
  946.             startContainer = nodeBeforeSplit,
  947.             startOffset = startContainer.childNodes.length,
  948.             endContainer = nodeAfterSplit,
  949.             endOffset = 0,
  950.             parent = nodeAfterSplit.parentNode,
  951.             child, node, prev, next, startAnchor;
  952.  
  953.         // 2. Move down into edge either side of split and insert any inline
  954.         // nodes at the beginning/end of the fragment
  955.         while ( ( child = startContainer.lastChild ) &&
  956.                 child.nodeType === ELEMENT_NODE ) {
  957.             if ( child.nodeName === 'BR' ) {
  958.                 startOffset -= 1;
  959.                 break;
  960.             }
  961.             startContainer = child;
  962.             startOffset = startContainer.childNodes.length;
  963.         }
  964.         while ( ( child = endContainer.firstChild ) &&
  965.                 child.nodeType === ELEMENT_NODE &&
  966.                 child.nodeName !== 'BR' ) {
  967.             endContainer = child;
  968.         }
  969.         startAnchor = startContainer.childNodes[ startOffset ] || null;
  970.         while ( ( child = frag.firstChild ) && isInline( child ) ) {
  971.             startContainer.insertBefore( child, startAnchor );
  972.         }
  973.         while ( ( child = frag.lastChild ) && isInline( child ) ) {
  974.             endContainer.insertBefore( child, endContainer.firstChild );
  975.             endOffset += 1;
  976.         }
  977.  
  978.         // 3. Fix cursor then insert block(s) in the fragment
  979.         node = frag;
  980.         while ( node = getNextBlock( node, root ) ) {
  981.             fixCursor( node, root );
  982.         }
  983.         parent.insertBefore( frag, nodeAfterSplit );
  984.  
  985.         // 4. Remove empty nodes created either side of split, then
  986.         // merge containers at the edges.
  987.         next = nodeBeforeSplit.nextSibling;
  988.         node = getPreviousBlock( next, root );
  989.         if ( node && !/\S/.test( node.textContent ) ) {
  990.             do {
  991.                 parent = node.parentNode;
  992.                 parent.removeChild( node );
  993.                 node = parent;
  994.             } while ( node && !node.lastChild && node !== root );
  995.         }
  996.         if ( !nodeBeforeSplit.parentNode ) {
  997.             nodeBeforeSplit = next.previousSibling;
  998.         }
  999.         if ( !startContainer.parentNode ) {
  1000.             startContainer = nodeBeforeSplit || next.parentNode;
  1001.             startOffset = nodeBeforeSplit ?
  1002.                 nodeBeforeSplit.childNodes.length : 0;
  1003.         }
  1004.         // Merge inserted containers with edges of split
  1005.         if ( isContainer( next ) ) {
  1006.             mergeContainers( next, root );
  1007.         }
  1008.  
  1009.         prev = nodeAfterSplit.previousSibling;
  1010.         node = isBlock( nodeAfterSplit ) ?
  1011.             nodeAfterSplit : getNextBlock( nodeAfterSplit, root );
  1012.         if ( node && !/\S/.test( node.textContent ) ) {
  1013.             do {
  1014.                 parent = node.parentNode;
  1015.                 parent.removeChild( node );
  1016.                 node = parent;
  1017.             } while ( node && !node.lastChild && node !== root );
  1018.         }
  1019.         if ( !nodeAfterSplit.parentNode ) {
  1020.             nodeAfterSplit = prev.nextSibling;
  1021.         }
  1022.         if ( !endOffset ) {
  1023.             endContainer = prev;
  1024.             endOffset = prev.childNodes.length;
  1025.         }
  1026.         // Merge inserted containers with edges of split
  1027.         if ( nodeAfterSplit && isContainer( nodeAfterSplit ) ) {
  1028.             mergeContainers( nodeAfterSplit, root );
  1029.         }
  1030.  
  1031.         range.setStart( startContainer, startOffset );
  1032.         range.setEnd( endContainer, endOffset );
  1033.         moveRangeBoundariesDownTree( range );
  1034.     }
  1035. };
  1036.  
  1037. // ---
  1038.  
  1039. var isNodeContainedInRange = function ( range, node, partial ) {
  1040.     var nodeRange = node.ownerDocument.createRange();
  1041.  
  1042.     nodeRange.selectNode( node );
  1043.  
  1044.     if ( partial ) {
  1045.         // Node must not finish before range starts or start after range
  1046.         // finishes.
  1047.         var nodeEndBeforeStart = ( range.compareBoundaryPoints(
  1048.                 END_TO_START, nodeRange ) > -1 ),
  1049.             nodeStartAfterEnd = ( range.compareBoundaryPoints(
  1050.                 START_TO_END, nodeRange ) < 1 );
  1051.         return ( !nodeEndBeforeStart && !nodeStartAfterEnd );
  1052.     }
  1053.     else {
  1054.         // Node must start after range starts and finish before range
  1055.         // finishes
  1056.         var nodeStartAfterStart = ( range.compareBoundaryPoints(
  1057.                 START_TO_START, nodeRange ) < 1 ),
  1058.             nodeEndBeforeEnd = ( range.compareBoundaryPoints(
  1059.                 END_TO_END, nodeRange ) > -1 );
  1060.         return ( nodeStartAfterStart && nodeEndBeforeEnd );
  1061.     }
  1062. };
  1063.  
  1064. var moveRangeBoundariesDownTree = function ( range ) {
  1065.     var startContainer = range.startContainer,
  1066.         startOffset = range.startOffset,
  1067.         endContainer = range.endContainer,
  1068.         endOffset = range.endOffset,
  1069.         maySkipBR = true,
  1070.         child;
  1071.  
  1072.     while ( startContainer.nodeType !== TEXT_NODE ) {
  1073.         child = startContainer.childNodes[ startOffset ];
  1074.         if ( !child || isLeaf( child ) ) {
  1075.             break;
  1076.         }
  1077.         startContainer = child;
  1078.         startOffset = 0;
  1079.     }
  1080.     if ( endOffset ) {
  1081.         while ( endContainer.nodeType !== TEXT_NODE ) {
  1082.             child = endContainer.childNodes[ endOffset - 1 ];
  1083.             if ( !child || isLeaf( child ) ) {
  1084.                 if ( maySkipBR && child && child.nodeName === 'BR' ) {
  1085.                     endOffset -= 1;
  1086.                     maySkipBR = false;
  1087.                     continue;
  1088.                 }
  1089.                 break;
  1090.             }
  1091.             endContainer = child;
  1092.             endOffset = getLength( endContainer );
  1093.         }
  1094.     } else {
  1095.         while ( endContainer.nodeType !== TEXT_NODE ) {
  1096.             child = endContainer.firstChild;
  1097.             if ( !child || isLeaf( child ) ) {
  1098.                 break;
  1099.             }
  1100.             endContainer = child;
  1101.         }
  1102.     }
  1103.  
  1104.     // If collapsed, this algorithm finds the nearest text node positions
  1105.     // *outside* the range rather than inside, but also it flips which is
  1106.     // assigned to which.
  1107.     if ( range.collapsed ) {
  1108.         range.setStart( endContainer, endOffset );
  1109.         range.setEnd( startContainer, startOffset );
  1110.     } else {
  1111.         range.setStart( startContainer, startOffset );
  1112.         range.setEnd( endContainer, endOffset );
  1113.     }
  1114. };
  1115.  
  1116. var moveRangeBoundariesUpTree = function ( range, common ) {
  1117.     var startContainer = range.startContainer,
  1118.         startOffset = range.startOffset,
  1119.         endContainer = range.endContainer,
  1120.         endOffset = range.endOffset,
  1121.         maySkipBR = true,
  1122.         parent;
  1123.  
  1124.     if ( !common ) {
  1125.         common = range.commonAncestorContainer;
  1126.     }
  1127.  
  1128.     while ( startContainer !== common && !startOffset ) {
  1129.         parent = startContainer.parentNode;
  1130.         startOffset = indexOf.call( parent.childNodes, startContainer );
  1131.         startContainer = parent;
  1132.     }
  1133.  
  1134.     while ( true ) {
  1135.         if ( maySkipBR &&
  1136.                 endContainer.nodeType !== TEXT_NODE &&
  1137.                 endContainer.childNodes[ endOffset ] &&
  1138.                 endContainer.childNodes[ endOffset ].nodeName === 'BR' ) {
  1139.             endOffset += 1;
  1140.             maySkipBR = false;
  1141.         }
  1142.         if ( endContainer === common ||
  1143.                 endOffset !== getLength( endContainer ) ) {
  1144.             break;
  1145.         }
  1146.         parent = endContainer.parentNode;
  1147.         endOffset = indexOf.call( parent.childNodes, endContainer ) + 1;
  1148.         endContainer = parent;
  1149.     }
  1150.  
  1151.     range.setStart( startContainer, startOffset );
  1152.     range.setEnd( endContainer, endOffset );
  1153. };
  1154.  
  1155. // Returns the first block at least partially contained by the range,
  1156. // or null if no block is contained by the range.
  1157. var getStartBlockOfRange = function ( range, root ) {
  1158.     var container = range.startContainer,
  1159.         block;
  1160.  
  1161.     // If inline, get the containing block.
  1162.     if ( isInline( container ) ) {
  1163.         block = getPreviousBlock( container, root );
  1164.     } else if ( container !== root && isBlock( container ) ) {
  1165.         block = container;
  1166.     } else {
  1167.         block = getNodeBefore( container, range.startOffset );
  1168.         block = getNextBlock( block, root );
  1169.     }
  1170.     // Check the block actually intersects the range
  1171.     return block && isNodeContainedInRange( range, block, true ) ? block : null;
  1172. };
  1173.  
  1174. // Returns the last block at least partially contained by the range,
  1175. // or null if no block is contained by the range.
  1176. var getEndBlockOfRange = function ( range, root ) {
  1177.     var container = range.endContainer,
  1178.         block, child;
  1179.  
  1180.     // If inline, get the containing block.
  1181.     if ( isInline( container ) ) {
  1182.         block = getPreviousBlock( container, root );
  1183.     } else if ( container !== root && isBlock( container ) ) {
  1184.         block = container;
  1185.     } else {
  1186.         block = getNodeAfter( container, range.endOffset );
  1187.         if ( !block || !isOrContains( root, block ) ) {
  1188.             block = root;
  1189.             while ( child = block.lastChild ) {
  1190.                 block = child;
  1191.             }
  1192.         }
  1193.         block = getPreviousBlock( block, root );
  1194.     }
  1195.     // Check the block actually intersects the range
  1196.     return block && isNodeContainedInRange( range, block, true ) ? block : null;
  1197. };
  1198.  
  1199. var contentWalker = new TreeWalker( null,
  1200.     SHOW_TEXT|SHOW_ELEMENT,
  1201.     function ( node ) {
  1202.         return node.nodeType === TEXT_NODE ?
  1203.             notWS.test( node.data ) :
  1204.             node.nodeName === 'IMG';
  1205.     }
  1206. );
  1207.  
  1208. var rangeDoesStartAtBlockBoundary = function ( range, root ) {
  1209.     var startContainer = range.startContainer;
  1210.     var startOffset = range.startOffset;
  1211.     var nodeAfterCursor;
  1212.  
  1213.     // If in the middle or end of a text node, we're not at the boundary.
  1214.     contentWalker.root = null;
  1215.     if ( startContainer.nodeType === TEXT_NODE ) {
  1216.         if ( startOffset ) {
  1217.             return false;
  1218.         }
  1219.         nodeAfterCursor = startContainer;
  1220.     } else {
  1221.         nodeAfterCursor = getNodeAfter( startContainer, startOffset );
  1222.         if ( nodeAfterCursor && !isOrContains( root, nodeAfterCursor ) ) {
  1223.             nodeAfterCursor = null;
  1224.         }
  1225.         // The cursor was right at the end of the document
  1226.         if ( !nodeAfterCursor ) {
  1227.             nodeAfterCursor = getNodeBefore( startContainer, startOffset );
  1228.             if ( nodeAfterCursor.nodeType === TEXT_NODE &&
  1229.                     nodeAfterCursor.length ) {
  1230.                 return false;
  1231.             }
  1232.         }
  1233.     }
  1234.  
  1235.     // Otherwise, look for any previous content in the same block.
  1236.     contentWalker.currentNode = nodeAfterCursor;
  1237.     contentWalker.root = getStartBlockOfRange( range, root );
  1238.  
  1239.     return !contentWalker.previousNode();
  1240. };
  1241.  
  1242. var rangeDoesEndAtBlockBoundary = function ( range, root ) {
  1243.     var endContainer = range.endContainer,
  1244.         endOffset = range.endOffset,
  1245.         length;
  1246.  
  1247.     // If in a text node with content, and not at the end, we're not
  1248.     // at the boundary
  1249.     contentWalker.root = null;
  1250.     if ( endContainer.nodeType === TEXT_NODE ) {
  1251.         length = endContainer.data.length;
  1252.         if ( length && endOffset < length ) {
  1253.             return false;
  1254.         }
  1255.         contentWalker.currentNode = endContainer;
  1256.     } else {
  1257.         contentWalker.currentNode = getNodeBefore( endContainer, endOffset );
  1258.     }
  1259.  
  1260.     // Otherwise, look for any further content in the same block.
  1261.     contentWalker.root = getEndBlockOfRange( range, root );
  1262.  
  1263.     return !contentWalker.nextNode();
  1264. };
  1265.  
  1266. var expandRangeToBlockBoundaries = function ( range, root ) {
  1267.     var start = getStartBlockOfRange( range, root ),
  1268.         end = getEndBlockOfRange( range, root ),
  1269.         parent;
  1270.  
  1271.     if ( start && end ) {
  1272.         parent = start.parentNode;
  1273.         range.setStart( parent, indexOf.call( parent.childNodes, start ) );
  1274.         parent = end.parentNode;
  1275.         range.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 );
  1276.     }
  1277. };
  1278.  
  1279. var keys = {
  1280.     8: 'backspace',
  1281.     9: 'tab',
  1282.     13: 'enter',
  1283.     32: 'space',
  1284.     33: 'pageup',
  1285.     34: 'pagedown',
  1286.     37: 'left',
  1287.     39: 'right',
  1288.     46: 'delete',
  1289.     219: '[',
  1290.     221: ']'
  1291. };
  1292.  
  1293. // Ref: http://unixpapa.com/js/key.html
  1294. var onKey = function ( event ) {
  1295.     var code = event.keyCode,
  1296.         key = keys[ code ],
  1297.         modifiers = '',
  1298.         range = this.getSelection();
  1299.  
  1300.     if ( event.defaultPrevented ) {
  1301.         return;
  1302.     }
  1303.  
  1304.     if ( !key ) {
  1305.         key = String.fromCharCode( code ).toLowerCase();
  1306.         // Only reliable for letters and numbers
  1307.         if ( !/^[A-Za-z0-9]$/.test( key ) ) {
  1308.             key = '';
  1309.         }
  1310.     }
  1311.  
  1312.     // On keypress, delete and '.' both have event.keyCode 46
  1313.     // Must check event.which to differentiate.
  1314.     if ( isPresto && event.which === 46 ) {
  1315.         key = '.';
  1316.     }
  1317.  
  1318.     // Function keys
  1319.     if ( 111 < code && code < 124 ) {
  1320.         key = 'f' + ( code - 111 );
  1321.     }
  1322.  
  1323.     // We need to apply the backspace/delete handlers regardless of
  1324.     // control key modifiers.
  1325.     if ( key !== 'backspace' && key !== 'delete' ) {
  1326.         if ( event.altKey  ) { modifiers += 'alt-'; }
  1327.         if ( event.ctrlKey ) { modifiers += 'ctrl-'; }
  1328.         if ( event.metaKey ) { modifiers += 'meta-'; }
  1329.     }
  1330.     // However, on Windows, shift-delete is apparently "cut" (WTF right?), so
  1331.     // we want to let the browser handle shift-delete.
  1332.     if ( event.shiftKey ) { modifiers += 'shift-'; }
  1333.  
  1334.     key = modifiers + key;
  1335.  
  1336.     if ( this._keyHandlers[ key ] ) {
  1337.         this._keyHandlers[ key ]( this, event, range );
  1338.     } else if ( key.length === 1 && !range.collapsed ) {
  1339.         // Record undo checkpoint.
  1340.         this.saveUndoState( range );
  1341.         // Delete the selection
  1342.         deleteContentsOfRange( range, this._root );
  1343.         this._ensureBottomLine();
  1344.         this.setSelection( range );
  1345.         this._updatePath( range, true );
  1346.     }
  1347. };
  1348.  
  1349. var mapKeyTo = function ( method ) {
  1350.     return function ( self, event ) {
  1351.         event.preventDefault();
  1352.         self[ method ]();
  1353.     };
  1354. };
  1355.  
  1356. var mapKeyToFormat = function ( tag, remove ) {
  1357.     remove = remove || null;
  1358.     return function ( self, event ) {
  1359.         event.preventDefault();
  1360.         var range = self.getSelection();
  1361.         if ( self.hasFormat( tag, null, range ) ) {
  1362.             self.changeFormat( null, { tag: tag }, range );
  1363.         } else {
  1364.             self.changeFormat( { tag: tag }, remove, range );
  1365.         }
  1366.     };
  1367. };
  1368.  
  1369. // If you delete the content inside a span with a font styling, Webkit will
  1370. // replace it with a <font> tag (!). If you delete all the text inside a
  1371. // link in Opera, it won't delete the link. Let's make things consistent. If
  1372. // you delete all text inside an inline tag, remove the inline tag.
  1373. var afterDelete = function ( self, range ) {
  1374.     try {
  1375.         if ( !range ) { range = self.getSelection(); }
  1376.         var node = range.startContainer,
  1377.             parent;
  1378.         // Climb the tree from the focus point while we are inside an empty
  1379.         // inline element
  1380.         if ( node.nodeType === TEXT_NODE ) {
  1381.             node = node.parentNode;
  1382.         }
  1383.         parent = node;
  1384.         while ( isInline( parent ) &&
  1385.                 ( !parent.textContent || parent.textContent === ZWS ) ) {
  1386.             node = parent;
  1387.             parent = node.parentNode;
  1388.         }
  1389.         // If focused in empty inline element
  1390.         if ( node !== parent ) {
  1391.             // Move focus to just before empty inline(s)
  1392.             range.setStart( parent,
  1393.                 indexOf.call( parent.childNodes, node ) );
  1394.             range.collapse( true );
  1395.             // Remove empty inline(s)
  1396.             parent.removeChild( node );
  1397.             // Fix cursor in block
  1398.             if ( !isBlock( parent ) ) {
  1399.                 parent = getPreviousBlock( parent, self._root );
  1400.             }
  1401.             fixCursor( parent, self._root );
  1402.             // Move cursor into text node
  1403.             moveRangeBoundariesDownTree( range );
  1404.         }
  1405.         // If you delete the last character in the sole <div> in Chrome,
  1406.         // it removes the div and replaces it with just a <br> inside the
  1407.         // root. Detach the <br>; the _ensureBottomLine call will insert a new
  1408.         // block.
  1409.         if ( node === self._root &&
  1410.                 ( node = node.firstChild ) && node.nodeName === 'BR' ) {
  1411.             detach( node );
  1412.         }
  1413.         self._ensureBottomLine();
  1414.         self.setSelection( range );
  1415.         self._updatePath( range, true );
  1416.     } catch ( error ) {
  1417.         self.didError( error );
  1418.     }
  1419. };
  1420.  
  1421. var keyHandlers = {
  1422.     enter: function ( self, event, range ) {
  1423.         var root = self._root;
  1424.         var block, parent, nodeAfterSplit;
  1425.  
  1426.         // We handle this ourselves
  1427.         event.preventDefault();
  1428.  
  1429.         // Save undo checkpoint and add any links in the preceding section.
  1430.         // Remove any zws so we don't think there's content in an empty
  1431.         // block.
  1432.         self._recordUndoState( range );
  1433.         addLinks( range.startContainer, root, self );
  1434.         self._removeZWS();
  1435.         self._getRangeAndRemoveBookmark( range );
  1436.  
  1437.         // Selected text is overwritten, therefore delete the contents
  1438.         // to collapse selection.
  1439.         if ( !range.collapsed ) {
  1440.             deleteContentsOfRange( range, root );
  1441.         }
  1442.  
  1443.         block = getStartBlockOfRange( range, root );
  1444.  
  1445.         // If this is a malformed bit of document or in a table;
  1446.         // just play it safe and insert a <br>.
  1447.         if ( !block || /^T[HD]$/.test( block.nodeName ) ) {
  1448.             insertNodeInRange( range, self.createElement( 'BR' ) );
  1449.             range.collapse( false );
  1450.             self.setSelection( range );
  1451.             self._updatePath( range, true );
  1452.             return;
  1453.         }
  1454.  
  1455.         // If in a list, we'll split the LI instead.
  1456.         if ( parent = getNearest( block, root, 'LI' ) ) {
  1457.             block = parent;
  1458.         }
  1459.  
  1460.         if ( !block.textContent ) {
  1461.             // Break list
  1462.             if ( getNearest( block, root, 'UL' ) ||
  1463.                     getNearest( block, root, 'OL' ) ) {
  1464.                 return self.modifyBlocks( decreaseListLevel, range );
  1465.             }
  1466.             // Break blockquote
  1467.             else if ( getNearest( block, root, 'BLOCKQUOTE' ) ) {
  1468.                 return self.modifyBlocks( removeBlockQuote, range );
  1469.             }
  1470.         }
  1471.  
  1472.         // Otherwise, split at cursor point.
  1473.         nodeAfterSplit = splitBlock( self, block,
  1474.             range.startContainer, range.startOffset );
  1475.  
  1476.         // Clean up any empty inlines if we hit enter at the beginning of the
  1477.         // block
  1478.         removeZWS( block );
  1479.         removeEmptyInlines( block );
  1480.         fixCursor( block, root );
  1481.  
  1482.         // Focus cursor
  1483.         // If there's a <b>/<i> etc. at the beginning of the split
  1484.         // make sure we focus inside it.
  1485.         while ( nodeAfterSplit.nodeType === ELEMENT_NODE ) {
  1486.             var child = nodeAfterSplit.firstChild,
  1487.                 next;
  1488.  
  1489.             // Don't continue links over a block break; unlikely to be the
  1490.             // desired outcome.
  1491.             if ( nodeAfterSplit.nodeName === 'A' &&
  1492.                     ( !nodeAfterSplit.textContent ||
  1493.                         nodeAfterSplit.textContent === ZWS ) ) {
  1494.                 child = self._doc.createTextNode( '' );
  1495.                 replaceWith( nodeAfterSplit, child );
  1496.                 nodeAfterSplit = child;
  1497.                 break;
  1498.             }
  1499.  
  1500.             while ( child && child.nodeType === TEXT_NODE && !child.data ) {
  1501.                 next = child.nextSibling;
  1502.                 if ( !next || next.nodeName === 'BR' ) {
  1503.                     break;
  1504.                 }
  1505.                 detach( child );
  1506.                 child = next;
  1507.             }
  1508.  
  1509.             // 'BR's essentially don't count; they're a browser hack.
  1510.             // If you try to select the contents of a 'BR', FF will not let
  1511.             // you type anything!
  1512.             if ( !child || child.nodeName === 'BR' ||
  1513.                     ( child.nodeType === TEXT_NODE && !isPresto ) ) {
  1514.                 break;
  1515.             }
  1516.             nodeAfterSplit = child;
  1517.         }
  1518.         range = self._createRange( nodeAfterSplit, 0 );
  1519.         self.setSelection( range );
  1520.         self._updatePath( range, true );
  1521.     },
  1522.     backspace: function ( self, event, range ) {
  1523.         var root = self._root;
  1524.         self._removeZWS();
  1525.         // Record undo checkpoint.
  1526.         self.saveUndoState( range );
  1527.         // If not collapsed, delete contents
  1528.         if ( !range.collapsed ) {
  1529.             event.preventDefault();
  1530.             deleteContentsOfRange( range, root );
  1531.             afterDelete( self, range );
  1532.         }
  1533.         // If at beginning of block, merge with previous
  1534.         else if ( rangeDoesStartAtBlockBoundary( range, root ) ) {
  1535.             event.preventDefault();
  1536.             var current = getStartBlockOfRange( range, root );
  1537.             var previous;
  1538.             if ( !current ) {
  1539.                 return;
  1540.             }
  1541.             // In case inline data has somehow got between blocks.
  1542.             fixContainer( current.parentNode, root );
  1543.             // Now get previous block
  1544.             previous = getPreviousBlock( current, root );
  1545.             // Must not be at the very beginning of the text area.
  1546.             if ( previous ) {
  1547.                 // If not editable, just delete whole block.
  1548.                 if ( !previous.isContentEditable ) {
  1549.                     detach( previous );
  1550.                     return;
  1551.                 }
  1552.                 // Otherwise merge.
  1553.                 mergeWithBlock( previous, current, range );
  1554.                 // If deleted line between containers, merge newly adjacent
  1555.                 // containers.
  1556.                 current = previous.parentNode;
  1557.                 while ( current !== root && !current.nextSibling ) {
  1558.                     current = current.parentNode;
  1559.                 }
  1560.                 if ( current !== root && ( current = current.nextSibling ) ) {
  1561.                     mergeContainers( current, root );
  1562.                 }
  1563.                 self.setSelection( range );
  1564.             }
  1565.             // If at very beginning of text area, allow backspace
  1566.             // to break lists/blockquote.
  1567.             else if ( current ) {
  1568.                 // Break list
  1569.                 if ( getNearest( current, root, 'UL' ) ||
  1570.                         getNearest( current, root, 'OL' ) ) {
  1571.                     return self.modifyBlocks( decreaseListLevel, range );
  1572.                 }
  1573.                 // Break blockquote
  1574.                 else if ( getNearest( current, root, 'BLOCKQUOTE' ) ) {
  1575.                     return self.modifyBlocks( decreaseBlockQuoteLevel, range );
  1576.                 }
  1577.                 self.setSelection( range );
  1578.                 self._updatePath( range, true );
  1579.             }
  1580.         }
  1581.         // Otherwise, leave to browser but check afterwards whether it has
  1582.         // left behind an empty inline tag.
  1583.         else {
  1584.             self.setSelection( range );
  1585.             setTimeout( function () { afterDelete( self ); }, 0 );
  1586.         }
  1587.     },
  1588.     'delete': function ( self, event, range ) {
  1589.         var root = self._root;
  1590.         var current, next, originalRange,
  1591.             cursorContainer, cursorOffset, nodeAfterCursor;
  1592.         self._removeZWS();
  1593.         // Record undo checkpoint.
  1594.         self.saveUndoState( range );
  1595.         // If not collapsed, delete contents
  1596.         if ( !range.collapsed ) {
  1597.             event.preventDefault();
  1598.             deleteContentsOfRange( range, root );
  1599.             afterDelete( self, range );
  1600.         }
  1601.         // If at end of block, merge next into this block
  1602.         else if ( rangeDoesEndAtBlockBoundary( range, root ) ) {
  1603.             event.preventDefault();
  1604.             current = getStartBlockOfRange( range, root );
  1605.             if ( !current ) {
  1606.                 return;
  1607.             }
  1608.             // In case inline data has somehow got between blocks.
  1609.             fixContainer( current.parentNode, root );
  1610.             // Now get next block
  1611.             next = getNextBlock( current, root );
  1612.             // Must not be at the very end of the text area.
  1613.             if ( next ) {
  1614.                 // If not editable, just delete whole block.
  1615.                 if ( !next.isContentEditable ) {
  1616.                     detach( next );
  1617.                     return;
  1618.                 }
  1619.                 // Otherwise merge.
  1620.                 mergeWithBlock( current, next, range );
  1621.                 // If deleted line between containers, merge newly adjacent
  1622.                 // containers.
  1623.                 next = current.parentNode;
  1624.                 while ( next !== root && !next.nextSibling ) {
  1625.                     next = next.parentNode;
  1626.                 }
  1627.                 if ( next !== root && ( next = next.nextSibling ) ) {
  1628.                     mergeContainers( next, root );
  1629.                 }
  1630.                 self.setSelection( range );
  1631.                 self._updatePath( range, true );
  1632.             }
  1633.         }
  1634.         // Otherwise, leave to browser but check afterwards whether it has
  1635.         // left behind an empty inline tag.
  1636.         else {
  1637.             // But first check if the cursor is just before an IMG tag. If so,
  1638.             // delete it ourselves, because the browser won't if it is not
  1639.             // inline.
  1640.             originalRange = range.cloneRange();
  1641.             moveRangeBoundariesUpTree( range, self._root );
  1642.             cursorContainer = range.endContainer;
  1643.             cursorOffset = range.endOffset;
  1644.             if ( cursorContainer.nodeType === ELEMENT_NODE ) {
  1645.                 nodeAfterCursor = cursorContainer.childNodes[ cursorOffset ];
  1646.                 if ( nodeAfterCursor && nodeAfterCursor.nodeName === 'IMG' ) {
  1647.                     event.preventDefault();
  1648.                     detach( nodeAfterCursor );
  1649.                     moveRangeBoundariesDownTree( range );
  1650.                     afterDelete( self, range );
  1651.                     return;
  1652.                 }
  1653.             }
  1654.             self.setSelection( originalRange );
  1655.             setTimeout( function () { afterDelete( self ); }, 0 );
  1656.         }
  1657.     },
  1658.     tab: function ( self, event, range ) {
  1659.         var root = self._root;
  1660.         var node, parent;
  1661.         self._removeZWS();
  1662.         // If no selection and at start of block
  1663.         if ( range.collapsed && rangeDoesStartAtBlockBoundary( range, root ) ) {
  1664.             node = getStartBlockOfRange( range, root );
  1665.             // Iterate through the block's parents
  1666.             while ( parent = node.parentNode ) {
  1667.                 // If we find a UL or OL (so are in a list, node must be an LI)
  1668.                 if ( parent.nodeName === 'UL' || parent.nodeName === 'OL' ) {
  1669.                     // Then increase the list level
  1670.                     event.preventDefault();
  1671.                     self.modifyBlocks( increaseListLevel, range );
  1672.                     break;
  1673.                 }
  1674.                 node = parent;
  1675.             }
  1676.         }
  1677.     },
  1678.     'shift-tab': function ( self, event, range ) {
  1679.         var root = self._root;
  1680.         var node;
  1681.         self._removeZWS();
  1682.         // If no selection and at start of block
  1683.         if ( range.collapsed && rangeDoesStartAtBlockBoundary( range, root ) ) {
  1684.             // Break list
  1685.             node = range.startContainer;
  1686.             if ( getNearest( node, root, 'UL' ) ||
  1687.                     getNearest( node, root, 'OL' ) ) {
  1688.                 event.preventDefault();
  1689.                 self.modifyBlocks( decreaseListLevel, range );
  1690.             }
  1691.         }
  1692.     },
  1693.     space: function ( self, _, range ) {
  1694.         var node, parent;
  1695.         self._recordUndoState( range );
  1696.         addLinks( range.startContainer, self._root, self );
  1697.         self._getRangeAndRemoveBookmark( range );
  1698.  
  1699.         // If the cursor is at the end of a link (<a>foo|</a>) then move it
  1700.         // outside of the link (<a>foo</a>|) so that the space is not part of
  1701.         // the link text.
  1702.         node = range.endContainer;
  1703.         parent = node.parentNode;
  1704.         if ( range.collapsed && parent.nodeName === 'A' &&
  1705.                 !node.nextSibling && range.endOffset === getLength( node ) ) {
  1706.             range.setStartAfter( parent );
  1707.         }
  1708.         // Delete the selection if not collapsed
  1709.         else if ( !range.collapsed ) {
  1710.             deleteContentsOfRange( range, self._root );
  1711.             self._ensureBottomLine();
  1712.             self.setSelection( range );
  1713.             self._updatePath( range, true );
  1714.         }
  1715.  
  1716.         self.setSelection( range );
  1717.     },
  1718.     left: function ( self ) {
  1719.         self._removeZWS();
  1720.     },
  1721.     right: function ( self ) {
  1722.         self._removeZWS();
  1723.     }
  1724. };
  1725.  
  1726. // Firefox pre v29 incorrectly handles Cmd-left/Cmd-right on Mac:
  1727. // it goes back/forward in history! Override to do the right
  1728. // thing.
  1729. // https://bugzilla.mozilla.org/show_bug.cgi?id=289384
  1730. if ( isMac && isGecko ) {
  1731.     keyHandlers[ 'meta-left' ] = function ( self, event ) {
  1732.         event.preventDefault();
  1733.         var sel = getWindowSelection( self );
  1734.         if ( sel && sel.modify ) {
  1735.             sel.modify( 'move', 'backward', 'lineboundary' );
  1736.         }
  1737.     };
  1738.     keyHandlers[ 'meta-right' ] = function ( self, event ) {
  1739.         event.preventDefault();
  1740.         var sel = getWindowSelection( self );
  1741.         if ( sel && sel.modify ) {
  1742.             sel.modify( 'move', 'forward', 'lineboundary' );
  1743.         }
  1744.     };
  1745. }
  1746.  
  1747. // System standard for page up/down on Mac is to just scroll, not move the
  1748. // cursor. On Linux/Windows, it should move the cursor, but some browsers don't
  1749. // implement this natively. Override to support it.
  1750. if ( !isMac ) {
  1751.     keyHandlers.pageup = function ( self ) {
  1752.         self.moveCursorToStart();
  1753.     };
  1754.     keyHandlers.pagedown = function ( self ) {
  1755.         self.moveCursorToEnd();
  1756.     };
  1757. }
  1758.  
  1759. keyHandlers[ ctrlKey + 'b' ] = mapKeyToFormat( 'B' );
  1760. keyHandlers[ ctrlKey + 'i' ] = mapKeyToFormat( 'I' );
  1761. keyHandlers[ ctrlKey + 'u' ] = mapKeyToFormat( 'U' );
  1762. keyHandlers[ ctrlKey + 'shift-7' ] = mapKeyToFormat( 'S' );
  1763. keyHandlers[ ctrlKey + 'shift-5' ] = mapKeyToFormat( 'SUB', { tag: 'SUP' } );
  1764. keyHandlers[ ctrlKey + 'shift-6' ] = mapKeyToFormat( 'SUP', { tag: 'SUB' } );
  1765. keyHandlers[ ctrlKey + 'shift-8' ] = mapKeyTo( 'makeUnorderedList' );
  1766. keyHandlers[ ctrlKey + 'shift-9' ] = mapKeyTo( 'makeOrderedList' );
  1767. keyHandlers[ ctrlKey + '[' ] = mapKeyTo( 'decreaseQuoteLevel' );
  1768. keyHandlers[ ctrlKey + ']' ] = mapKeyTo( 'increaseQuoteLevel' );
  1769. keyHandlers[ ctrlKey + 'y' ] = mapKeyTo( 'redo' );
  1770. keyHandlers[ ctrlKey + 'z' ] = mapKeyTo( 'undo' );
  1771. keyHandlers[ ctrlKey + 'shift-z' ] = mapKeyTo( 'redo' );
  1772.  
  1773. var fontSizes = {
  1774.     1: 10,
  1775.     2: 13,
  1776.     3: 16,
  1777.     4: 18,
  1778.     5: 24,
  1779.     6: 32,
  1780.     7: 48
  1781. };
  1782.  
  1783. var styleToSemantic = {
  1784.     backgroundColor: {
  1785.         regexp: notWS,
  1786.         replace: function ( doc, colour ) {
  1787.             return createElement( doc, 'SPAN', {
  1788.                 'class': HIGHLIGHT_CLASS,
  1789.                 style: 'background-color:' + colour
  1790.             });
  1791.         }
  1792.     },
  1793.     color: {
  1794.         regexp: notWS,
  1795.         replace: function ( doc, colour ) {
  1796.             return createElement( doc, 'SPAN', {
  1797.                 'class': COLOUR_CLASS,
  1798.                 style: 'color:' + colour
  1799.             });
  1800.         }
  1801.     },
  1802.     fontWeight: {
  1803.         regexp: /^bold|^700/i,
  1804.         replace: function ( doc ) {
  1805.             return createElement( doc, 'B' );
  1806.         }
  1807.     },
  1808.     fontStyle: {
  1809.         regexp: /^italic/i,
  1810.         replace: function ( doc ) {
  1811.             return createElement( doc, 'I' );
  1812.         }
  1813.     },
  1814.     fontFamily: {
  1815.         regexp: notWS,
  1816.         replace: function ( doc, family ) {
  1817.             return createElement( doc, 'SPAN', {
  1818.                 'class': FONT_FAMILY_CLASS,
  1819.                 style: 'font-family:' + family
  1820.             });
  1821.         }
  1822.     },
  1823.     fontSize: {
  1824.         regexp: notWS,
  1825.         replace: function ( doc, size ) {
  1826.             return createElement( doc, 'SPAN', {
  1827.                 'class': FONT_SIZE_CLASS,
  1828.                 style: 'font-size:' + size
  1829.             });
  1830.         }
  1831.     },
  1832.     textDecoration: {
  1833.         regexp: /^underline/i,
  1834.         replace: function ( doc ) {
  1835.             return createElement( doc, 'U' );
  1836.         }
  1837.     }
  1838. };
  1839.  
  1840. var replaceWithTag = function ( tag ) {
  1841.     return function ( node, parent ) {
  1842.         var el = createElement( node.ownerDocument, tag );
  1843.         parent.replaceChild( el, node );
  1844.         el.appendChild( empty( node ) );
  1845.         return el;
  1846.     };
  1847. };
  1848.  
  1849. var replaceStyles = function ( node, parent ) {
  1850.     var style = node.style;
  1851.     var doc = node.ownerDocument;
  1852.     var attr, converter, css, newTreeBottom, newTreeTop, el;
  1853.  
  1854.     for ( attr in styleToSemantic ) {
  1855.         converter = styleToSemantic[ attr ];
  1856.         css = style[ attr ];
  1857.         if ( css && converter.regexp.test( css ) ) {
  1858.             el = converter.replace( doc, css );
  1859.             if ( !newTreeTop ) {
  1860.                 newTreeTop = el;
  1861.             }
  1862.             if ( newTreeBottom ) {
  1863.                 newTreeBottom.appendChild( el );
  1864.             }
  1865.             newTreeBottom = el;
  1866.             node.style[ attr ] = '';
  1867.         }
  1868.     }
  1869.  
  1870.     if ( newTreeTop ) {
  1871.         newTreeBottom.appendChild( empty( node ) );
  1872.         if ( node.nodeName === 'SPAN' ) {
  1873.             parent.replaceChild( newTreeTop, node );
  1874.         } else {
  1875.             node.appendChild( newTreeTop );
  1876.         }
  1877.     }
  1878.  
  1879.     return newTreeBottom || node;
  1880. };
  1881.  
  1882. var stylesRewriters = {
  1883.     P: replaceStyles,
  1884.     SPAN: replaceStyles,
  1885.     STRONG: replaceWithTag( 'B' ),
  1886.     EM: replaceWithTag( 'I' ),
  1887.     INS: replaceWithTag( 'U' ),
  1888.     STRIKE: replaceWithTag( 'S' ),
  1889.     FONT: function ( node, parent ) {
  1890.         var face = node.face,
  1891.             size = node.size,
  1892.             colour = node.color,
  1893.             doc = node.ownerDocument,
  1894.             fontSpan, sizeSpan, colourSpan,
  1895.             newTreeBottom, newTreeTop;
  1896.         if ( face ) {
  1897.             fontSpan = createElement( doc, 'SPAN', {
  1898.                 'class': FONT_FAMILY_CLASS,
  1899.                 style: 'font-family:' + face
  1900.             });
  1901.             newTreeTop = fontSpan;
  1902.             newTreeBottom = fontSpan;
  1903.         }
  1904.         if ( size ) {
  1905.             sizeSpan = createElement( doc, 'SPAN', {
  1906.                 'class': FONT_SIZE_CLASS,
  1907.                 style: 'font-size:' + fontSizes[ size ] + 'px'
  1908.             });
  1909.             if ( !newTreeTop ) {
  1910.                 newTreeTop = sizeSpan;
  1911.             }
  1912.             if ( newTreeBottom ) {
  1913.                 newTreeBottom.appendChild( sizeSpan );
  1914.             }
  1915.             newTreeBottom = sizeSpan;
  1916.         }
  1917.         if ( colour && /^#?([\dA-F]{3}){1,2}$/i.test( colour ) ) {
  1918.             if ( colour.charAt( 0 ) !== '#' ) {
  1919.                 colour = '#' + colour;
  1920.             }
  1921.             colourSpan = createElement( doc, 'SPAN', {
  1922.                 'class': COLOUR_CLASS,
  1923.                 style: 'color:' + colour
  1924.             });
  1925.             if ( !newTreeTop ) {
  1926.                 newTreeTop = colourSpan;
  1927.             }
  1928.             if ( newTreeBottom ) {
  1929.                 newTreeBottom.appendChild( colourSpan );
  1930.             }
  1931.             newTreeBottom = colourSpan;
  1932.         }
  1933.         if ( !newTreeTop ) {
  1934.             newTreeTop = newTreeBottom = createElement( doc, 'SPAN' );
  1935.         }
  1936.         parent.replaceChild( newTreeTop, node );
  1937.         newTreeBottom.appendChild( empty( node ) );
  1938.         return newTreeBottom;
  1939.     },
  1940.     TT: function ( node, parent ) {
  1941.         var el = createElement( node.ownerDocument, 'SPAN', {
  1942.             'class': FONT_FAMILY_CLASS,
  1943.             style: 'font-family:menlo,consolas,"courier new",monospace'
  1944.         });
  1945.         parent.replaceChild( el, node );
  1946.         el.appendChild( empty( node ) );
  1947.         return el;
  1948.     }
  1949. };
  1950.  
  1951. var allowedBlock = /^(?: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)$/;
  1952.  
  1953. var blacklist = /^(?:HEAD|META|STYLE)/;
  1954.  
  1955. var walker = new TreeWalker( null, SHOW_TEXT|SHOW_ELEMENT, function () {
  1956.     return true;
  1957. });
  1958.  
  1959. /*
  1960.     Two purposes:
  1961.  
  1962.     1. Remove nodes we don't want, such as weird <o:p> tags, comment nodes
  1963.        and whitespace nodes.
  1964.     2. Convert inline tags into our preferred format.
  1965. */
  1966. var cleanTree = function cleanTree ( node, preserveWS ) {
  1967.     var children = node.childNodes,
  1968.         nonInlineParent, i, l, child, nodeName, nodeType, rewriter, childLength,
  1969.         startsWithWS, endsWithWS, data, sibling;
  1970.  
  1971.     nonInlineParent = node;
  1972.     while ( isInline( nonInlineParent ) ) {
  1973.         nonInlineParent = nonInlineParent.parentNode;
  1974.     }
  1975.     walker.root = nonInlineParent;
  1976.  
  1977.     for ( i = 0, l = children.length; i < l; i += 1 ) {
  1978.         child = children[i];
  1979.         nodeName = child.nodeName;
  1980.         nodeType = child.nodeType;
  1981.         rewriter = stylesRewriters[ nodeName ];
  1982.         if ( nodeType === ELEMENT_NODE ) {
  1983.             childLength = child.childNodes.length;
  1984.             if ( rewriter ) {
  1985.                 child = rewriter( child, node );
  1986.             } else if ( blacklist.test( nodeName ) ) {
  1987.                 node.removeChild( child );
  1988.                 i -= 1;
  1989.                 l -= 1;
  1990.                 continue;
  1991.             } else if ( !allowedBlock.test( nodeName ) && !isInline( child ) ) {
  1992.                 i -= 1;
  1993.                 l += childLength - 1;
  1994.                 node.replaceChild( empty( child ), child );
  1995.                 continue;
  1996.             }
  1997.             if ( childLength ) {
  1998.                 cleanTree( child, preserveWS || ( nodeName === 'PRE' ) );
  1999.             }
  2000.         } else {
  2001.             if ( nodeType === TEXT_NODE ) {
  2002.                 data = child.data;
  2003.                 startsWithWS = !notWS.test( data.charAt( 0 ) );
  2004.                 endsWithWS = !notWS.test( data.charAt( data.length - 1 ) );
  2005.                 if ( preserveWS || ( !startsWithWS && !endsWithWS ) ) {
  2006.                     continue;
  2007.                 }
  2008.                 // Iterate through the nodes; if we hit some other content
  2009.                 // before the start of a new block we don't trim
  2010.                 if ( startsWithWS ) {
  2011.                     walker.currentNode = child;
  2012.                     while ( sibling = walker.previousPONode() ) {
  2013.                         nodeName = sibling.nodeName;
  2014.                         if ( nodeName === 'IMG' ||
  2015.                                 ( nodeName === '#text' &&
  2016.                                     notWS.test( sibling.data ) ) ) {
  2017.                             break;
  2018.                         }
  2019.                         if ( !isInline( sibling ) ) {
  2020.                             sibling = null;
  2021.                             break;
  2022.                         }
  2023.                     }
  2024.                     data = data.replace( /^[ \t\r\n]+/g, sibling ? ' ' : '' );
  2025.                 }
  2026.                 if ( endsWithWS ) {
  2027.                     walker.currentNode = child;
  2028.                     while ( sibling = walker.nextNode() ) {
  2029.                         if ( nodeName === 'IMG' ||
  2030.                                 ( nodeName === '#text' &&
  2031.                                     notWS.test( sibling.data ) ) ) {
  2032.                             break;
  2033.                         }
  2034.                         if ( !isInline( sibling ) ) {
  2035.                             sibling = null;
  2036.                             break;
  2037.                         }
  2038.                     }
  2039.                     data = data.replace( /[ \t\r\n]+$/g, sibling ? ' ' : '' );
  2040.                 }
  2041.                 if ( data ) {
  2042.                     child.data = data;
  2043.                     continue;
  2044.                 }
  2045.             }
  2046.             node.removeChild( child );
  2047.             i -= 1;
  2048.             l -= 1;
  2049.         }
  2050.     }
  2051.     return node;
  2052. };
  2053.  
  2054. // ---
  2055.  
  2056. var removeEmptyInlines = function removeEmptyInlines ( node ) {
  2057.     var children = node.childNodes,
  2058.         l = children.length,
  2059.         child;
  2060.     while ( l-- ) {
  2061.         child = children[l];
  2062.         if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) {
  2063.             removeEmptyInlines( child );
  2064.             if ( isInline( child ) && !child.firstChild ) {
  2065.                 node.removeChild( child );
  2066.             }
  2067.         } else if ( child.nodeType === TEXT_NODE && !child.data ) {
  2068.             node.removeChild( child );
  2069.         }
  2070.     }
  2071. };
  2072.  
  2073. // ---
  2074.  
  2075. var notWSTextNode = function ( node ) {
  2076.     return node.nodeType === ELEMENT_NODE ?
  2077.         node.nodeName === 'BR' :
  2078.         notWS.test( node.data );
  2079. };
  2080. var isLineBreak = function ( br ) {
  2081.     var block = br.parentNode,
  2082.         walker;
  2083.     while ( isInline( block ) ) {
  2084.         block = block.parentNode;
  2085.     }
  2086.     walker = new TreeWalker(
  2087.         block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode );
  2088.     walker.currentNode = br;
  2089.     return !!walker.nextNode();
  2090. };
  2091.  
  2092. // <br> elements are treated specially, and differently depending on the
  2093. // browser, when in rich text editor mode. When adding HTML from external
  2094. // sources, we must remove them, replacing the ones that actually affect
  2095. // line breaks by wrapping the inline text in a <div>. Browsers that want <br>
  2096. // elements at the end of each block will then have them added back in a later
  2097. // fixCursor method call.
  2098. var cleanupBRs = function ( node, root ) {
  2099.     var brs = node.querySelectorAll( 'BR' ),
  2100.         brBreaksLine = [],
  2101.         l = brs.length,
  2102.         i, br, parent;
  2103.  
  2104.     // Must calculate whether the <br> breaks a line first, because if we
  2105.     // have two <br>s next to each other, after the first one is converted
  2106.     // to a block split, the second will be at the end of a block and
  2107.     // therefore seem to not be a line break. But in its original context it
  2108.     // was, so we should also convert it to a block split.
  2109.     for ( i = 0; i < l; i += 1 ) {
  2110.         brBreaksLine[i] = isLineBreak( brs[i] );
  2111.     }
  2112.     while ( l-- ) {
  2113.         br = brs[l];
  2114.         // Cleanup may have removed it
  2115.         parent = br.parentNode;
  2116.         if ( !parent ) { continue; }
  2117.         // If it doesn't break a line, just remove it; it's not doing
  2118.         // anything useful. We'll add it back later if required by the
  2119.         // browser. If it breaks a line, wrap the content in div tags
  2120.         // and replace the brs.
  2121.         if ( !brBreaksLine[l] ) {
  2122.             detach( br );
  2123.         } else if ( !isInline( parent ) ) {
  2124.             fixContainer( parent, root );
  2125.         }
  2126.     }
  2127. };
  2128.  
  2129. // The (non-standard but supported enough) innerText property is based on the
  2130. // render tree in Firefox and possibly other browsers, so we must insert the
  2131. // DOM node into the document to ensure the text part is correct.
  2132. var setClipboardData = function ( clipboardData, node, root ) {
  2133.     var body = node.ownerDocument.body;
  2134.     var html, text;
  2135.  
  2136.     // Firefox will add an extra new line for BRs at the end of block when
  2137.     // calculating innerText, even though they don't actually affect display.
  2138.     // So we need to remove them first.
  2139.     cleanupBRs( node, root );
  2140.  
  2141.     node.setAttribute( 'style',
  2142.         'position:fixed;overflow:hidden;bottom:100%;right:100%;' );
  2143.     body.appendChild( node );
  2144.     html = node.innerHTML;
  2145.     text = node.innerText || node.textContent;
  2146.  
  2147.     // Firefox (and others?) returns unix line endings (\n) even on Windows.
  2148.     // If on Windows, normalise to \r\n, since Notepad and some other crappy
  2149.     // apps do not understand just \n.
  2150.     if ( isWin ) {
  2151.         text = text.replace( /\r?\n/g, '\r\n' );
  2152.     }
  2153.  
  2154.     clipboardData.setData( 'text/html', html );
  2155.     clipboardData.setData( 'text/plain', text );
  2156.  
  2157.     body.removeChild( node );
  2158. };
  2159.  
  2160. var onCut = function ( event ) {
  2161.     var clipboardData = event.clipboardData;
  2162.     var range = this.getSelection();
  2163.     var node = this.createElement( 'div' );
  2164.     var root = this._root;
  2165.     var self = this;
  2166.  
  2167.     // Save undo checkpoint
  2168.     this.saveUndoState( range );
  2169.  
  2170.     // Edge only seems to support setting plain text as of 2016-03-11.
  2171.     // Mobile Safari flat out doesn't work:
  2172.     // https://bugs.webkit.org/show_bug.cgi?id=143776
  2173.     if ( !isEdge && !isIOS && clipboardData ) {
  2174.         moveRangeBoundariesUpTree( range, root );
  2175.         node.appendChild( deleteContentsOfRange( range, root ) );
  2176.         setClipboardData( clipboardData, node, root );
  2177.         event.preventDefault();
  2178.     } else {
  2179.         setTimeout( function () {
  2180.             try {
  2181.                 // If all content removed, ensure div at start of root.
  2182.                 self._ensureBottomLine();
  2183.             } catch ( error ) {
  2184.                 self.didError( error );
  2185.             }
  2186.         }, 0 );
  2187.     }
  2188.  
  2189.     this.setSelection( range );
  2190. };
  2191.  
  2192. var onCopy = function ( event ) {
  2193.     var clipboardData = event.clipboardData;
  2194.     var range = this.getSelection();
  2195.     var node = this.createElement( 'div' );
  2196.     var root = this._root;
  2197.     var startBlock, endBlock, copyRoot, contents, parent, newContents;
  2198.  
  2199.     // Edge only seems to support setting plain text as of 2016-03-11.
  2200.     // Mobile Safari flat out doesn't work:
  2201.     // https://bugs.webkit.org/show_bug.cgi?id=143776
  2202.     if ( !isEdge && !isIOS && clipboardData ) {
  2203.         range = range.cloneRange();
  2204.         startBlock = getStartBlockOfRange( range, root );
  2205.         endBlock = getEndBlockOfRange( range, root );
  2206.         copyRoot = ( ( startBlock === endBlock ) && startBlock ) || root;
  2207.         moveRangeBoundariesDownTree( range );
  2208.         moveRangeBoundariesUpTree( range, copyRoot );
  2209.         contents = range.cloneContents();
  2210.         parent = range.commonAncestorContainer;
  2211.         if ( parent.nodeType === TEXT_NODE ) {
  2212.             parent = parent.parentNode;
  2213.         }
  2214.         while ( parent && parent !== copyRoot ) {
  2215.             newContents = parent.cloneNode( false );
  2216.             newContents.appendChild( contents );
  2217.             contents = newContents;
  2218.             parent = parent.parentNode;
  2219.         }
  2220.         node.appendChild( contents );
  2221.  
  2222.         setClipboardData( clipboardData, node, root );
  2223.         event.preventDefault();
  2224.     }
  2225. };
  2226.  
  2227. // Need to monitor for shift key like this, as event.shiftKey is not available
  2228. // in paste event.
  2229. function monitorShiftKey ( event ) {
  2230.     this.isShiftDown = event.shiftKey;
  2231. }
  2232.  
  2233. var onPaste = function ( event ) {
  2234.     var clipboardData = event.clipboardData;
  2235.     var items = clipboardData && clipboardData.items;
  2236.     var choosePlain = this.isShiftDown;
  2237.     var fireDrop = false;
  2238.     var hasImage = false;
  2239.     var plainItem = null;
  2240.     var self = this;
  2241.     var l, item, type, types, data;
  2242.  
  2243.     // Current HTML5 Clipboard interface
  2244.     // ---------------------------------
  2245.     // https://html.spec.whatwg.org/multipage/interaction.html
  2246.  
  2247.     // Edge only provides access to plain text as of 2016-03-11.
  2248.     if ( !isEdge && items ) {
  2249.         event.preventDefault();
  2250.         l = items.length;
  2251.         while ( l-- ) {
  2252.             item = items[l];
  2253.             type = item.type;
  2254.             if ( !choosePlain && type === 'text/html' ) {
  2255.                 /*jshint loopfunc: true */
  2256.                 item.getAsString( function ( html ) {
  2257.                     self.insertHTML( html, true );
  2258.                 });
  2259.                 /*jshint loopfunc: false */
  2260.                 return;
  2261.             }
  2262.             if ( type === 'text/plain' ) {
  2263.                 plainItem = item;
  2264.             }
  2265.             if ( !choosePlain && /^image\/.*/.test( type ) ) {
  2266.                 hasImage = true;
  2267.             }
  2268.         }
  2269.         // Treat image paste as a drop of an image file.
  2270.         if ( hasImage ) {
  2271.             this.fireEvent( 'dragover', {
  2272.                 dataTransfer: clipboardData,
  2273.                 /*jshint loopfunc: true */
  2274.                 preventDefault: function () {
  2275.                     fireDrop = true;
  2276.                 }
  2277.                 /*jshint loopfunc: false */
  2278.             });
  2279.             if ( fireDrop ) {
  2280.                 this.fireEvent( 'drop', {
  2281.                     dataTransfer: clipboardData
  2282.                 });
  2283.             }
  2284.         } else if ( plainItem ) {
  2285.             plainItem.getAsString( function ( text ) {
  2286.                 self.insertPlainText( text, true );
  2287.             });
  2288.         }
  2289.         return;
  2290.     }
  2291.  
  2292.     // Old interface
  2293.     // -------------
  2294.  
  2295.     // Safari (and indeed many other OS X apps) copies stuff as text/rtf
  2296.     // rather than text/html; even from a webpage in Safari. The only way
  2297.     // to get an HTML version is to fallback to letting the browser insert
  2298.     // the content. Same for getting image data. *Sigh*.
  2299.     //
  2300.     // Firefox is even worse: it doesn't even let you know that there might be
  2301.     // an RTF version on the clipboard, but it will also convert to HTML if you
  2302.     // let the browser insert the content. I've filed
  2303.     // https://bugzilla.mozilla.org/show_bug.cgi?id=1254028
  2304.     types = clipboardData && clipboardData.types;
  2305.     if ( !isEdge && types && (
  2306.             indexOf.call( types, 'text/html' ) > -1 || (
  2307.                 !isGecko &&
  2308.                 indexOf.call( types, 'text/plain' ) > -1 &&
  2309.                 indexOf.call( types, 'text/rtf' ) < 0 )
  2310.             )) {
  2311.         event.preventDefault();
  2312.         // Abiword on Linux copies a plain text and html version, but the HTML
  2313.         // version is the empty string! So always try to get HTML, but if none,
  2314.         // insert plain text instead. On iOS, Facebook (and possibly other
  2315.         // apps?) copy links as type text/uri-list, but also insert a **blank**
  2316.         // text/plain item onto the clipboard. Why? Who knows.
  2317.         if ( !choosePlain && ( data = clipboardData.getData( 'text/html' ) ) ) {
  2318.             this.insertHTML( data, true );
  2319.         } else if (
  2320.                 ( data = clipboardData.getData( 'text/plain' ) ) ||
  2321.                 ( data = clipboardData.getData( 'text/uri-list' ) ) ) {
  2322.             this.insertPlainText( data, true );
  2323.         }
  2324.         return;
  2325.     }
  2326.  
  2327.     // No interface. Includes all versions of IE :(
  2328.     // --------------------------------------------
  2329.  
  2330.     this._awaitingPaste = true;
  2331.  
  2332.     var body = this._doc.body,
  2333.         range = this.getSelection(),
  2334.         startContainer = range.startContainer,
  2335.         startOffset = range.startOffset,
  2336.         endContainer = range.endContainer,
  2337.         endOffset = range.endOffset;
  2338.  
  2339.     // We need to position the pasteArea in the visible portion of the screen
  2340.     // to stop the browser auto-scrolling.
  2341.     var pasteArea = this.createElement( 'DIV', {
  2342.         contenteditable: 'true',
  2343.         style: 'position:fixed; overflow:hidden; top:0; right:100%; width:1px; height:1px;'
  2344.     });
  2345.     body.appendChild( pasteArea );
  2346.     range.selectNodeContents( pasteArea );
  2347.     this.setSelection( range );
  2348.  
  2349.     // A setTimeout of 0 means this is added to the back of the
  2350.     // single javascript thread, so it will be executed after the
  2351.     // paste event.
  2352.     setTimeout( function () {
  2353.         try {
  2354.             // IE sometimes fires the beforepaste event twice; make sure it is
  2355.             // not run again before our after paste function is called.
  2356.             self._awaitingPaste = false;
  2357.  
  2358.             // Get the pasted content and clean
  2359.             var html = '',
  2360.                 next = pasteArea,
  2361.                 first, range;
  2362.  
  2363.             // #88: Chrome can apparently split the paste area if certain
  2364.             // content is inserted; gather them all up.
  2365.             while ( pasteArea = next ) {
  2366.                 next = pasteArea.nextSibling;
  2367.                 detach( pasteArea );
  2368.                 // Safari and IE like putting extra divs around things.
  2369.                 first = pasteArea.firstChild;
  2370.                 if ( first && first === pasteArea.lastChild &&
  2371.                         first.nodeName === 'DIV' ) {
  2372.                     pasteArea = first;
  2373.                 }
  2374.                 html += pasteArea.innerHTML;
  2375.             }
  2376.  
  2377.             range = self._createRange(
  2378.                 startContainer, startOffset, endContainer, endOffset );
  2379.             self.setSelection( range );
  2380.  
  2381.             if ( html ) {
  2382.                 self.insertHTML( html, true );
  2383.             }
  2384.         } catch ( error ) {
  2385.             self.didError( error );
  2386.         }
  2387.     }, 0 );
  2388. };
  2389.  
  2390. // On Windows you can drag an drop text. We can't handle this ourselves, because
  2391. // as far as I can see, there's no way to get the drop insertion point. So just
  2392. // save an undo state and hope for the best.
  2393. var onDrop = function ( event ) {
  2394.     var types = event.dataTransfer.types;
  2395.     var l = types.length;
  2396.     var hasPlain = false;
  2397.     var hasHTML = false;
  2398.     while ( l-- ) {
  2399.         switch ( types[l] ) {
  2400.         case 'text/plain':
  2401.             hasPlain = true;
  2402.             break;
  2403.         case 'text/html':
  2404.             hasHTML = true;
  2405.             break;
  2406.         default:
  2407.             return;
  2408.         }
  2409.     }
  2410.     if ( hasHTML || hasPlain ) {
  2411.         this.saveUndoState();
  2412.     }
  2413. };
  2414.  
  2415. function mergeObjects ( base, extras, mayOverride ) {
  2416.     var prop, value;
  2417.     if ( !base ) {
  2418.         base = {};
  2419.     }
  2420.     if ( extras ) {
  2421.         for ( prop in extras ) {
  2422.             if ( mayOverride || !( prop in base ) ) {
  2423.                 value = extras[ prop ];
  2424.                 base[ prop ] = ( value && value.constructor === Object ) ?
  2425.                     mergeObjects( base[ prop ], value, mayOverride ) :
  2426.                     value;
  2427.             }
  2428.         }
  2429.     }
  2430.     return base;
  2431. }
  2432.  
  2433. function Squire ( root, config ) {
  2434.     if ( root.nodeType === DOCUMENT_NODE ) {
  2435.         root = root.body;
  2436.     }
  2437.     var doc = root.ownerDocument;
  2438.     var win = doc.defaultView;
  2439.     var mutation;
  2440.  
  2441.     this._win = win;
  2442.     this._doc = doc;
  2443.     this._root = root;
  2444.  
  2445.     this._events = {};
  2446.  
  2447.     this._isFocused = false;
  2448.     this._lastSelection = null;
  2449.  
  2450.     // IE loses selection state of iframe on blur, so make sure we
  2451.     // cache it just before it loses focus.
  2452.     if ( losesSelectionOnBlur ) {
  2453.         this.addEventListener( 'beforedeactivate', this.getSelection );
  2454.     }
  2455.  
  2456.     this._hasZWS = false;
  2457.  
  2458.     this._lastAnchorNode = null;
  2459.     this._lastFocusNode = null;
  2460.     this._path = '';
  2461.     this._willUpdatePath = false;
  2462.  
  2463.     if ( 'onselectionchange' in doc ) {
  2464.         this.addEventListener( 'selectionchange', this._updatePathOnEvent );
  2465.     } else {
  2466.         this.addEventListener( 'keyup', this._updatePathOnEvent );
  2467.         this.addEventListener( 'mouseup', this._updatePathOnEvent );
  2468.     }
  2469.  
  2470.     this._undoIndex = -1;
  2471.     this._undoStack = [];
  2472.     this._undoStackLength = 0;
  2473.     this._isInUndoState = false;
  2474.     this._ignoreChange = false;
  2475.     this._ignoreAllChanges = false;
  2476.  
  2477.     if ( canObserveMutations ) {
  2478.         mutation = new MutationObserver( this._docWasChanged.bind( this ) );
  2479.         mutation.observe( root, {
  2480.             childList: true,
  2481.             attributes: true,
  2482.             characterData: true,
  2483.             subtree: true
  2484.         });
  2485.         this._mutation = mutation;
  2486.     } else {
  2487.         this.addEventListener( 'keyup', this._keyUpDetectChange );
  2488.     }
  2489.  
  2490.     // On blur, restore focus except if the user taps or clicks to focus a
  2491.     // specific point. Can't actually use click event because focus happens
  2492.     // before click, so use mousedown/touchstart
  2493.     this._restoreSelection = false;
  2494.     this.addEventListener( 'blur', enableRestoreSelection );
  2495.     this.addEventListener( 'mousedown', disableRestoreSelection );
  2496.     this.addEventListener( 'touchstart', disableRestoreSelection );
  2497.     this.addEventListener( 'focus', restoreSelection );
  2498.  
  2499.     // IE sometimes fires the beforepaste event twice; make sure it is not run
  2500.     // again before our after paste function is called.
  2501.     this._awaitingPaste = false;
  2502.     this.addEventListener( isIElt11 ? 'beforecut' : 'cut', onCut );
  2503.     this.addEventListener( 'copy', onCopy );
  2504.     this.addEventListener( 'keydown', monitorShiftKey );
  2505.     this.addEventListener( 'keyup', monitorShiftKey );
  2506.     this.addEventListener( isIElt11 ? 'beforepaste' : 'paste', onPaste );
  2507.     this.addEventListener( 'drop', onDrop );
  2508.  
  2509.     // Opera does not fire keydown repeatedly.
  2510.     this.addEventListener( isPresto ? 'keypress' : 'keydown', onKey );
  2511.  
  2512.     // Add key handlers
  2513.     this._keyHandlers = Object.create( keyHandlers );
  2514.  
  2515.     // Override default properties
  2516.     this.setConfig( config );
  2517.  
  2518.     // Fix IE<10's buggy implementation of Text#splitText.
  2519.     // If the split is at the end of the node, it doesn't insert the newly split
  2520.     // node into the document, and sets its value to undefined rather than ''.
  2521.     // And even if the split is not at the end, the original node is removed
  2522.     // from the document and replaced by another, rather than just having its
  2523.     // data shortened.
  2524.     // We used to feature test for this, but then found the feature test would
  2525.     // sometimes pass, but later on the buggy behaviour would still appear.
  2526.     // I think IE10 does not have the same bug, but it doesn't hurt to replace
  2527.     // its native fn too and then we don't need yet another UA category.
  2528.     if ( isIElt11 ) {
  2529.         win.Text.prototype.splitText = function ( offset ) {
  2530.             var afterSplit = this.ownerDocument.createTextNode(
  2531.                     this.data.slice( offset ) ),
  2532.                 next = this.nextSibling,
  2533.                 parent = this.parentNode,
  2534.                 toDelete = this.length - offset;
  2535.             if ( next ) {
  2536.                 parent.insertBefore( afterSplit, next );
  2537.             } else {
  2538.                 parent.appendChild( afterSplit );
  2539.             }
  2540.             if ( toDelete ) {
  2541.                 this.deleteData( offset, toDelete );
  2542.             }
  2543.             return afterSplit;
  2544.         };
  2545.     }
  2546.  
  2547.     root.setAttribute( 'contenteditable', 'true' );
  2548.  
  2549.     // Remove Firefox's built-in controls
  2550.     try {
  2551.         doc.execCommand( 'enableObjectResizing', false, 'false' );
  2552.         doc.execCommand( 'enableInlineTableEditing', false, 'false' );
  2553.     } catch ( error ) {}
  2554.  
  2555.     root.__squire__ = this;
  2556.  
  2557.     // Need to register instance before calling setHTML, so that the fixCursor
  2558.     // function can lookup any default block tag options set.
  2559.     this.setHTML( '' );
  2560. }
  2561.  
  2562. var proto = Squire.prototype;
  2563.  
  2564. var sanitizeToDOMFragment = function ( html, isPaste, self ) {
  2565.     var doc = self._doc;
  2566.     var frag = html ? DOMPurify.sanitize( html, {
  2567.         WHOLE_DOCUMENT: false,
  2568.         RETURN_DOM: true,
  2569.         RETURN_DOM_FRAGMENT: true
  2570.     }) : null;
  2571.     return frag ? doc.importNode( frag, true ) : doc.createDocumentFragment();
  2572. };
  2573.  
  2574. proto.setConfig = function ( config ) {
  2575.     config = mergeObjects({
  2576.         blockTag: 'DIV',
  2577.         blockAttributes: null,
  2578.         tagAttributes: {
  2579.             blockquote: null,
  2580.             ul: null,
  2581.             ol: null,
  2582.             li: null,
  2583.             a: null
  2584.         },
  2585.         leafNodeNames: leafNodeNames,
  2586.         undo: {
  2587.             documentSizeThreshold: -1, // -1 means no threshold
  2588.             undoLimit: -1 // -1 means no limit
  2589.         },
  2590.         isInsertedHTMLSanitized: true,
  2591.         isSetHTMLSanitized: true,
  2592.         sanitizeToDOMFragment:
  2593.             typeof DOMPurify !== 'undefined' && DOMPurify.isSupported ?
  2594.             sanitizeToDOMFragment : null
  2595.  
  2596.     }, config, true );
  2597.  
  2598.     // Users may specify block tag in lower case
  2599.     config.blockTag = config.blockTag.toUpperCase();
  2600.  
  2601.     this._config = config;
  2602.  
  2603.     return this;
  2604. };
  2605.  
  2606. proto.createElement = function ( tag, props, children ) {
  2607.     return createElement( this._doc, tag, props, children );
  2608. };
  2609.  
  2610. proto.createDefaultBlock = function ( children ) {
  2611.     var config = this._config;
  2612.     return fixCursor(
  2613.         this.createElement( config.blockTag, config.blockAttributes, children ),
  2614.         this._root
  2615.     );
  2616. };
  2617.  
  2618. proto.didError = function ( error ) {
  2619.     console.log( error );
  2620. };
  2621.  
  2622. proto.getDocument = function () {
  2623.     return this._doc;
  2624. };
  2625. proto.getRoot = function () {
  2626.     return this._root;
  2627. };
  2628.  
  2629. proto.modifyDocument = function ( modificationCallback ) {
  2630.     var mutation = this._mutation;
  2631.     if ( mutation ) {
  2632.         if ( mutation.takeRecords().length ) {
  2633.             this._docWasChanged();
  2634.         }
  2635.         mutation.disconnect();
  2636.     }
  2637.  
  2638.     this._ignoreAllChanges = true;
  2639.     modificationCallback();
  2640.     this._ignoreAllChanges = false;
  2641.  
  2642.     if ( mutation ) {
  2643.         mutation.observe( this._root, {
  2644.             childList: true,
  2645.             attributes: true,
  2646.             characterData: true,
  2647.             subtree: true
  2648.         });
  2649.         this._ignoreChange = false;
  2650.     }
  2651. };
  2652.  
  2653. // --- Events ---
  2654.  
  2655. // Subscribing to these events won't automatically add a listener to the
  2656. // document node, since these events are fired in a custom manner by the
  2657. // editor code.
  2658. var customEvents = {
  2659.     pathChange: 1, select: 1, input: 1, undoStateChange: 1
  2660. };
  2661.  
  2662. proto.fireEvent = function ( type, event ) {
  2663.     var handlers = this._events[ type ];
  2664.     var isFocused, l, obj;
  2665.     // UI code, especially modal views, may be monitoring for focus events and
  2666.     // immediately removing focus. In certain conditions, this can cause the
  2667.     // focus event to fire after the blur event, which can cause an infinite
  2668.     // loop. So we detect whether we're actually focused/blurred before firing.
  2669.     if ( /^(?:focus|blur)/.test( type ) ) {
  2670.         isFocused = isOrContains( this._root, this._doc.activeElement );
  2671.         if ( type === 'focus' ) {
  2672.             if ( !isFocused || this._isFocused ) {
  2673.                 return this;
  2674.             }
  2675.             this._isFocused = true;
  2676.         } else {
  2677.             if ( isFocused || !this._isFocused ) {
  2678.                 return this;
  2679.             }
  2680.             this._isFocused = false;
  2681.         }
  2682.     }
  2683.     if ( handlers ) {
  2684.         if ( !event ) {
  2685.             event = {};
  2686.         }
  2687.         if ( event.type !== type ) {
  2688.             event.type = type;
  2689.         }
  2690.         // Clone handlers array, so any handlers added/removed do not affect it.
  2691.         handlers = handlers.slice();
  2692.         l = handlers.length;
  2693.         while ( l-- ) {
  2694.             obj = handlers[l];
  2695.             try {
  2696.                 if ( obj.handleEvent ) {
  2697.                     obj.handleEvent( event );
  2698.                 } else {
  2699.                     obj.call( this, event );
  2700.                 }
  2701.             } catch ( error ) {
  2702.                 error.details = 'Squire: fireEvent error. Event type: ' + type;
  2703.                 this.didError( error );
  2704.             }
  2705.         }
  2706.     }
  2707.     return this;
  2708. };
  2709.  
  2710. proto.destroy = function () {
  2711.     var events = this._events;
  2712.     var type;
  2713.  
  2714.     for ( type in events ) {
  2715.         this.removeEventListener( type );
  2716.     }
  2717.     if ( this._mutation ) {
  2718.         this._mutation.disconnect();
  2719.     }
  2720.     delete this._root.__squire__;
  2721.  
  2722.     // Destroy undo stack
  2723.     this._undoIndex = -1;
  2724.     this._undoStack = [];
  2725.     this._undoStackLength = 0;
  2726. };
  2727.  
  2728. proto.handleEvent = function ( event ) {
  2729.     this.fireEvent( event.type, event );
  2730. };
  2731.  
  2732. proto.addEventListener = function ( type, fn ) {
  2733.     var handlers = this._events[ type ];
  2734.     var target = this._root;
  2735.     if ( !fn ) {
  2736.         this.didError({
  2737.             name: 'Squire: addEventListener with null or undefined fn',
  2738.             message: 'Event type: ' + type
  2739.         });
  2740.         return this;
  2741.     }
  2742.     if ( !handlers ) {
  2743.         handlers = this._events[ type ] = [];
  2744.         if ( !customEvents[ type ] ) {
  2745.             if ( type === 'selectionchange' ) {
  2746.                 target = this._doc;
  2747.             }
  2748.             target.addEventListener( type, this, true );
  2749.         }
  2750.     }
  2751.     handlers.push( fn );
  2752.     return this;
  2753. };
  2754.  
  2755. proto.removeEventListener = function ( type, fn ) {
  2756.     var handlers = this._events[ type ];
  2757.     var target = this._root;
  2758.     var l;
  2759.     if ( handlers ) {
  2760.         if ( fn ) {
  2761.             l = handlers.length;
  2762.             while ( l-- ) {
  2763.                 if ( handlers[l] === fn ) {
  2764.                     handlers.splice( l, 1 );
  2765.                 }
  2766.             }
  2767.         } else {
  2768.             handlers.length = 0;
  2769.         }
  2770.         if ( !handlers.length ) {
  2771.             delete this._events[ type ];
  2772.             if ( !customEvents[ type ] ) {
  2773.                 if ( type === 'selectionchange' ) {
  2774.                     target = this._doc;
  2775.                 }
  2776.                 target.removeEventListener( type, this, true );
  2777.             }
  2778.         }
  2779.     }
  2780.     return this;
  2781. };
  2782.  
  2783. // --- Selection and Path ---
  2784.  
  2785. proto._createRange =
  2786.         function ( range, startOffset, endContainer, endOffset ) {
  2787.     if ( range instanceof this._win.Range ) {
  2788.         return range.cloneRange();
  2789.     }
  2790.     var domRange = this._doc.createRange();
  2791.     domRange.setStart( range, startOffset );
  2792.     if ( endContainer ) {
  2793.         domRange.setEnd( endContainer, endOffset );
  2794.     } else {
  2795.         domRange.setEnd( range, startOffset );
  2796.     }
  2797.     return domRange;
  2798. };
  2799.  
  2800. proto.getCursorPosition = function ( range ) {
  2801.     if ( ( !range && !( range = this.getSelection() ) ) ||
  2802.             !range.getBoundingClientRect ) {
  2803.         return null;
  2804.     }
  2805.     // Get the bounding rect
  2806.     var rect = range.getBoundingClientRect();
  2807.     var node, parent;
  2808.     if ( rect && !rect.top ) {
  2809.         this._ignoreChange = true;
  2810.         node = this._doc.createElement( 'SPAN' );
  2811.         node.textContent = ZWS;
  2812.         insertNodeInRange( range, node );
  2813.         rect = node.getBoundingClientRect();
  2814.         parent = node.parentNode;
  2815.         parent.removeChild( node );
  2816.         mergeInlines( parent, range );
  2817.     }
  2818.     return rect;
  2819. };
  2820.  
  2821. proto._moveCursorTo = function ( toStart ) {
  2822.     var root = this._root,
  2823.         range = this._createRange( root, toStart ? 0 : root.childNodes.length );
  2824.     moveRangeBoundariesDownTree( range );
  2825.     this.setSelection( range );
  2826.     return this;
  2827. };
  2828. proto.moveCursorToStart = function () {
  2829.     return this._moveCursorTo( true );
  2830. };
  2831. proto.moveCursorToEnd = function () {
  2832.     return this._moveCursorTo( false );
  2833. };
  2834.  
  2835. var getWindowSelection = function ( self ) {
  2836.     return self._win.getSelection() || null;
  2837. };
  2838.  
  2839. proto.setSelection = function ( range ) {
  2840.     if ( range ) {
  2841.         this._lastSelection = range;
  2842.         // If we're setting selection, that automatically, and synchronously, // triggers a focus event. So just store the selection and mark it as
  2843.         // needing restore on focus.
  2844.         if ( !this._isFocused ) {
  2845.             enableRestoreSelection.call( this );
  2846.         } else if ( isAndroid && !this._restoreSelection ) {
  2847.             // Android closes the keyboard on removeAllRanges() and doesn't
  2848.             // open it again when addRange() is called, sigh.
  2849.             // Since Android doesn't trigger a focus event in setSelection(),
  2850.             // use a blur/focus dance to work around this by letting the
  2851.             // selection be restored on focus.
  2852.             // Need to check for !this._restoreSelection to avoid infinite loop
  2853.             enableRestoreSelection.call( this );
  2854.             this.blur();
  2855.             this.focus();
  2856.         } else {
  2857.             // iOS bug: if you don't focus the iframe before setting the
  2858.             // selection, you can end up in a state where you type but the input
  2859.             // doesn't get directed into the contenteditable area but is instead
  2860.             // lost in a black hole. Very strange.
  2861.             if ( isIOS ) {
  2862.                 this._win.focus();
  2863.             }
  2864.             var sel = getWindowSelection( this );
  2865.             if ( sel ) {
  2866.                 sel.removeAllRanges();
  2867.                 sel.addRange( range );
  2868.             }
  2869.         }
  2870.     }
  2871.     return this;
  2872. };
  2873.  
  2874. proto.getSelection = function () {
  2875.     var sel = getWindowSelection( this );
  2876.     var root = this._root;
  2877.     var selection, startContainer, endContainer;
  2878.     if ( sel && sel.rangeCount ) {
  2879.         selection  = sel.getRangeAt( 0 ).cloneRange();
  2880.         startContainer = selection.startContainer;
  2881.         endContainer = selection.endContainer;
  2882.         // FF can return the selection as being inside an <img>. WTF?
  2883.         if ( startContainer && isLeaf( startContainer ) ) {
  2884.             selection.setStartBefore( startContainer );
  2885.         }
  2886.         if ( endContainer && isLeaf( endContainer ) ) {
  2887.             selection.setEndBefore( endContainer );
  2888.         }
  2889.     }
  2890.     if ( selection &&
  2891.             isOrContains( root, selection.commonAncestorContainer ) ) {
  2892.         this._lastSelection = selection;
  2893.     } else {
  2894.         selection = this._lastSelection;
  2895.     }
  2896.     if ( !selection ) {
  2897.         selection = this._createRange( root.firstChild, 0 );
  2898.     }
  2899.     return selection;
  2900. };
  2901.  
  2902. function enableRestoreSelection () {
  2903.     this._restoreSelection = true;
  2904. }
  2905. function disableRestoreSelection () {
  2906.     this._restoreSelection = false;
  2907. }
  2908. function restoreSelection () {
  2909.     if ( this._restoreSelection ) {
  2910.         this.setSelection( this._lastSelection );
  2911.     }
  2912. }
  2913.  
  2914. proto.getSelectedText = function () {
  2915.     var range = this.getSelection(),
  2916.         walker = new TreeWalker(
  2917.             range.commonAncestorContainer,
  2918.             SHOW_TEXT|SHOW_ELEMENT,
  2919.             function ( node ) {
  2920.                 return isNodeContainedInRange( range, node, true );
  2921.             }
  2922.         ),
  2923.         startContainer = range.startContainer,
  2924.         endContainer = range.endContainer,
  2925.         node = walker.currentNode = startContainer,
  2926.         textContent = '',
  2927.         addedTextInBlock = false,
  2928.         value;
  2929.  
  2930.     if ( !walker.filter( node ) ) {
  2931.         node = walker.nextNode();
  2932.     }
  2933.  
  2934.     while ( node ) {
  2935.         if ( node.nodeType === TEXT_NODE ) {
  2936.             value = node.data;
  2937.             if ( value && ( /\S/.test( value ) ) ) {
  2938.                 if ( node === endContainer ) {
  2939.                     value = value.slice( 0, range.endOffset );
  2940.                 }
  2941.                 if ( node === startContainer ) {
  2942.                     value = value.slice( range.startOffset );