BVB Source Codes

Squire Show Editor.js Source code

Return Download Squire: download Editor.js Source code - Download Squire Source code - Type:.js
  1. /*jshint strict:false, undef:false, unused:false */
  2.  
  3. function mergeObjects ( base, extras, mayOverride ) {
  4.     var prop, value;
  5.     if ( !base ) {
  6.         base = {};
  7.     }
  8.     if ( extras ) {
  9.         for ( prop in extras ) {
  10.             if ( mayOverride || !( prop in base ) ) {
  11.                 value = extras[ prop ];
  12.                 base[ prop ] = ( value && value.constructor === Object ) ?
  13.                     mergeObjects( base[ prop ], value, mayOverride ) :
  14.                     value;
  15.             }
  16.         }
  17.     }
  18.     return base;
  19. }
  20.  
  21. function Squire ( root, config ) {
  22.     if ( root.nodeType === DOCUMENT_NODE ) {
  23.         root = root.body;
  24.     }
  25.     var doc = root.ownerDocument;
  26.     var win = doc.defaultView;
  27.     var mutation;
  28.  
  29.     this._win = win;
  30.     this._doc = doc;
  31.     this._root = root;
  32.  
  33.     this._events = {};
  34.  
  35.     this._isFocused = false;
  36.     this._lastSelection = null;
  37.  
  38.     // IE loses selection state of iframe on blur, so make sure we
  39.     // cache it just before it loses focus.
  40.     if ( losesSelectionOnBlur ) {
  41.         this.addEventListener( 'beforedeactivate', this.getSelection );
  42.     }
  43.  
  44.     this._hasZWS = false;
  45.  
  46.     this._lastAnchorNode = null;
  47.     this._lastFocusNode = null;
  48.     this._path = '';
  49.     this._willUpdatePath = false;
  50.  
  51.     if ( 'onselectionchange' in doc ) {
  52.         this.addEventListener( 'selectionchange', this._updatePathOnEvent );
  53.     } else {
  54.         this.addEventListener( 'keyup', this._updatePathOnEvent );
  55.         this.addEventListener( 'mouseup', this._updatePathOnEvent );
  56.     }
  57.  
  58.     this._undoIndex = -1;
  59.     this._undoStack = [];
  60.     this._undoStackLength = 0;
  61.     this._isInUndoState = false;
  62.     this._ignoreChange = false;
  63.     this._ignoreAllChanges = false;
  64.  
  65.     if ( canObserveMutations ) {
  66.         mutation = new MutationObserver( this._docWasChanged.bind( this ) );
  67.         mutation.observe( root, {
  68.             childList: true,
  69.             attributes: true,
  70.             characterData: true,
  71.             subtree: true
  72.         });
  73.         this._mutation = mutation;
  74.     } else {
  75.         this.addEventListener( 'keyup', this._keyUpDetectChange );
  76.     }
  77.  
  78.     // On blur, restore focus except if the user taps or clicks to focus a
  79.     // specific point. Can't actually use click event because focus happens
  80.     // before click, so use mousedown/touchstart
  81.     this._restoreSelection = false;
  82.     this.addEventListener( 'blur', enableRestoreSelection );
  83.     this.addEventListener( 'mousedown', disableRestoreSelection );
  84.     this.addEventListener( 'touchstart', disableRestoreSelection );
  85.     this.addEventListener( 'focus', restoreSelection );
  86.  
  87.     // IE sometimes fires the beforepaste event twice; make sure it is not run
  88.     // again before our after paste function is called.
  89.     this._awaitingPaste = false;
  90.     this.addEventListener( isIElt11 ? 'beforecut' : 'cut', onCut );
  91.     this.addEventListener( 'copy', onCopy );
  92.     this.addEventListener( 'keydown', monitorShiftKey );
  93.     this.addEventListener( 'keyup', monitorShiftKey );
  94.     this.addEventListener( isIElt11 ? 'beforepaste' : 'paste', onPaste );
  95.     this.addEventListener( 'drop', onDrop );
  96.  
  97.     // Opera does not fire keydown repeatedly.
  98.     this.addEventListener( isPresto ? 'keypress' : 'keydown', onKey );
  99.  
  100.     // Add key handlers
  101.     this._keyHandlers = Object.create( keyHandlers );
  102.  
  103.     // Override default properties
  104.     this.setConfig( config );
  105.  
  106.     // Fix IE<10's buggy implementation of Text#splitText.
  107.     // If the split is at the end of the node, it doesn't insert the newly split
  108.     // node into the document, and sets its value to undefined rather than ''.
  109.     // And even if the split is not at the end, the original node is removed
  110.     // from the document and replaced by another, rather than just having its
  111.     // data shortened.
  112.     // We used to feature test for this, but then found the feature test would
  113.     // sometimes pass, but later on the buggy behaviour would still appear.
  114.     // I think IE10 does not have the same bug, but it doesn't hurt to replace
  115.     // its native fn too and then we don't need yet another UA category.
  116.     if ( isIElt11 ) {
  117.         win.Text.prototype.splitText = function ( offset ) {
  118.             var afterSplit = this.ownerDocument.createTextNode(
  119.                     this.data.slice( offset ) ),
  120.                 next = this.nextSibling,
  121.                 parent = this.parentNode,
  122.                 toDelete = this.length - offset;
  123.             if ( next ) {
  124.                 parent.insertBefore( afterSplit, next );
  125.             } else {
  126.                 parent.appendChild( afterSplit );
  127.             }
  128.             if ( toDelete ) {
  129.                 this.deleteData( offset, toDelete );
  130.             }
  131.             return afterSplit;
  132.         };
  133.     }
  134.  
  135.     root.setAttribute( 'contenteditable', 'true' );
  136.  
  137.     // Remove Firefox's built-in controls
  138.     try {
  139.         doc.execCommand( 'enableObjectResizing', false, 'false' );
  140.         doc.execCommand( 'enableInlineTableEditing', false, 'false' );
  141.     } catch ( error ) {}
  142.  
  143.     root.__squire__ = this;
  144.  
  145.     // Need to register instance before calling setHTML, so that the fixCursor
  146.     // function can lookup any default block tag options set.
  147.     this.setHTML( '' );
  148. }
  149.  
  150. var proto = Squire.prototype;
  151.  
  152. var sanitizeToDOMFragment = function ( html, isPaste, self ) {
  153.     var doc = self._doc;
  154.     var frag = html ? DOMPurify.sanitize( html, {
  155.         WHOLE_DOCUMENT: false,
  156.         RETURN_DOM: true,
  157.         RETURN_DOM_FRAGMENT: true
  158.     }) : null;
  159.     return frag ? doc.importNode( frag, true ) : doc.createDocumentFragment();
  160. };
  161.  
  162. proto.setConfig = function ( config ) {
  163.     config = mergeObjects({
  164.         blockTag: 'DIV',
  165.         blockAttributes: null,
  166.         tagAttributes: {
  167.             blockquote: null,
  168.             ul: null,
  169.             ol: null,
  170.             li: null,
  171.             a: null
  172.         },
  173.         leafNodeNames: leafNodeNames,
  174.         undo: {
  175.             documentSizeThreshold: -1, // -1 means no threshold
  176.             undoLimit: -1 // -1 means no limit
  177.         },
  178.         isInsertedHTMLSanitized: true,
  179.         isSetHTMLSanitized: true,
  180.         sanitizeToDOMFragment:
  181.             typeof DOMPurify !== 'undefined' && DOMPurify.isSupported ?
  182.             sanitizeToDOMFragment : null
  183.  
  184.     }, config, true );
  185.  
  186.     // Users may specify block tag in lower case
  187.     config.blockTag = config.blockTag.toUpperCase();
  188.  
  189.     this._config = config;
  190.  
  191.     return this;
  192. };
  193.  
  194. proto.createElement = function ( tag, props, children ) {
  195.     return createElement( this._doc, tag, props, children );
  196. };
  197.  
  198. proto.createDefaultBlock = function ( children ) {
  199.     var config = this._config;
  200.     return fixCursor(
  201.         this.createElement( config.blockTag, config.blockAttributes, children ),
  202.         this._root
  203.     );
  204. };
  205.  
  206. proto.didError = function ( error ) {
  207.     console.log( error );
  208. };
  209.  
  210. proto.getDocument = function () {
  211.     return this._doc;
  212. };
  213. proto.getRoot = function () {
  214.     return this._root;
  215. };
  216.  
  217. proto.modifyDocument = function ( modificationCallback ) {
  218.     var mutation = this._mutation;
  219.     if ( mutation ) {
  220.         if ( mutation.takeRecords().length ) {
  221.             this._docWasChanged();
  222.         }
  223.         mutation.disconnect();
  224.     }
  225.  
  226.     this._ignoreAllChanges = true;
  227.     modificationCallback();
  228.     this._ignoreAllChanges = false;
  229.  
  230.     if ( mutation ) {
  231.         mutation.observe( this._root, {
  232.             childList: true,
  233.             attributes: true,
  234.             characterData: true,
  235.             subtree: true
  236.         });
  237.         this._ignoreChange = false;
  238.     }
  239. };
  240.  
  241. // --- Events ---
  242.  
  243. // Subscribing to these events won't automatically add a listener to the
  244. // document node, since these events are fired in a custom manner by the
  245. // editor code.
  246. var customEvents = {
  247.     pathChange: 1, select: 1, input: 1, undoStateChange: 1
  248. };
  249.  
  250. proto.fireEvent = function ( type, event ) {
  251.     var handlers = this._events[ type ];
  252.     var isFocused, l, obj;
  253.     // UI code, especially modal views, may be monitoring for focus events and
  254.     // immediately removing focus. In certain conditions, this can cause the
  255.     // focus event to fire after the blur event, which can cause an infinite
  256.     // loop. So we detect whether we're actually focused/blurred before firing.
  257.     if ( /^(?:focus|blur)/.test( type ) ) {
  258.         isFocused = isOrContains( this._root, this._doc.activeElement );
  259.         if ( type === 'focus' ) {
  260.             if ( !isFocused || this._isFocused ) {
  261.                 return this;
  262.             }
  263.             this._isFocused = true;
  264.         } else {
  265.             if ( isFocused || !this._isFocused ) {
  266.                 return this;
  267.             }
  268.             this._isFocused = false;
  269.         }
  270.     }
  271.     if ( handlers ) {
  272.         if ( !event ) {
  273.             event = {};
  274.         }
  275.         if ( event.type !== type ) {
  276.             event.type = type;
  277.         }
  278.         // Clone handlers array, so any handlers added/removed do not affect it.
  279.         handlers = handlers.slice();
  280.         l = handlers.length;
  281.         while ( l-- ) {
  282.             obj = handlers[l];
  283.             try {
  284.                 if ( obj.handleEvent ) {
  285.                     obj.handleEvent( event );
  286.                 } else {
  287.                     obj.call( this, event );
  288.                 }
  289.             } catch ( error ) {
  290.                 error.details = 'Squire: fireEvent error. Event type: ' + type;
  291.                 this.didError( error );
  292.             }
  293.         }
  294.     }
  295.     return this;
  296. };
  297.  
  298. proto.destroy = function () {
  299.     var events = this._events;
  300.     var type;
  301.  
  302.     for ( type in events ) {
  303.         this.removeEventListener( type );
  304.     }
  305.     if ( this._mutation ) {
  306.         this._mutation.disconnect();
  307.     }
  308.     delete this._root.__squire__;
  309.  
  310.     // Destroy undo stack
  311.     this._undoIndex = -1;
  312.     this._undoStack = [];
  313.     this._undoStackLength = 0;
  314. };
  315.  
  316. proto.handleEvent = function ( event ) {
  317.     this.fireEvent( event.type, event );
  318. };
  319.  
  320. proto.addEventListener = function ( type, fn ) {
  321.     var handlers = this._events[ type ];
  322.     var target = this._root;
  323.     if ( !fn ) {
  324.         this.didError({
  325.             name: 'Squire: addEventListener with null or undefined fn',
  326.             message: 'Event type: ' + type
  327.         });
  328.         return this;
  329.     }
  330.     if ( !handlers ) {
  331.         handlers = this._events[ type ] = [];
  332.         if ( !customEvents[ type ] ) {
  333.             if ( type === 'selectionchange' ) {
  334.                 target = this._doc;
  335.             }
  336.             target.addEventListener( type, this, true );
  337.         }
  338.     }
  339.     handlers.push( fn );
  340.     return this;
  341. };
  342.  
  343. proto.removeEventListener = function ( type, fn ) {
  344.     var handlers = this._events[ type ];
  345.     var target = this._root;
  346.     var l;
  347.     if ( handlers ) {
  348.         if ( fn ) {
  349.             l = handlers.length;
  350.             while ( l-- ) {
  351.                 if ( handlers[l] === fn ) {
  352.                     handlers.splice( l, 1 );
  353.                 }
  354.             }
  355.         } else {
  356.             handlers.length = 0;
  357.         }
  358.         if ( !handlers.length ) {
  359.             delete this._events[ type ];
  360.             if ( !customEvents[ type ] ) {
  361.                 if ( type === 'selectionchange' ) {
  362.                     target = this._doc;
  363.                 }
  364.                 target.removeEventListener( type, this, true );
  365.             }
  366.         }
  367.     }
  368.     return this;
  369. };
  370.  
  371. // --- Selection and Path ---
  372.  
  373. proto._createRange =
  374.         function ( range, startOffset, endContainer, endOffset ) {
  375.     if ( range instanceof this._win.Range ) {
  376.         return range.cloneRange();
  377.     }
  378.     var domRange = this._doc.createRange();
  379.     domRange.setStart( range, startOffset );
  380.     if ( endContainer ) {
  381.         domRange.setEnd( endContainer, endOffset );
  382.     } else {
  383.         domRange.setEnd( range, startOffset );
  384.     }
  385.     return domRange;
  386. };
  387.  
  388. proto.getCursorPosition = function ( range ) {
  389.     if ( ( !range && !( range = this.getSelection() ) ) ||
  390.             !range.getBoundingClientRect ) {
  391.         return null;
  392.     }
  393.     // Get the bounding rect
  394.     var rect = range.getBoundingClientRect();
  395.     var node, parent;
  396.     if ( rect && !rect.top ) {
  397.         this._ignoreChange = true;
  398.         node = this._doc.createElement( 'SPAN' );
  399.         node.textContent = ZWS;
  400.         insertNodeInRange( range, node );
  401.         rect = node.getBoundingClientRect();
  402.         parent = node.parentNode;
  403.         parent.removeChild( node );
  404.         mergeInlines( parent, range );
  405.     }
  406.     return rect;
  407. };
  408.  
  409. proto._moveCursorTo = function ( toStart ) {
  410.     var root = this._root,
  411.         range = this._createRange( root, toStart ? 0 : root.childNodes.length );
  412.     moveRangeBoundariesDownTree( range );
  413.     this.setSelection( range );
  414.     return this;
  415. };
  416. proto.moveCursorToStart = function () {
  417.     return this._moveCursorTo( true );
  418. };
  419. proto.moveCursorToEnd = function () {
  420.     return this._moveCursorTo( false );
  421. };
  422.  
  423. var getWindowSelection = function ( self ) {
  424.     return self._win.getSelection() || null;
  425. };
  426.  
  427. proto.setSelection = function ( range ) {
  428.     if ( range ) {
  429.         this._lastSelection = range;
  430.         // If we're setting selection, that automatically, and synchronously, // triggers a focus event. So just store the selection and mark it as
  431.         // needing restore on focus.
  432.         if ( !this._isFocused ) {
  433.             enableRestoreSelection.call( this );
  434.         } else if ( isAndroid && !this._restoreSelection ) {
  435.             // Android closes the keyboard on removeAllRanges() and doesn't
  436.             // open it again when addRange() is called, sigh.
  437.             // Since Android doesn't trigger a focus event in setSelection(),
  438.             // use a blur/focus dance to work around this by letting the
  439.             // selection be restored on focus.
  440.             // Need to check for !this._restoreSelection to avoid infinite loop
  441.             enableRestoreSelection.call( this );
  442.             this.blur();
  443.             this.focus();
  444.         } else {
  445.             // iOS bug: if you don't focus the iframe before setting the
  446.             // selection, you can end up in a state where you type but the input
  447.             // doesn't get directed into the contenteditable area but is instead
  448.             // lost in a black hole. Very strange.
  449.             if ( isIOS ) {
  450.                 this._win.focus();
  451.             }
  452.             var sel = getWindowSelection( this );
  453.             if ( sel ) {
  454.                 sel.removeAllRanges();
  455.                 sel.addRange( range );
  456.             }
  457.         }
  458.     }
  459.     return this;
  460. };
  461.  
  462. proto.getSelection = function () {
  463.     var sel = getWindowSelection( this );
  464.     var root = this._root;
  465.     var selection, startContainer, endContainer;
  466.     if ( sel && sel.rangeCount ) {
  467.         selection  = sel.getRangeAt( 0 ).cloneRange();
  468.         startContainer = selection.startContainer;
  469.         endContainer = selection.endContainer;
  470.         // FF can return the selection as being inside an <img>. WTF?
  471.         if ( startContainer && isLeaf( startContainer ) ) {
  472.             selection.setStartBefore( startContainer );
  473.         }
  474.         if ( endContainer && isLeaf( endContainer ) ) {
  475.             selection.setEndBefore( endContainer );
  476.         }
  477.     }
  478.     if ( selection &&
  479.             isOrContains( root, selection.commonAncestorContainer ) ) {
  480.         this._lastSelection = selection;
  481.     } else {
  482.         selection = this._lastSelection;
  483.     }
  484.     if ( !selection ) {
  485.         selection = this._createRange( root.firstChild, 0 );
  486.     }
  487.     return selection;
  488. };
  489.  
  490. function enableRestoreSelection () {
  491.     this._restoreSelection = true;
  492. }
  493. function disableRestoreSelection () {
  494.     this._restoreSelection = false;
  495. }
  496. function restoreSelection () {
  497.     if ( this._restoreSelection ) {
  498.         this.setSelection( this._lastSelection );
  499.     }
  500. }
  501.  
  502. proto.getSelectedText = function () {
  503.     var range = this.getSelection(),
  504.         walker = new TreeWalker(
  505.             range.commonAncestorContainer,
  506.             SHOW_TEXT|SHOW_ELEMENT,
  507.             function ( node ) {
  508.                 return isNodeContainedInRange( range, node, true );
  509.             }
  510.         ),
  511.         startContainer = range.startContainer,
  512.         endContainer = range.endContainer,
  513.         node = walker.currentNode = startContainer,
  514.         textContent = '',
  515.         addedTextInBlock = false,
  516.         value;
  517.  
  518.     if ( !walker.filter( node ) ) {
  519.         node = walker.nextNode();
  520.     }
  521.  
  522.     while ( node ) {
  523.         if ( node.nodeType === TEXT_NODE ) {
  524.             value = node.data;
  525.             if ( value && ( /\S/.test( value ) ) ) {
  526.                 if ( node === endContainer ) {
  527.                     value = value.slice( 0, range.endOffset );
  528.                 }
  529.                 if ( node === startContainer ) {
  530.                     value = value.slice( range.startOffset );
  531.                 }
  532.                 textContent += value;
  533.                 addedTextInBlock = true;
  534.             }
  535.         } else if ( node.nodeName === 'BR' ||
  536.                 addedTextInBlock && !isInline( node ) ) {
  537.             textContent += '\n';
  538.             addedTextInBlock = false;
  539.         }
  540.         node = walker.nextNode();
  541.     }
  542.  
  543.     return textContent;
  544. };
  545.  
  546. proto.getPath = function () {
  547.     return this._path;
  548. };
  549.  
  550. // --- Workaround for browsers that can't focus empty text nodes ---
  551.  
  552. // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256
  553.  
  554. // Walk down the tree starting at the root and remove any ZWS. If the node only
  555. // contained ZWS space then remove it too. We may want to keep one ZWS node at
  556. // the bottom of the tree so the block can be selected. Define that node as the
  557. // keepNode.
  558. var removeZWS = function ( root, keepNode ) {
  559.     var walker = new TreeWalker( root, SHOW_TEXT, function () {
  560.             return true;
  561.         }, false ),
  562.         parent, node, index;
  563.     while ( node = walker.nextNode() ) {
  564.         while ( ( index = node.data.indexOf( ZWS ) ) > -1  &&
  565.                 ( !keepNode || node.parentNode !== keepNode ) ) {
  566.             if ( node.length === 1 ) {
  567.                 do {
  568.                     parent = node.parentNode;
  569.                     parent.removeChild( node );
  570.                     node = parent;
  571.                     walker.currentNode = parent;
  572.                 } while ( isInline( node ) && !getLength( node ) );
  573.                 break;
  574.             } else {
  575.                 node.deleteData( index, 1 );
  576.             }
  577.         }
  578.     }
  579. };
  580.  
  581. proto._didAddZWS = function () {
  582.     this._hasZWS = true;
  583. };
  584. proto._removeZWS = function () {
  585.     if ( !this._hasZWS ) {
  586.         return;
  587.     }
  588.     removeZWS( this._root );
  589.     this._hasZWS = false;
  590. };
  591.  
  592. // --- Path change events ---
  593.  
  594. proto._updatePath = function ( range, force ) {
  595.     var anchor = range.startContainer,
  596.         focus = range.endContainer,
  597.         newPath;
  598.     if ( force || anchor !== this._lastAnchorNode ||
  599.             focus !== this._lastFocusNode ) {
  600.         this._lastAnchorNode = anchor;
  601.         this._lastFocusNode = focus;
  602.         newPath = ( anchor && focus ) ? ( anchor === focus ) ?
  603.             getPath( focus, this._root ) : '(selection)' : '';
  604.         if ( this._path !== newPath ) {
  605.             this._path = newPath;
  606.             this.fireEvent( 'pathChange', { path: newPath } );
  607.         }
  608.     }
  609.     if ( !range.collapsed ) {
  610.         this.fireEvent( 'select' );
  611.     }
  612. };
  613.  
  614. // selectionchange is fired synchronously in IE when removing current selection
  615. // and when setting new selection; keyup/mouseup may have processing we want
  616. // to do first. Either way, send to next event loop.
  617. proto._updatePathOnEvent = function () {
  618.     var self = this;
  619.     if ( !self._willUpdatePath ) {
  620.         self._willUpdatePath = true;
  621.         setTimeout( function () {
  622.             self._willUpdatePath = false;
  623.             self._updatePath( self.getSelection() );
  624.         }, 0 );
  625.     }
  626. };
  627.  
  628. // --- Focus ---
  629.  
  630. proto.focus = function () {
  631.     this._root.focus();
  632.  
  633.     if ( isIE ) {
  634.         this.fireEvent( 'focus' );
  635.     }
  636.  
  637.     return this;
  638. };
  639.  
  640. proto.blur = function () {
  641.     this._root.blur();
  642.  
  643.     if ( isIE ) {
  644.         this.fireEvent( 'blur' );
  645.     }
  646.  
  647.     return this;
  648. };
  649.  
  650. // --- Bookmarking ---
  651.  
  652. var startSelectionId = 'squire-selection-start';
  653. var endSelectionId = 'squire-selection-end';
  654.  
  655. proto._saveRangeToBookmark = function ( range ) {
  656.     var startNode = this.createElement( 'INPUT', {
  657.             id: startSelectionId,
  658.             type: 'hidden'
  659.         }),
  660.         endNode = this.createElement( 'INPUT', {
  661.             id: endSelectionId,
  662.             type: 'hidden'
  663.         }),
  664.         temp;
  665.  
  666.     insertNodeInRange( range, startNode );
  667.     range.collapse( false );
  668.     insertNodeInRange( range, endNode );
  669.  
  670.     // In a collapsed range, the start is sometimes inserted after the end!
  671.     if ( startNode.compareDocumentPosition( endNode ) &
  672.             DOCUMENT_POSITION_PRECEDING ) {
  673.         startNode.id = endSelectionId;
  674.         endNode.id = startSelectionId;
  675.         temp = startNode;
  676.         startNode = endNode;
  677.         endNode = temp;
  678.     }
  679.  
  680.     range.setStartAfter( startNode );
  681.     range.setEndBefore( endNode );
  682. };
  683.  
  684. proto._getRangeAndRemoveBookmark = function ( range ) {
  685.     var root = this._root,
  686.         start = root.querySelector( '#' + startSelectionId ),
  687.         end = root.querySelector( '#' + endSelectionId );
  688.  
  689.     if ( start && end ) {
  690.         var startContainer = start.parentNode,
  691.             endContainer = end.parentNode,
  692.             startOffset = indexOf.call( startContainer.childNodes, start ),
  693.             endOffset = indexOf.call( endContainer.childNodes, end );
  694.  
  695.         if ( startContainer === endContainer ) {
  696.             endOffset -= 1;
  697.         }
  698.  
  699.         detach( start );
  700.         detach( end );
  701.  
  702.         if ( !range ) {
  703.             range = this._doc.createRange();
  704.         }
  705.         range.setStart( startContainer, startOffset );
  706.         range.setEnd( endContainer, endOffset );
  707.  
  708.         // Merge any text nodes we split
  709.         mergeInlines( startContainer, range );
  710.         if ( startContainer !== endContainer ) {
  711.             mergeInlines( endContainer, range );
  712.         }
  713.  
  714.         // If we didn't split a text node, we should move into any adjacent
  715.         // text node to current selection point
  716.         if ( range.collapsed ) {
  717.             startContainer = range.startContainer;
  718.             if ( startContainer.nodeType === TEXT_NODE ) {
  719.                 endContainer = startContainer.childNodes[ range.startOffset ];
  720.                 if ( !endContainer || endContainer.nodeType !== TEXT_NODE ) {
  721.                     endContainer =
  722.                         startContainer.childNodes[ range.startOffset - 1 ];
  723.                 }
  724.                 if ( endContainer && endContainer.nodeType === TEXT_NODE ) {
  725.                     range.setStart( endContainer, 0 );
  726.                     range.collapse( true );
  727.                 }
  728.             }
  729.         }
  730.     }
  731.     return range || null;
  732. };
  733.  
  734. // --- Undo ---
  735.  
  736. proto._keyUpDetectChange = function ( event ) {
  737.     var code = event.keyCode;
  738.     // Presume document was changed if:
  739.     // 1. A modifier key (other than shift) wasn't held down
  740.     // 2. The key pressed is not in range 16<=x<=20 (control keys)
  741.     // 3. The key pressed is not in range 33<=x<=45 (navigation keys)
  742.     if ( !event.ctrlKey && !event.metaKey && !event.altKey &&
  743.             ( code < 16 || code > 20 ) &&
  744.             ( code < 33 || code > 45 ) ) {
  745.         this._docWasChanged();
  746.     }
  747. };
  748.  
  749. proto._docWasChanged = function () {
  750.     if ( canWeakMap ) {
  751.         nodeCategoryCache = new WeakMap();
  752.     }
  753.     if ( this._ignoreAllChanges ) {
  754.         return;
  755.     }
  756.  
  757.     if ( canObserveMutations && this._ignoreChange ) {
  758.         this._ignoreChange = false;
  759.         return;
  760.     }
  761.     if ( this._isInUndoState ) {
  762.         this._isInUndoState = false;
  763.         this.fireEvent( 'undoStateChange', {
  764.             canUndo: true,
  765.             canRedo: false
  766.         });
  767.     }
  768.     this.fireEvent( 'input' );
  769. };
  770.  
  771. // Leaves bookmark
  772. proto._recordUndoState = function ( range ) {
  773.     // Don't record if we're already in an undo state
  774.     if ( !this._isInUndoState ) {
  775.         // Advance pointer to new position
  776.         var undoIndex = this._undoIndex += 1;
  777.         var undoStack = this._undoStack;
  778.         var undoConfig = this._config.undo;
  779.         var undoThreshold = undoConfig.documentSizeThreshold;
  780.         var undoLimit = undoConfig.undoLimit;
  781.         var html;
  782.  
  783.         // Truncate stack if longer (i.e. if has been previously undone)
  784.         if ( undoIndex < this._undoStackLength ) {
  785.             undoStack.length = this._undoStackLength = undoIndex;
  786.         }
  787.  
  788.         // Get data
  789.         if ( range ) {
  790.             this._saveRangeToBookmark( range );
  791.         }
  792.         html = this._getHTML();
  793.  
  794.         // If this document is above the configured size threshold,
  795.         // limit the number of saved undo states.
  796.         // Threshold is in bytes, JS uses 2 bytes per character
  797.         if ( undoThreshold > -1 && html.length * 2 > undoThreshold ) {
  798.             if ( undoLimit > -1 && undoIndex > undoLimit ) {
  799.                 undoStack.splice( 0, undoIndex - undoLimit );
  800.                 undoIndex = this._undoIndex = undoLimit;
  801.                 this._undoStackLength = undoLimit;
  802.             }
  803.         }
  804.  
  805.         // Save data
  806.         undoStack[ undoIndex ] = html;
  807.         this._undoStackLength += 1;
  808.         this._isInUndoState = true;
  809.     }
  810. };
  811.  
  812. proto.saveUndoState = function ( range ) {
  813.     if ( range === undefined ) {
  814.         range = this.getSelection();
  815.     }
  816.     if ( !this._isInUndoState ) {
  817.         this._recordUndoState( range );
  818.         this._getRangeAndRemoveBookmark( range );
  819.     }
  820.     return this;
  821. };
  822.  
  823. proto.undo = function () {
  824.     // Sanity check: must not be at beginning of the history stack
  825.     if ( this._undoIndex !== 0 || !this._isInUndoState ) {
  826.         // Make sure any changes since last checkpoint are saved.
  827.         this._recordUndoState( this.getSelection() );
  828.  
  829.         this._undoIndex -= 1;
  830.         this._setHTML( this._undoStack[ this._undoIndex ] );
  831.         var range = this._getRangeAndRemoveBookmark();
  832.         if ( range ) {
  833.             this.setSelection( range );
  834.         }
  835.         this._isInUndoState = true;
  836.         this.fireEvent( 'undoStateChange', {
  837.             canUndo: this._undoIndex !== 0,
  838.             canRedo: true
  839.         });
  840.         this.fireEvent( 'input' );
  841.     }
  842.     return this;
  843. };
  844.  
  845. proto.redo = function () {
  846.     // Sanity check: must not be at end of stack and must be in an undo
  847.     // state.
  848.     var undoIndex = this._undoIndex,
  849.         undoStackLength = this._undoStackLength;
  850.     if ( undoIndex + 1 < undoStackLength && this._isInUndoState ) {
  851.         this._undoIndex += 1;
  852.         this._setHTML( this._undoStack[ this._undoIndex ] );
  853.         var range = this._getRangeAndRemoveBookmark();
  854.         if ( range ) {
  855.             this.setSelection( range );
  856.         }
  857.         this.fireEvent( 'undoStateChange', {
  858.             canUndo: true,
  859.             canRedo: undoIndex + 2 < undoStackLength
  860.         });
  861.         this.fireEvent( 'input' );
  862.     }
  863.     return this;
  864. };
  865.  
  866. // --- Inline formatting ---
  867.  
  868. // Looks for matching tag and attributes, so won't work
  869. // if <strong> instead of <b> etc.
  870. proto.hasFormat = function ( tag, attributes, range ) {
  871.     // 1. Normalise the arguments and get selection
  872.     tag = tag.toUpperCase();
  873.     if ( !attributes ) { attributes = {}; }
  874.     if ( !range && !( range = this.getSelection() ) ) {
  875.         return false;
  876.     }
  877.  
  878.     // Sanitize range to prevent weird IE artifacts
  879.     if ( !range.collapsed &&
  880.             range.startContainer.nodeType === TEXT_NODE &&
  881.             range.startOffset === range.startContainer.length &&
  882.             range.startContainer.nextSibling ) {
  883.         range.setStartBefore( range.startContainer.nextSibling );
  884.     }
  885.     if ( !range.collapsed &&
  886.             range.endContainer.nodeType === TEXT_NODE &&
  887.             range.endOffset === 0 &&
  888.             range.endContainer.previousSibling ) {
  889.         range.setEndAfter( range.endContainer.previousSibling );
  890.     }
  891.  
  892.     // If the common ancestor is inside the tag we require, we definitely
  893.     // have the format.
  894.     var root = this._root;
  895.     var common = range.commonAncestorContainer;
  896.     var walker, node;
  897.     if ( getNearest( common, root, tag, attributes ) ) {
  898.         return true;
  899.     }
  900.  
  901.     // If common ancestor is a text node and doesn't have the format, we
  902.     // definitely don't have it.
  903.     if ( common.nodeType === TEXT_NODE ) {
  904.         return false;
  905.     }
  906.  
  907.     // Otherwise, check each text node at least partially contained within
  908.     // the selection and make sure all of them have the format we want.
  909.     walker = new TreeWalker( common, SHOW_TEXT, function ( node ) {
  910.         return isNodeContainedInRange( range, node, true );
  911.     }, false );
  912.  
  913.     var seenNode = false;
  914.     while ( node = walker.nextNode() ) {
  915.         if ( !getNearest( node, root, tag, attributes ) ) {
  916.             return false;
  917.         }
  918.         seenNode = true;
  919.     }
  920.  
  921.     return seenNode;
  922. };
  923.  
  924. // Extracts the font-family and font-size (if any) of the element
  925. // holding the cursor. If there's a selection, returns an empty object.
  926. proto.getFontInfo = function ( range ) {
  927.     var fontInfo = {
  928.         color: undefined,
  929.         backgroundColor: undefined,
  930.         family: undefined,
  931.         size: undefined
  932.     };
  933.     var seenAttributes = 0;
  934.     var element, style, attr;
  935.  
  936.     if ( !range && !( range = this.getSelection() ) ) {
  937.         return fontInfo;
  938.     }
  939.  
  940.     element = range.commonAncestorContainer;
  941.     if ( range.collapsed || element.nodeType === TEXT_NODE ) {
  942.         if ( element.nodeType === TEXT_NODE ) {
  943.             element = element.parentNode;
  944.         }
  945.         while ( seenAttributes < 4 && element ) {
  946.             if ( style = element.style ) {
  947.                 if ( !fontInfo.color && ( attr = style.color ) ) {
  948.                     fontInfo.color = attr;
  949.                     seenAttributes += 1;
  950.                 }
  951.                 if ( !fontInfo.backgroundColor &&
  952.                         ( attr = style.backgroundColor ) ) {
  953.                     fontInfo.backgroundColor = attr;
  954.                     seenAttributes += 1;
  955.                 }
  956.                 if ( !fontInfo.family && ( attr = style.fontFamily ) ) {
  957.                     fontInfo.family = attr;
  958.                     seenAttributes += 1;
  959.                 }
  960.                 if ( !fontInfo.size && ( attr = style.fontSize ) ) {
  961.                     fontInfo.size = attr;
  962.                     seenAttributes += 1;
  963.                 }
  964.             }
  965.             element = element.parentNode;
  966.         }
  967.     }
  968.     return fontInfo;
  969. };
  970.  
  971. proto._addFormat = function ( tag, attributes, range ) {
  972.     // If the range is collapsed we simply insert the node by wrapping
  973.     // it round the range and focus it.
  974.     var root = this._root;
  975.     var el, walker, startContainer, endContainer, startOffset, endOffset,
  976.         node, needsFormat, block;
  977.  
  978.     if ( range.collapsed ) {
  979.         el = fixCursor( this.createElement( tag, attributes ), root );
  980.         insertNodeInRange( range, el );
  981.         range.setStart( el.firstChild, el.firstChild.length );
  982.         range.collapse( true );
  983.  
  984.         // Clean up any previous formats that may have been set on this block
  985.         // that are unused.
  986.         block = el;
  987.         while ( isInline( block ) ) {
  988.             block = block.parentNode;
  989.         }
  990.         removeZWS( block, el );
  991.     }
  992.     // Otherwise we find all the textnodes in the range (splitting
  993.     // partially selected nodes) and if they're not already formatted
  994.     // correctly we wrap them in the appropriate tag.
  995.     else {
  996.         // Create an iterator to walk over all the text nodes under this
  997.         // ancestor which are in the range and not already formatted
  998.         // correctly.
  999.         //
  1000.         // In Blink/WebKit, empty blocks may have no text nodes, just a <br>.
  1001.         // Therefore we wrap this in the tag as well, as this will then cause it
  1002.         // to apply when the user types something in the block, which is
  1003.         // presumably what was intended.
  1004.         //
  1005.         // IMG tags are included because we may want to create a link around
  1006.         // them, and adding other styles is harmless.
  1007.         walker = new TreeWalker(
  1008.             range.commonAncestorContainer,
  1009.             SHOW_TEXT|SHOW_ELEMENT,
  1010.             function ( node ) {
  1011.                 return ( node.nodeType === TEXT_NODE ||
  1012.                         node.nodeName === 'BR' ||
  1013.                         node.nodeName === 'IMG'
  1014.                     ) && isNodeContainedInRange( range, node, true );
  1015.             },
  1016.             false
  1017.         );
  1018.  
  1019.         // Start at the beginning node of the range and iterate through
  1020.         // all the nodes in the range that need formatting.
  1021.         startContainer = range.startContainer;
  1022.         startOffset = range.startOffset;
  1023.         endContainer = range.endContainer;
  1024.         endOffset = range.endOffset;
  1025.  
  1026.         // Make sure we start with a valid node.
  1027.         walker.currentNode = startContainer;
  1028.         if ( !walker.filter( startContainer ) ) {
  1029.             startContainer = walker.nextNode();
  1030.             startOffset = 0;
  1031.         }
  1032.  
  1033.         // If there are no interesting nodes in the selection, abort
  1034.         if ( !startContainer ) {
  1035.             return range;
  1036.         }
  1037.  
  1038.         do {
  1039.             node = walker.currentNode;
  1040.             needsFormat = !getNearest( node, root, tag, attributes );
  1041.             if ( needsFormat ) {
  1042.                 // <br> can never be a container node, so must have a text node
  1043.                 // if node == (end|start)Container
  1044.                 if ( node === endContainer && node.length > endOffset ) {
  1045.                     node.splitText( endOffset );
  1046.                 }
  1047.                 if ( node === startContainer && startOffset ) {
  1048.                     node = node.splitText( startOffset );
  1049.                     if ( endContainer === startContainer ) {
  1050.                         endContainer = node;
  1051.                         endOffset -= startOffset;
  1052.                     }
  1053.                     startContainer = node;
  1054.                     startOffset = 0;
  1055.                 }
  1056.                 el = this.createElement( tag, attributes );
  1057.                 replaceWith( node, el );
  1058.                 el.appendChild( node );
  1059.             }
  1060.         } while ( walker.nextNode() );
  1061.  
  1062.         // If we don't finish inside a text node, offset may have changed.
  1063.         if ( endContainer.nodeType !== TEXT_NODE ) {
  1064.             if ( node.nodeType === TEXT_NODE ) {
  1065.                 endContainer = node;
  1066.                 endOffset = node.length;
  1067.             } else {
  1068.                 // If <br>, we must have just wrapped it, so it must have only
  1069.                 // one child
  1070.                 endContainer = node.parentNode;
  1071.                 endOffset = 1;
  1072.             }
  1073.         }
  1074.  
  1075.         // Now set the selection to as it was before
  1076.         range = this._createRange(
  1077.             startContainer, startOffset, endContainer, endOffset );
  1078.     }
  1079.     return range;
  1080. };
  1081.  
  1082. proto._removeFormat = function ( tag, attributes, range, partial ) {
  1083.     // Add bookmark
  1084.     this._saveRangeToBookmark( range );
  1085.  
  1086.     // We need a node in the selection to break the surrounding
  1087.     // formatted text.
  1088.     var doc = this._doc,
  1089.         fixer;
  1090.     if ( range.collapsed ) {
  1091.         if ( cantFocusEmptyTextNodes ) {
  1092.             fixer = doc.createTextNode( ZWS );
  1093.             this._didAddZWS();
  1094.         } else {
  1095.             fixer = doc.createTextNode( '' );
  1096.         }
  1097.         insertNodeInRange( range, fixer );
  1098.     }
  1099.  
  1100.     // Find block-level ancestor of selection
  1101.     var root = range.commonAncestorContainer;
  1102.     while ( isInline( root ) ) {
  1103.         root = root.parentNode;
  1104.     }
  1105.  
  1106.     // Find text nodes inside formatTags that are not in selection and
  1107.     // add an extra tag with the same formatting.
  1108.     var startContainer = range.startContainer,
  1109.         startOffset = range.startOffset,
  1110.         endContainer = range.endContainer,
  1111.         endOffset = range.endOffset,
  1112.         toWrap = [],
  1113.         examineNode = function ( node, exemplar ) {
  1114.             // If the node is completely contained by the range then
  1115.             // we're going to remove all formatting so ignore it.
  1116.             if ( isNodeContainedInRange( range, node, false ) ) {
  1117.                 return;
  1118.             }
  1119.  
  1120.             var isText = ( node.nodeType === TEXT_NODE ),
  1121.                 child, next;
  1122.  
  1123.             // If not at least partially contained, wrap entire contents
  1124.             // in a clone of the tag we're removing and we're done.
  1125.             if ( !isNodeContainedInRange( range, node, true ) ) {
  1126.                 // Ignore bookmarks and empty text nodes
  1127.                 if ( node.nodeName !== 'INPUT' &&
  1128.                         ( !isText || node.data ) ) {
  1129.                     toWrap.push([ exemplar, node ]);
  1130.                 }
  1131.                 return;
  1132.             }
  1133.  
  1134.             // Split any partially selected text nodes.
  1135.             if ( isText ) {
  1136.                 if ( node === endContainer && endOffset !== node.length ) {
  1137.                     toWrap.push([ exemplar, node.splitText( endOffset ) ]);
  1138.                 }
  1139.                 if ( node === startContainer && startOffset ) {
  1140.                     node.splitText( startOffset );
  1141.                     toWrap.push([ exemplar, node ]);
  1142.                 }
  1143.             }
  1144.             // If not a text node, recurse onto all children.
  1145.             // Beware, the tree may be rewritten with each call
  1146.             // to examineNode, hence find the next sibling first.
  1147.             else {
  1148.                 for ( child = node.firstChild; child; child = next ) {
  1149.                     next = child.nextSibling;
  1150.                     examineNode( child, exemplar );
  1151.                 }
  1152.             }
  1153.         },
  1154.         formatTags = Array.prototype.filter.call(
  1155.             root.getElementsByTagName( tag ), function ( el ) {
  1156.                 return isNodeContainedInRange( range, el, true ) &&
  1157.                     hasTagAttributes( el, tag, attributes );
  1158.             }
  1159.         );
  1160.  
  1161.     if ( !partial ) {
  1162.         formatTags.forEach( function ( node ) {
  1163.             examineNode( node, node );
  1164.         });
  1165.     }
  1166.  
  1167.     // Now wrap unselected nodes in the tag
  1168.     toWrap.forEach( function ( item ) {
  1169.         // [ exemplar, node ] tuple
  1170.         var el = item[0].cloneNode( false ),
  1171.             node = item[1];
  1172.         replaceWith( node, el );
  1173.         el.appendChild( node );
  1174.     });
  1175.     // and remove old formatting tags.
  1176.     formatTags.forEach( function ( el ) {
  1177.         replaceWith( el, empty( el ) );
  1178.     });
  1179.  
  1180.     // Merge adjacent inlines:
  1181.     this._getRangeAndRemoveBookmark( range );
  1182.     if ( fixer ) {
  1183.         range.collapse( false );
  1184.     }
  1185.     mergeInlines( root, range );
  1186.  
  1187.     return range;
  1188. };
  1189.  
  1190. proto.changeFormat = function ( add, remove, range, partial ) {
  1191.     // Normalise the arguments and get selection
  1192.     if ( !range && !( range = this.getSelection() ) ) {
  1193.         return this;
  1194.     }
  1195.  
  1196.     // Save undo checkpoint
  1197.     this.saveUndoState( range );
  1198.  
  1199.     if ( remove ) {
  1200.         range = this._removeFormat( remove.tag.toUpperCase(),
  1201.             remove.attributes || {}, range, partial );
  1202.     }
  1203.     if ( add ) {
  1204.         range = this._addFormat( add.tag.toUpperCase(),
  1205.             add.attributes || {}, range );
  1206.     }
  1207.  
  1208.     this.setSelection( range );
  1209.     this._updatePath( range, true );
  1210.  
  1211.     // We're not still in an undo state
  1212.     if ( !canObserveMutations ) {
  1213.         this._docWasChanged();
  1214.     }
  1215.  
  1216.     return this;
  1217. };
  1218.  
  1219. // --- Block formatting ---
  1220.  
  1221. var tagAfterSplit = {
  1222.     DT:  'DD',
  1223.     DD:  'DT',
  1224.     LI:  'LI'
  1225. };
  1226.  
  1227. var splitBlock = function ( self, block, node, offset ) {
  1228.     var splitTag = tagAfterSplit[ block.nodeName ],
  1229.         splitProperties = null,
  1230.         nodeAfterSplit = split( node, offset, block.parentNode, self._root ),
  1231.         config = self._config;
  1232.  
  1233.     if ( !splitTag ) {
  1234.         splitTag = config.blockTag;
  1235.         splitProperties = config.blockAttributes;
  1236.     }
  1237.  
  1238.     // Make sure the new node is the correct type.
  1239.     if ( !hasTagAttributes( nodeAfterSplit, splitTag, splitProperties ) ) {
  1240.         block = createElement( nodeAfterSplit.ownerDocument,
  1241.             splitTag, splitProperties );
  1242.         if ( nodeAfterSplit.dir ) {
  1243.             block.dir = nodeAfterSplit.dir;
  1244.         }
  1245.         replaceWith( nodeAfterSplit, block );
  1246.         block.appendChild( empty( nodeAfterSplit ) );
  1247.         nodeAfterSplit = block;
  1248.     }
  1249.     return nodeAfterSplit;
  1250. };
  1251.  
  1252. proto.forEachBlock = function ( fn, mutates, range ) {
  1253.     if ( !range && !( range = this.getSelection() ) ) {
  1254.         return this;
  1255.     }
  1256.  
  1257.     // Save undo checkpoint
  1258.     if ( mutates ) {
  1259.         this.saveUndoState( range );
  1260.     }
  1261.  
  1262.     var root = this._root;
  1263.     var start = getStartBlockOfRange( range, root );
  1264.     var end = getEndBlockOfRange( range, root );
  1265.     if ( start && end ) {
  1266.         do {
  1267.             if ( fn( start ) || start === end ) { break; }
  1268.         } while ( start = getNextBlock( start, root ) );
  1269.     }
  1270.  
  1271.     if ( mutates ) {
  1272.         this.setSelection( range );
  1273.  
  1274.         // Path may have changed
  1275.         this._updatePath( range, true );
  1276.  
  1277.         // We're not still in an undo state
  1278.         if ( !canObserveMutations ) {
  1279.             this._docWasChanged();
  1280.         }
  1281.     }
  1282.     return this;
  1283. };
  1284.  
  1285. proto.modifyBlocks = function ( modify, range ) {
  1286.     if ( !range && !( range = this.getSelection() ) ) {
  1287.         return this;
  1288.     }
  1289.  
  1290.     // 1. Save undo checkpoint and bookmark selection
  1291.     if ( this._isInUndoState ) {
  1292.         this._saveRangeToBookmark( range );
  1293.     } else {
  1294.         this._recordUndoState( range );
  1295.     }
  1296.  
  1297.     var root = this._root;
  1298.     var frag;
  1299.  
  1300.     // 2. Expand range to block boundaries
  1301.     expandRangeToBlockBoundaries( range, root );
  1302.  
  1303.     // 3. Remove range.
  1304.     moveRangeBoundariesUpTree( range, root );
  1305.     frag = extractContentsOfRange( range, root, root );
  1306.  
  1307.     // 4. Modify tree of fragment and reinsert.
  1308.     insertNodeInRange( range, modify.call( this, frag ) );
  1309.  
  1310.     // 5. Merge containers at edges
  1311.     if ( range.endOffset < range.endContainer.childNodes.length ) {
  1312.         mergeContainers( range.endContainer.childNodes[ range.endOffset ], root );
  1313.     }
  1314.     mergeContainers( range.startContainer.childNodes[ range.startOffset ], root );
  1315.  
  1316.     // 6. Restore selection
  1317.     this._getRangeAndRemoveBookmark( range );
  1318.     this.setSelection( range );
  1319.     this._updatePath( range, true );
  1320.  
  1321.     // 7. We're not still in an undo state
  1322.     if ( !canObserveMutations ) {
  1323.         this._docWasChanged();
  1324.     }
  1325.  
  1326.     return this;
  1327. };
  1328.  
  1329. var increaseBlockQuoteLevel = function ( frag ) {
  1330.     return this.createElement( 'BLOCKQUOTE',
  1331.         this._config.tagAttributes.blockquote, [
  1332.             frag
  1333.         ]);
  1334. };
  1335.  
  1336. var decreaseBlockQuoteLevel = function ( frag ) {
  1337.     var root = this._root;
  1338.     var blockquotes = frag.querySelectorAll( 'blockquote' );
  1339.     Array.prototype.filter.call( blockquotes, function ( el ) {
  1340.         return !getNearest( el.parentNode, root, 'BLOCKQUOTE' );
  1341.     }).forEach( function ( el ) {
  1342.         replaceWith( el, empty( el ) );
  1343.     });
  1344.     return frag;
  1345. };
  1346.  
  1347. var removeBlockQuote = function (/* frag */) {
  1348.     return this.createDefaultBlock([
  1349.         this.createElement( 'INPUT', {
  1350.             id: startSelectionId,
  1351.             type: 'hidden'
  1352.         }),
  1353.         this.createElement( 'INPUT', {
  1354.             id: endSelectionId,
  1355.             type: 'hidden'
  1356.         })
  1357.     ]);
  1358. };
  1359.  
  1360. var makeList = function ( self, frag, type ) {
  1361.     var walker = getBlockWalker( frag, self._root ),
  1362.         node, tag, prev, newLi,
  1363.         tagAttributes = self._config.tagAttributes,
  1364.         listAttrs = tagAttributes[ type.toLowerCase() ],
  1365.         listItemAttrs = tagAttributes.li;
  1366.  
  1367.     while ( node = walker.nextNode() ) {
  1368.         if ( node.parentNode.nodeName === 'LI' ) {
  1369.             node = node.parentNode;
  1370.             walker.currentNode = node.lastChild;
  1371.         }
  1372.         if ( node.nodeName !== 'LI' ) {
  1373.             newLi = self.createElement( 'LI', listItemAttrs );
  1374.             if ( node.dir ) {
  1375.                 newLi.dir = node.dir;
  1376.             }
  1377.  
  1378.             // Have we replaced the previous block with a new <ul>/<ol>?
  1379.             if ( ( prev = node.previousSibling ) && prev.nodeName === type ) {
  1380.                 prev.appendChild( newLi );
  1381.                 detach( node );
  1382.             }
  1383.             // Otherwise, replace this block with the <ul>/<ol>
  1384.             else {
  1385.                 replaceWith(
  1386.                     node,
  1387.                     self.createElement( type, listAttrs, [
  1388.                         newLi
  1389.                     ])
  1390.                 );
  1391.             }
  1392.             newLi.appendChild( empty( node ) );
  1393.             walker.currentNode = newLi;
  1394.         } else {
  1395.             node = node.parentNode;
  1396.             tag = node.nodeName;
  1397.             if ( tag !== type && ( /^[OU]L$/.test( tag ) ) ) {
  1398.                 replaceWith( node,
  1399.                     self.createElement( type, listAttrs, [ empty( node ) ] )
  1400.                 );
  1401.             }
  1402.         }
  1403.     }
  1404. };
  1405.  
  1406. var makeUnorderedList = function ( frag ) {
  1407.     makeList( this, frag, 'UL' );
  1408.     return frag;
  1409. };
  1410.  
  1411. var makeOrderedList = function ( frag ) {
  1412.     makeList( this, frag, 'OL' );
  1413.     return frag;
  1414. };
  1415.  
  1416. var removeList = function ( frag ) {
  1417.     var lists = frag.querySelectorAll( 'UL, OL' ),
  1418.         items =  frag.querySelectorAll( 'LI' ),
  1419.         root = this._root,
  1420.         i, l, list, listFrag, item;
  1421.     for ( i = 0, l = lists.length; i < l; i += 1 ) {
  1422.         list = lists[i];
  1423.         listFrag = empty( list );
  1424.         fixContainer( listFrag, root );
  1425.         replaceWith( list, listFrag );
  1426.     }
  1427.  
  1428.     for ( i = 0, l = items.length; i < l; i += 1 ) {
  1429.         item = items[i];
  1430.         if ( isBlock( item ) ) {
  1431.             replaceWith( item,
  1432.                 this.createDefaultBlock([ empty( item ) ])
  1433.             );
  1434.         } else {
  1435.             fixContainer( item, root );
  1436.             replaceWith( item, empty( item ) );
  1437.         }
  1438.     }
  1439.     return frag;
  1440. };
  1441.  
  1442. var increaseListLevel = function ( frag ) {
  1443.     var items = frag.querySelectorAll( 'LI' ),
  1444.         i, l, item,
  1445.         type, newParent,
  1446.         tagAttributes = this._config.tagAttributes,
  1447.         listAttrs;
  1448.     for ( i = 0, l = items.length; i < l; i += 1 ) {
  1449.         item = items[i];
  1450.         if ( !isContainer( item.firstChild ) ) {
  1451.             // type => 'UL' or 'OL'
  1452.             type = item.parentNode.nodeName;
  1453.             newParent = item.previousSibling;
  1454.             if ( !newParent || !( newParent = newParent.lastChild ) ||
  1455.                     newParent.nodeName !== type ) {
  1456.                 listAttrs = tagAttributes[ type.toLowerCase() ];
  1457.                 newParent = this.createElement( type, listAttrs );
  1458.  
  1459.                 replaceWith(
  1460.                     item,
  1461.                     newParent
  1462.                 );
  1463.             }
  1464.             newParent.appendChild( item );
  1465.         }
  1466.     }
  1467.     return frag;
  1468. };
  1469.  
  1470. var decreaseListLevel = function ( frag ) {
  1471.     var root = this._root;
  1472.     var items = frag.querySelectorAll( 'LI' );
  1473.     Array.prototype.filter.call( items, function ( el ) {
  1474.         return !isContainer( el.firstChild );
  1475.     }).forEach( function ( item ) {
  1476.         var parent = item.parentNode,
  1477.             newParent = parent.parentNode,
  1478.             first = item.firstChild,
  1479.             node = first,
  1480.             next;
  1481.         if ( item.previousSibling ) {
  1482.             parent = split( parent, item, newParent, root );
  1483.         }
  1484.  
  1485.         // if the new parent is another list then we simply move the node
  1486.         // e.g. `ul > ul > li` becomes `ul > li`
  1487.         if ( /^[OU]L$/.test( newParent.nodeName ) ) {
  1488.             newParent.insertBefore( item, parent );
  1489.             if ( !parent.firstChild ) {
  1490.                 newParent.removeChild( parent );
  1491.             }
  1492.         } else {
  1493.             while ( node ) {
  1494.                 next = node.nextSibling;
  1495.                 if ( isContainer( node ) ) {
  1496.                     break;
  1497.                 }
  1498.                 newParent.insertBefore( node, parent );
  1499.                 node = next;
  1500.             }
  1501.         }
  1502.         if ( newParent.nodeName === 'LI' && first.previousSibling ) {
  1503.             split( newParent, first, newParent.parentNode, root );
  1504.         }
  1505.         while ( item !== frag && !item.childNodes.length ) {
  1506.             parent = item.parentNode;
  1507.             parent.removeChild( item );
  1508.             item = parent;
  1509.         }
  1510.     }, this );
  1511.     fixContainer( frag, root );
  1512.     return frag;
  1513. };
  1514.  
  1515. proto._ensureBottomLine = function () {
  1516.     var root = this._root;
  1517.     var last = root.lastElementChild;
  1518.     if ( !last ||
  1519.             last.nodeName !== this._config.blockTag || !isBlock( last ) ) {
  1520.         root.appendChild( this.createDefaultBlock() );
  1521.     }
  1522. };
  1523.  
  1524. // --- Keyboard interaction ---
  1525.  
  1526. proto.setKeyHandler = function ( key, fn ) {
  1527.     this._keyHandlers[ key ] = fn;
  1528.     return this;
  1529. };
  1530.  
  1531. // --- Get/Set data ---
  1532.  
  1533. proto._getHTML = function () {
  1534.     return this._root.innerHTML;
  1535. };
  1536.  
  1537. proto._setHTML = function ( html ) {
  1538.     var root = this._root;
  1539.     var node = root;
  1540.     node.innerHTML = html;
  1541.     do {
  1542.         fixCursor( node, root );
  1543.     } while ( node = getNextBlock( node, root ) );
  1544.     this._ignoreChange = true;
  1545. };
  1546.  
  1547. proto.getHTML = function ( withBookMark ) {
  1548.     var brs = [],
  1549.         root, node, fixer, html, l, range;
  1550.     if ( withBookMark && ( range = this.getSelection() ) ) {
  1551.         this._saveRangeToBookmark( range );
  1552.     }
  1553.     if ( useTextFixer ) {
  1554.         root = this._root;
  1555.         node = root;
  1556.         while ( node = getNextBlock( node, root ) ) {
  1557.             if ( !node.textContent && !node.querySelector( 'BR' ) ) {
  1558.                 fixer = this.createElement( 'BR' );
  1559.                 node.appendChild( fixer );
  1560.                 brs.push( fixer );
  1561.             }
  1562.         }
  1563.     }
  1564.     html = this._getHTML().replace( /\u200B/g, '' );
  1565.     if ( useTextFixer ) {
  1566.         l = brs.length;
  1567.         while ( l-- ) {
  1568.             detach( brs[l] );
  1569.         }
  1570.     }
  1571.     if ( range ) {
  1572.         this._getRangeAndRemoveBookmark( range );
  1573.     }
  1574.     return html;
  1575. };
  1576.  
  1577. proto.setHTML = function ( html ) {
  1578.     var config = this._config;
  1579.     var sanitizeToDOMFragment = config.isSetHTMLSanitized ?
  1580.             config.sanitizeToDOMFragment : null;
  1581.     var root = this._root;
  1582.     var div, frag, child;
  1583.  
  1584.     // Parse HTML into DOM tree
  1585.     if ( typeof sanitizeToDOMFragment === 'function' ) {
  1586.         frag = sanitizeToDOMFragment( html, false, this );
  1587.     } else {
  1588.         div = this.createElement( 'DIV' );
  1589.         div.innerHTML = html;
  1590.         frag = this._doc.createDocumentFragment();
  1591.         frag.appendChild( empty( div ) );
  1592.     }
  1593.  
  1594.     cleanTree( frag );
  1595.     cleanupBRs( frag, root );
  1596.  
  1597.     fixContainer( frag, root );
  1598.  
  1599.     // Fix cursor
  1600.     var node = frag;
  1601.     while ( node = getNextBlock( node, root ) ) {
  1602.         fixCursor( node, root );
  1603.     }
  1604.  
  1605.     // Don't fire an input event
  1606.     this._ignoreChange = true;
  1607.  
  1608.     // Remove existing root children
  1609.     while ( child = root.lastChild ) {
  1610.         root.removeChild( child );
  1611.     }
  1612.  
  1613.     // And insert new content
  1614.     root.appendChild( frag );
  1615.     fixCursor( root, root );
  1616.  
  1617.     // Reset the undo stack
  1618.     this._undoIndex = -1;
  1619.     this._undoStack.length = 0;
  1620.     this._undoStackLength = 0;
  1621.     this._isInUndoState = false;
  1622.  
  1623.     // Record undo state
  1624.     var range = this._getRangeAndRemoveBookmark() ||
  1625.         this._createRange( root.firstChild, 0 );
  1626.     this.saveUndoState( range );
  1627.     // IE will also set focus when selecting text so don't use
  1628.     // setSelection. Instead, just store it in lastSelection, so if
  1629.     // anything calls getSelection before first focus, we have a range
  1630.     // to return.
  1631.     this._lastSelection = range;
  1632.     enableRestoreSelection.call( this );
  1633.     this._updatePath( range, true );
  1634.  
  1635.     return this;
  1636. };
  1637.  
  1638. proto.insertElement = function ( el, range ) {
  1639.     if ( !range ) { range = this.getSelection(); }
  1640.     range.collapse( true );
  1641.     if ( isInline( el ) ) {
  1642.         insertNodeInRange( range, el );
  1643.         range.setStartAfter( el );
  1644.     } else {
  1645.         // Get containing block node.
  1646.         var root = this._root;
  1647.         var splitNode = getStartBlockOfRange( range, root ) || root;
  1648.         var parent, nodeAfterSplit;
  1649.         // While at end of container node, move up DOM tree.
  1650.         while ( splitNode !== root && !splitNode.nextSibling ) {
  1651.             splitNode = splitNode.parentNode;
  1652.         }
  1653.         // If in the middle of a container node, split up to root.
  1654.         if ( splitNode !== root ) {
  1655.             parent = splitNode.parentNode;
  1656.             nodeAfterSplit = split( parent, splitNode.nextSibling, root, root );
  1657.         }
  1658.         if ( nodeAfterSplit ) {
  1659.             root.insertBefore( el, nodeAfterSplit );
  1660.         } else {
  1661.             root.appendChild( el );
  1662.             // Insert blank line below block.
  1663.             nodeAfterSplit = this.createDefaultBlock();
  1664.             root.appendChild( nodeAfterSplit );
  1665.         }
  1666.         range.setStart( nodeAfterSplit, 0 );
  1667.         range.setEnd( nodeAfterSplit, 0 );
  1668.         moveRangeBoundariesDownTree( range );
  1669.     }
  1670.     this.focus();
  1671.     this.setSelection( range );
  1672.     this._updatePath( range );
  1673.  
  1674.     if ( !canObserveMutations ) {
  1675.         this._docWasChanged();
  1676.     }
  1677.  
  1678.     return this;
  1679. };
  1680.  
  1681. proto.insertImage = function ( src, attributes ) {
  1682.     var img = this.createElement( 'IMG', mergeObjects({
  1683.         src: src
  1684.     }, attributes, true ));
  1685.     this.insertElement( img );
  1686.     return img;
  1687. };
  1688.  
  1689. var linkRegExp = /\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?芦禄鈥溾€濃€樷€橾))|([\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,}\b)/i;
  1690.  
  1691. var addLinks = function ( frag, root, self ) {
  1692.     var doc = frag.ownerDocument,
  1693.         walker = new TreeWalker( frag, SHOW_TEXT,
  1694.                 function ( node ) {
  1695.             return !getNearest( node, root, 'A' );
  1696.         }, false ),
  1697.         defaultAttributes = self._config.tagAttributes.a,
  1698.         node, data, parent, match, index, endIndex, child;
  1699.     while ( node = walker.nextNode() ) {
  1700.         data = node.data;
  1701.         parent = node.parentNode;
  1702.         while ( match = linkRegExp.exec( data ) ) {
  1703.             index = match.index;
  1704.             endIndex = index + match[0].length;
  1705.             if ( index ) {
  1706.                 child = doc.createTextNode( data.slice( 0, index ) );
  1707.                 parent.insertBefore( child, node );
  1708.             }
  1709.             child = self.createElement( 'A', mergeObjects({
  1710.                 href: match[1] ?
  1711.                     /^(?:ht|f)tps?:/.test( match[1] ) ?
  1712.                         match[1] :
  1713.                         'http://' + match[1] :
  1714.                     'mailto:' + match[2]
  1715.             }, defaultAttributes, false ));
  1716.             child.textContent = data.slice( index, endIndex );
  1717.             parent.insertBefore( child, node );
  1718.             node.data = data = data.slice( endIndex );
  1719.         }
  1720.     }
  1721. };
  1722.  
  1723. // Insert HTML at the cursor location. If the selection is not collapsed
  1724. // insertTreeFragmentIntoRange will delete the selection so that it is replaced
  1725. // by the html being inserted.
  1726. proto.insertHTML = function ( html, isPaste ) {
  1727.     var config = this._config;
  1728.     var sanitizeToDOMFragment = config.isInsertedHTMLSanitized ?
  1729.             config.sanitizeToDOMFragment : null;
  1730.     var range = this.getSelection();
  1731.     var doc = this._doc;
  1732.     var startFragmentIndex, endFragmentIndex;
  1733.     var div, frag, root, node, event;
  1734.  
  1735.     // Edge doesn't just copy the fragment, but includes the surrounding guff
  1736.     // including the full <head> of the page. Need to strip this out. If
  1737.     // available use DOMPurify to parse and sanitise.
  1738.     if ( typeof sanitizeToDOMFragment === 'function' ) {
  1739.         frag = sanitizeToDOMFragment( html, isPaste, this );
  1740.     } else {
  1741.         if ( isPaste ) {
  1742.             startFragmentIndex = html.indexOf( '<!--StartFragment-->' );
  1743.             endFragmentIndex = html.lastIndexOf( '<!--EndFragment-->' );
  1744.             if ( startFragmentIndex > -1 && endFragmentIndex > -1 ) {
  1745.                 html = html.slice( startFragmentIndex + 20, endFragmentIndex );
  1746.             }
  1747.         }
  1748.         // Parse HTML into DOM tree
  1749.         div = this.createElement( 'DIV' );
  1750.         div.innerHTML = html;
  1751.         frag = doc.createDocumentFragment();
  1752.         frag.appendChild( empty( div ) );
  1753.     }
  1754.  
  1755.     // Record undo checkpoint
  1756.     this.saveUndoState( range );
  1757.  
  1758.     try {
  1759.         root = this._root;
  1760.         node = frag;
  1761.         event = {
  1762.             fragment: frag,
  1763.             preventDefault: function () {
  1764.                 this.defaultPrevented = true;
  1765.             },
  1766.             defaultPrevented: false
  1767.         };
  1768.  
  1769.         addLinks( frag, frag, this );
  1770.         cleanTree( frag );
  1771.         cleanupBRs( frag, root );
  1772.         removeEmptyInlines( frag );
  1773.         frag.normalize();
  1774.  
  1775.         while ( node = getNextBlock( node, frag ) ) {
  1776.             fixCursor( node, root );
  1777.         }
  1778.  
  1779.         if ( isPaste ) {
  1780.             this.fireEvent( 'willPaste', event );
  1781.         }
  1782.  
  1783.         if ( !event.defaultPrevented ) {
  1784.             insertTreeFragmentIntoRange( range, event.fragment, root );
  1785.             if ( !canObserveMutations ) {
  1786.                 this._docWasChanged();
  1787.             }
  1788.             range.collapse( false );
  1789.             this._ensureBottomLine();
  1790.         }
  1791.  
  1792.         this.setSelection( range );
  1793.         this._updatePath( range, true );
  1794.         // Safari sometimes loses focus after paste. Weird.
  1795.         if ( isPaste ) {
  1796.             this.focus();
  1797.         }
  1798.     } catch ( error ) {
  1799.         this.didError( error );
  1800.     }
  1801.     return this;
  1802. };
  1803.  
  1804. var escapeHTMLFragement = function ( text ) {
  1805.     return text.split( '&' ).join( '&amp;' )
  1806.                .split( '<' ).join( '&lt;'  )
  1807.                .split( '>' ).join( '&gt;'  )
  1808.                .split( '"' ).join( '&quot;'  );
  1809. };
  1810.  
  1811. proto.insertPlainText = function ( plainText, isPaste ) {
  1812.     var lines = plainText.split( '\n' );
  1813.     var config = this._config;
  1814.     var tag = config.blockTag;
  1815.     var attributes = config.blockAttributes;
  1816.     var closeBlock  = '</' + tag + '>';
  1817.     var openBlock = '<' + tag;
  1818.     var attr, i, l, line;
  1819.  
  1820.     for ( attr in attributes ) {
  1821.         openBlock += ' ' + attr + '="' +
  1822.             escapeHTMLFragement( attributes[ attr ] ) +
  1823.         '"';
  1824.     }
  1825.     openBlock += '>';
  1826.  
  1827.     for ( i = 0, l = lines.length; i < l; i += 1 ) {
  1828.         line = lines[i];
  1829.         line = escapeHTMLFragement( line ).replace( / (?= )/g, '&nbsp;' );
  1830.         // Wrap all but first/last lines in <div></div>
  1831.         if ( i && i + 1 < l ) {
  1832.             line = openBlock + ( line || '<BR>' ) + closeBlock;
  1833.         }
  1834.         lines[i] = line;
  1835.     }
  1836.     return this.insertHTML( lines.join( '' ), isPaste );
  1837. };
  1838.  
  1839. // --- Formatting ---
  1840.  
  1841. var command = function ( method, arg, arg2 ) {
  1842.     return function () {
  1843.         this[ method ]( arg, arg2 );
  1844.         return this.focus();
  1845.     };
  1846. };
  1847.  
  1848. proto.addStyles = function ( styles ) {
  1849.     if ( styles ) {
  1850.         var head = this._doc.documentElement.firstChild,
  1851.             style = this.createElement( 'STYLE', {
  1852.                 type: 'text/css'
  1853.             });
  1854.         style.appendChild( this._doc.createTextNode( styles ) );
  1855.         head.appendChild( style );
  1856.     }
  1857.     return this;
  1858. };
  1859.  
  1860. proto.bold = command( 'changeFormat', { tag: 'B' } );
  1861. proto.italic = command( 'changeFormat', { tag: 'I' } );
  1862. proto.underline = command( 'changeFormat', { tag: 'U' } );
  1863. proto.strikethrough = command( 'changeFormat', { tag: 'S' } );
  1864. proto.subscript = command( 'changeFormat', { tag: 'SUB' }, { tag: 'SUP' } );
  1865. proto.superscript = command( 'changeFormat', { tag: 'SUP' }, { tag: 'SUB' } );
  1866.  
  1867. proto.removeBold = command( 'changeFormat', null, { tag: 'B' } );
  1868. proto.removeItalic = command( 'changeFormat', null, { tag: 'I' } );
  1869. proto.removeUnderline = command( 'changeFormat', null, { tag: 'U' } );
  1870. proto.removeStrikethrough = command( 'changeFormat', null, { tag: 'S' } );
  1871. proto.removeSubscript = command( 'changeFormat', null, { tag: 'SUB' } );
  1872. proto.removeSuperscript = command( 'changeFormat', null, { tag: 'SUP' } );
  1873.  
  1874. proto.makeLink = function ( url, attributes ) {
  1875.     var range = this.getSelection();
  1876.     if ( range.collapsed ) {
  1877.         var protocolEnd = url.indexOf( ':' ) + 1;
  1878.         if ( protocolEnd ) {
  1879.             while ( url[ protocolEnd ] === '/' ) { protocolEnd += 1; }
  1880.         }
  1881.         insertNodeInRange(
  1882.             range,
  1883.             this._doc.createTextNode( url.slice( protocolEnd ) )
  1884.         );
  1885.     }
  1886.     attributes = mergeObjects(
  1887.         mergeObjects({
  1888.             href: url
  1889.         }, attributes, true ),
  1890.         this._config.tagAttributes.a,
  1891.         false
  1892.     );
  1893.  
  1894.     this.changeFormat({
  1895.         tag: 'A',
  1896.         attributes: attributes
  1897.     }, {
  1898.         tag: 'A'
  1899.     }, range );
  1900.     return this.focus();
  1901. };
  1902. proto.removeLink = function () {
  1903.     this.changeFormat( null, {
  1904.         tag: 'A'
  1905.     }, this.getSelection(), true );
  1906.     return this.focus();
  1907. };
  1908.  
  1909. proto.setFontFace = function ( name ) {
  1910.     this.changeFormat( name ? {
  1911.         tag: 'SPAN',
  1912.         attributes: {
  1913.             'class': FONT_FAMILY_CLASS,
  1914.             style: 'font-family: ' + name + ', sans-serif;'
  1915.         }
  1916.     } : null, {
  1917.         tag: 'SPAN',
  1918.         attributes: { 'class': FONT_FAMILY_CLASS }
  1919.     });
  1920.     return this.focus();
  1921. };
  1922. proto.setFontSize = function ( size ) {
  1923.     this.changeFormat( size ? {
  1924.         tag: 'SPAN',
  1925.         attributes: {
  1926.             'class': FONT_SIZE_CLASS,
  1927.             style: 'font-size: ' +
  1928.                 ( typeof size === 'number' ? size + 'px' : size )
  1929.         }
  1930.     } : null, {
  1931.         tag: 'SPAN',
  1932.         attributes: { 'class': FONT_SIZE_CLASS }
  1933.     });
  1934.     return this.focus();
  1935. };
  1936.  
  1937. proto.setTextColour = function ( colour ) {
  1938.     this.changeFormat( colour ? {
  1939.         tag: 'SPAN',
  1940.         attributes: {
  1941.             'class': COLOUR_CLASS,
  1942.             style: 'color:' + colour
  1943.         }
  1944.     } : null, {
  1945.         tag: 'SPAN',
  1946.         attributes: { 'class': COLOUR_CLASS }
  1947.     });
  1948.     return this.focus();
  1949. };
  1950.  
  1951. proto.setHighlightColour = function ( colour ) {
  1952.     this.changeFormat( colour ? {
  1953.         tag: 'SPAN',
  1954.         attributes: {
  1955.             'class': HIGHLIGHT_CLASS,
  1956.             style: 'background-color:' + colour
  1957.         }
  1958.     } : colour, {
  1959.         tag: 'SPAN',
  1960.         attributes: { 'class': HIGHLIGHT_CLASS }
  1961.     });
  1962.     return this.focus();
  1963. };
  1964.  
  1965. proto.setTextAlignment = function ( alignment ) {
  1966.     this.forEachBlock( function ( block ) {
  1967.         var className = block.className
  1968.             .split( /\s+/ )
  1969.             .filter( function ( klass ) {
  1970.                 return !!klass && !/^align/.test( klass );
  1971.             })
  1972.             .join( ' ' );
  1973.         if ( alignment ) {
  1974.             block.className = className + ' align-' + alignment;
  1975.             block.style.textAlign = alignment;
  1976.         } else {
  1977.             block.className = className;
  1978.             block.style.textAlign = '';
  1979.         }
  1980.     }, true );
  1981.     return this.focus();
  1982. };
  1983.  
  1984. proto.setTextDirection = function ( direction ) {
  1985.     this.forEachBlock( function ( block ) {
  1986.         if ( direction ) {
  1987.             block.dir = direction;
  1988.         } else {
  1989.             block.removeAttribute( 'dir' );
  1990.         }
  1991.     }, true );
  1992.     return this.focus();
  1993. };
  1994.  
  1995. function removeFormatting ( self, root, clean ) {
  1996.     var node, next;
  1997.     for ( node = root.firstChild; node; node = next ) {
  1998.         next = node.nextSibling;
  1999.         if ( isInline( node ) ) {
  2000.             if ( node.nodeType === TEXT_NODE || node.nodeName === 'BR' || node.nodeName === 'IMG' ) {
  2001.                 clean.appendChild( node );
  2002.                 continue;
  2003.             }
  2004.         } else if ( isBlock( node ) ) {
  2005.             clean.appendChild( self.createDefaultBlock([
  2006.                 removeFormatting(
  2007.                     self, node, self._doc.createDocumentFragment() )
  2008.             ]));
  2009.             continue;
  2010.         }
  2011.         removeFormatting( self, node, clean );
  2012.     }
  2013.     return clean;
  2014. }
  2015.  
  2016. proto.removeAllFormatting = function ( range ) {
  2017.     if ( !range && !( range = this.getSelection() ) || range.collapsed ) {
  2018.         return this;
  2019.     }
  2020.  
  2021.     var root = this._root;
  2022.     var stopNode = range.commonAncestorContainer;
  2023.     while ( stopNode && !isBlock( stopNode ) ) {
  2024.         stopNode = stopNode.parentNode;
  2025.     }
  2026.     if ( !stopNode ) {
  2027.         expandRangeToBlockBoundaries( range, root );
  2028.         stopNode = root;
  2029.     }
  2030.     if ( stopNode.nodeType === TEXT_NODE ) {
  2031.         return this;
  2032.     }
  2033.  
  2034.     // Record undo point
  2035.     this.saveUndoState( range );
  2036.  
  2037.     // Avoid splitting where we're already at edges.
  2038.     moveRangeBoundariesUpTree( range, stopNode );
  2039.  
  2040.     // Split the selection up to the block, or if whole selection in same
  2041.     // block, expand range boundaries to ends of block and split up to root.
  2042.     var doc = stopNode.ownerDocument;
  2043.     var startContainer = range.startContainer;
  2044.     var startOffset = range.startOffset;
  2045.     var endContainer = range.endContainer;
  2046.     var endOffset = range.endOffset;
  2047.  
  2048.     // Split end point first to avoid problems when end and start
  2049.     // in same container.
  2050.     var formattedNodes = doc.createDocumentFragment();
  2051.     var cleanNodes = doc.createDocumentFragment();
  2052.     var nodeAfterSplit = split( endContainer, endOffset, stopNode, root );
  2053.     var nodeInSplit = split( startContainer, startOffset, stopNode, root );
  2054.     var nextNode, childNodes;
  2055.  
  2056.     // Then replace contents in split with a cleaned version of the same:
  2057.     // blocks become default blocks, text and leaf nodes survive, everything
  2058.     // else is obliterated.
  2059.     while ( nodeInSplit !== nodeAfterSplit ) {
  2060.         nextNode = nodeInSplit.nextSibling;
  2061.         formattedNodes.appendChild( nodeInSplit );
  2062.         nodeInSplit = nextNode;
  2063.     }
  2064.     removeFormatting( this, formattedNodes, cleanNodes );
  2065.     cleanNodes.normalize();
  2066.     nodeInSplit = cleanNodes.firstChild;
  2067.     nextNode = cleanNodes.lastChild;
  2068.  
  2069.     // Restore selection
  2070.     childNodes = stopNode.childNodes;
  2071.     if ( nodeInSplit ) {
  2072.         stopNode.insertBefore( cleanNodes, nodeAfterSplit );
  2073.         startOffset = indexOf.call( childNodes, nodeInSplit );
  2074.         endOffset = indexOf.call( childNodes, nextNode ) + 1;
  2075.     } else {
  2076.         startOffset = indexOf.call( childNodes, nodeAfterSplit );
  2077.         endOffset = startOffset;
  2078.     }
  2079.  
  2080.     // Merge text nodes at edges, if possible
  2081.     range.setStart( stopNode, startOffset );
  2082.     range.setEnd( stopNode, endOffset );
  2083.     mergeInlines( stopNode, range );
  2084.  
  2085.     // And move back down the tree
  2086.     moveRangeBoundariesDownTree( range );
  2087.  
  2088.     this.setSelection( range );
  2089.     this._updatePath( range, true );
  2090.  
  2091.     return this.focus();
  2092. };
  2093.  
  2094. proto.increaseQuoteLevel = command( 'modifyBlocks', increaseBlockQuoteLevel );
  2095. proto.decreaseQuoteLevel = command( 'modifyBlocks', decreaseBlockQuoteLevel );
  2096.  
  2097. proto.makeUnorderedList = command( 'modifyBlocks', makeUnorderedList );
  2098. proto.makeOrderedList = command( 'modifyBlocks', makeOrderedList );
  2099. proto.removeList = command( 'modifyBlocks', removeList );
  2100.  
  2101. proto.increaseListLevel = command( 'modifyBlocks', increaseListLevel );
  2102. proto.decreaseListLevel = command( 'modifyBlocks', decreaseListLevel );
  2103.  
downloadEditor.js Source code - Download Squire Source code
Related Source Codes/Software:
thor - Thor is a toolkit for building powerful command-li... 2017-01-08
glide - Package Management for Golang h... 2017-01-08
TextFieldEffects - Custom UITextFields effects inspired by Codrops, b... 2017-01-08
flowchart.js - Draws simple SVG flow chart diagrams from textual ... 2017-01-08
RoundedImageView - A fast ImageView that supports rounded corners, ov... 2017-01-07
webpack-demos - a collection of simple demos of Webpack 2017-01-08
amazon-dsstne - Deep Scalable Sparse Tensor Network Engine (DSSTNE... 2017-01-08
rq - Simple job queues for Python ht... 2017-01-08
emmet-vim - emmet for vim: http://emmet.io/ ... 2017-01-08
prose - A Content Editor for GitHub. ht... 2017-01-08
CRYENGINE - CRYENGINE is a powerful real-time game development... 2017-06-11
postal - 2017-06-11
reactide - Reactide is the first dedicated IDE for React web ... 2017-06-11
rkt - rkt is a pod-native container engine for Linux. It... 2017-06-11
uWebSockets - Tiny WebSockets https://for... 2017-06-11
realworld - TodoMVC for the RealWorld - Exemplary fullstack Me... 2017-06-11
goreplay - GoReplay is an open-source tool for capturing and ... 2017-06-10
pyenv - Simple Python version management 2017-06-10
redux-saga - An alternative side effect model for Redux apps ... 2017-06-10
angular-starter - 2017-06-10

 Back to top