/* Copyright © 2011-2013 by Neil Jenkins. MIT Licensed. */ ( function ( doc, undefined ) { "use strict"; var DOCUMENT_POSITION_PRECEDING = 2; // Node.DOCUMENT_POSITION_PRECEDING var ELEMENT_NODE = 1; // Node.ELEMENT_NODE; var TEXT_NODE = 3; // Node.TEXT_NODE; var SHOW_ELEMENT = 1; // NodeFilter.SHOW_ELEMENT; var SHOW_TEXT = 4; // NodeFilter.SHOW_TEXT; var START_TO_START = 0; // Range.START_TO_START var START_TO_END = 1; // Range.START_TO_END var END_TO_END = 2; // Range.END_TO_END var END_TO_START = 3; // Range.END_TO_START var ZWS = '\u200B'; var win = doc.defaultView; var ua = navigator.userAgent; var isIOS = /iP(?:ad|hone|od)/.test( ua ); var isMac = /Mac OS X/.test( ua ); var isGecko = /Gecko\//.test( ua ); var isIElt11 = /Trident\/[456]\./.test( ua ); var isPresto = !!win.opera; var isWebKit = /WebKit\//.test( ua ); var ctrlKey = isMac ? 'meta-' : 'ctrl-'; var useTextFixer = isIElt11 || isPresto; var cantFocusEmptyTextNodes = isIElt11 || isWebKit; var losesSelectionOnBlur = isIElt11; var canObserveMutations = typeof MutationObserver !== 'undefined'; // Use [^ \t\r\n] instead of \S so that nbsp does not count as white-space var notWS = /[^ \t\r\n]/; var indexOf = Array.prototype.indexOf; /* Native TreeWalker is buggy in IE and Opera: * IE9/10 sometimes throw errors when calling TreeWalker#nextNode or TreeWalker#previousNode. No way to feature detect this. * Some versions of Opera have a bug in TreeWalker#previousNode which makes it skip to the wrong node. Rather than risk further bugs, it's easiest just to implement our own (subset) of the spec in all browsers. */ var typeToBitArray = { // ELEMENT_NODE 1: 1, // ATTRIBUTE_NODE 2: 2, // TEXT_NODE 3: 4, // COMMENT_NODE 8: 128, // DOCUMENT_NODE 9: 256, // DOCUMENT_FRAGMENT_NODE 11: 1024 }; function TreeWalker ( root, nodeType, filter ) { this.root = this.currentNode = root; this.nodeType = nodeType; this.filter = filter; } TreeWalker.prototype.nextNode = function () { var current = this.currentNode, root = this.root, nodeType = this.nodeType, filter = this.filter, node; while ( true ) { node = current.firstChild; while ( !node && current ) { if ( current === root ) { break; } node = current.nextSibling; if ( !node ) { current = current.parentNode; } } if ( !node ) { return null; } if ( ( typeToBitArray[ node.nodeType ] & nodeType ) && filter( node ) ) { this.currentNode = node; return node; } current = node; } }; TreeWalker.prototype.previousNode = function () { var current = this.currentNode, root = this.root, nodeType = this.nodeType, filter = this.filter, node; while ( true ) { if ( current === root ) { return null; } node = current.previousSibling; if ( node ) { while ( current = node.lastChild ) { node = current; } } else { node = current.parentNode; } if ( !node ) { return null; } if ( ( typeToBitArray[ node.nodeType ] & nodeType ) && filter( node ) ) { this.currentNode = node; return node; } current = node; } }; var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TR(?:IKE|ONG)|MALL|AMP)?|U|VAR|WBR)$/; var leafNodeNames = { BR: 1, IMG: 1, INPUT: 1 }; function every ( nodeList, fn ) { var l = nodeList.length; while ( l-- ) { if ( !fn( nodeList[l] ) ) { return false; } } return true; } // --- function hasTagAttributes ( node, tag, attributes ) { if ( node.nodeName !== tag ) { return false; } for ( var attr in attributes ) { if ( node.getAttribute( attr ) !== attributes[ attr ] ) { return false; } } return true; } function areAlike ( node, node2 ) { return ( node.nodeType === node2.nodeType && node.nodeName === node2.nodeName && node.className === node2.className && ( ( !node.style && !node2.style ) || node.style.cssText === node2.style.cssText ) ); } function isLeaf ( node ) { return node.nodeType === ELEMENT_NODE && !!leafNodeNames[ node.nodeName ]; } function isInline ( node ) { return inlineNodeNames.test( node.nodeName ); } function isBlock ( node ) { return node.nodeType === ELEMENT_NODE && !isInline( node ) && every( node.childNodes, isInline ); } function isContainer ( node ) { return node.nodeType === ELEMENT_NODE && !isInline( node ) && !isBlock( node ); } function getBlockWalker ( node ) { var doc = node.ownerDocument, walker = new TreeWalker( doc.body, SHOW_ELEMENT, isBlock, false ); walker.currentNode = node; return walker; } function getPreviousBlock ( node ) { return getBlockWalker( node ).previousNode(); } function getNextBlock ( node ) { return getBlockWalker( node ).nextNode(); } function getNearest ( node, tag, attributes ) { do { if ( hasTagAttributes( node, tag, attributes ) ) { return node; } } while ( node = node.parentNode ); return null; } function getPath ( node ) { var parent = node.parentNode, path, id, className, classNames; if ( !parent || node.nodeType !== ELEMENT_NODE ) { path = parent ? getPath( parent ) : ''; } else { path = getPath( parent ); path += ( path ? '>' : '' ) + node.nodeName; if ( id = node.id ) { path += '#' + id; } if ( className = node.className.trim() ) { classNames = className.split( /\s\s*/ ); classNames.sort(); path += '.'; path += classNames.join( '.' ); } } return path; } function getLength ( node ) { var nodeType = node.nodeType; return nodeType === ELEMENT_NODE ? node.childNodes.length : node.length || 0; } function detach ( node ) { var parent = node.parentNode; if ( parent ) { parent.removeChild( node ); } return node; } function replaceWith ( node, node2 ) { var parent = node.parentNode; if ( parent ) { parent.replaceChild( node2, node ); } } function empty ( node ) { var frag = node.ownerDocument.createDocumentFragment(), childNodes = node.childNodes, l = childNodes ? childNodes.length : 0; while ( l-- ) { frag.appendChild( node.firstChild ); } return frag; } function createElement ( doc, tag, props, children ) { var el = doc.createElement( tag ), attr, value, i, l; if ( props instanceof Array ) { children = props; props = null; } if ( props ) { for ( attr in props ) { value = props[ attr ]; if ( value !== undefined ) { el.setAttribute( attr, props[ attr ] ); } } } if ( children ) { for ( i = 0, l = children.length; i < l; i += 1 ) { el.appendChild( children[i] ); } } return el; } function fixCursor ( node ) { // In Webkit and Gecko, block level elements are collapsed and // unfocussable if they have no content. To remedy this, a
must be // inserted. In Opera and IE, we just need a textnode in order for the // cursor to appear. var doc = node.ownerDocument, root = node, fixer, child, instance; if ( node.nodeName === 'BODY' ) { if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) { instance = getSquireInstance( doc ); fixer = instance ? instance.createDefaultBlock() : createElement( doc, 'DIV' ); if ( child ) { node.replaceChild( fixer, child ); } else { node.appendChild( fixer ); } node = fixer; fixer = null; } } if ( isInline( node ) ) { child = node.firstChild; while ( cantFocusEmptyTextNodes && child && child.nodeType === TEXT_NODE && !child.data ) { node.removeChild( child ); child = node.firstChild; } if ( !child ) { if ( cantFocusEmptyTextNodes ) { fixer = doc.createTextNode( ZWS ); getSquireInstance( doc )._didAddZWS(); } else { fixer = doc.createTextNode( '' ); } } } else { if ( useTextFixer ) { while ( node.nodeType !== TEXT_NODE && !isLeaf( node ) ) { child = node.firstChild; if ( !child ) { fixer = doc.createTextNode( '' ); break; } node = child; } if ( node.nodeType === TEXT_NODE ) { // Opera will collapse the block element if it contains // just spaces (but not if it contains no data at all). if ( /^ +$/.test( node.data ) ) { node.data = ''; } } else if ( isLeaf( node ) ) { node.parentNode.insertBefore( doc.createTextNode( '' ), node ); } } else if ( !node.querySelector( 'BR' ) ) { fixer = createElement( doc, 'BR' ); while ( ( child = node.lastElementChild ) && !isInline( child ) ) { node = child; } } } if ( fixer ) { node.appendChild( fixer ); } return root; } // Recursively examine container nodes and wrap any inline children. function fixContainer ( container ) { var children = container.childNodes, doc = container.ownerDocument, wrapper = null, i, l, child, isBR; for ( i = 0, l = children.length; i < l; i += 1 ) { child = children[i]; isBR = child.nodeName === 'BR'; if ( !isBR && isInline( child ) ) { if ( !wrapper ) { wrapper = createElement( doc, 'DIV' ); } wrapper.appendChild( child ); i -= 1; l -= 1; } else if ( isBR || wrapper ) { if ( !wrapper ) { wrapper = createElement( doc, 'DIV' ); } fixCursor( wrapper ); if ( isBR ) { container.replaceChild( wrapper, child ); } else { container.insertBefore( wrapper, child ); i += 1; l += 1; } wrapper = null; } if ( isContainer( child ) ) { fixContainer( child ); } } if ( wrapper ) { container.appendChild( fixCursor( wrapper ) ); } return container; } function split ( node, offset, stopNode ) { var nodeType = node.nodeType, parent, clone, next; if ( nodeType === TEXT_NODE && node !== stopNode ) { return split( node.parentNode, node.splitText( offset ), stopNode ); } if ( nodeType === ELEMENT_NODE ) { if ( typeof( offset ) === 'number' ) { offset = offset < node.childNodes.length ? node.childNodes[ offset ] : null; } if ( node === stopNode ) { return offset; } // Clone node without children parent = node.parentNode; clone = node.cloneNode( false ); // Add right-hand siblings to the clone while ( offset ) { next = offset.nextSibling; clone.appendChild( offset ); offset = next; } // Maintain li numbering if inside a quote. if ( node.nodeName === 'OL' && getNearest( node, 'BLOCKQUOTE' ) ) { clone.start = ( +node.start || 1 ) + node.childNodes.length - 1; } // DO NOT NORMALISE. This may undo the fixCursor() call // of a node lower down the tree! // We need something in the element in order for the cursor to appear. fixCursor( node ); fixCursor( clone ); // Inject clone after original node if ( next = node.nextSibling ) { parent.insertBefore( clone, next ); } else { parent.appendChild( clone ); } // Keep on splitting up the tree return split( parent, clone, stopNode ); } return offset; } function mergeInlines ( node, range ) { if ( node.nodeType !== ELEMENT_NODE ) { return; } var children = node.childNodes, l = children.length, frags = [], child, prev, len; while ( l-- ) { child = children[l]; prev = l && children[ l - 1 ]; if ( l && isInline( child ) && areAlike( child, prev ) && !leafNodeNames[ child.nodeName ] ) { if ( range.startContainer === child ) { range.startContainer = prev; range.startOffset += getLength( prev ); } if ( range.endContainer === child ) { range.endContainer = prev; range.endOffset += getLength( prev ); } if ( range.startContainer === node ) { if ( range.startOffset > l ) { range.startOffset -= 1; } else if ( range.startOffset === l ) { range.startContainer = prev; range.startOffset = getLength( prev ); } } if ( range.endContainer === node ) { if ( range.endOffset > l ) { range.endOffset -= 1; } else if ( range.endOffset === l ) { range.endContainer = prev; range.endOffset = getLength( prev ); } } detach( child ); if ( child.nodeType === TEXT_NODE ) { prev.appendData( child.data ); } else { frags.push( empty( child ) ); } } else if ( child.nodeType === ELEMENT_NODE ) { len = frags.length; while ( len-- ) { child.appendChild( frags.pop() ); } mergeInlines( child, range ); } } } function mergeWithBlock ( block, next, range ) { var container = next, last, offset, _range; while ( container.parentNode.childNodes.length === 1 ) { container = container.parentNode; } detach( container ); offset = block.childNodes.length; // Remove extra
fixer if present. last = block.lastChild; if ( last && last.nodeName === 'BR' ) { block.removeChild( last ); offset -= 1; } _range = { startContainer: block, startOffset: offset, endContainer: block, endOffset: offset }; block.appendChild( empty( next ) ); mergeInlines( block, _range ); range.setStart( _range.startContainer, _range.startOffset ); range.collapse( true ); // Opera inserts a BR if you delete the last piece of text // in a block-level element. Unfortunately, it then gets // confused when setting the selection subsequently and // refuses to accept the range that finishes just before the // BR. Removing the BR fixes the bug. // Steps to reproduce bug: Type "a-b-c" (where - is return) // then backspace twice. The cursor goes to the top instead // of after "b". if ( isPresto && ( last = block.lastChild ) && last.nodeName === 'BR' ) { block.removeChild( last ); } } function mergeContainers ( node ) { var prev = node.previousSibling, first = node.firstChild, doc = node.ownerDocument, isListItem = ( node.nodeName === 'LI' ), needsFix, block; // Do not merge LIs, unless it only contains a UL if ( isListItem && ( !first || !/^[OU]L$/.test( first.nodeName ) ) ) { return; } if ( prev && areAlike( prev, node ) ) { if ( !isContainer( prev ) ) { if ( isListItem ) { block = createElement( doc, 'DIV' ); block.appendChild( empty( prev ) ); prev.appendChild( block ); } else { return; } } detach( node ); needsFix = !isContainer( node ); prev.appendChild( empty( node ) ); if ( needsFix ) { fixContainer( prev ); } if ( first ) { mergeContainers( first ); } } else if ( isListItem ) { prev = createElement( doc, 'DIV' ); node.insertBefore( prev, first ); fixCursor( prev ); } } var getNodeBefore = function ( node, offset ) { var children = node.childNodes; while ( offset && node.nodeType === ELEMENT_NODE ) { node = children[ offset - 1 ]; children = node.childNodes; offset = children.length; } return node; }; var getNodeAfter = function ( node, offset ) { if ( node.nodeType === ELEMENT_NODE ) { var children = node.childNodes; if ( offset < children.length ) { node = children[ offset ]; } else { while ( node && !node.nextSibling ) { node = node.parentNode; } if ( node ) { node = node.nextSibling; } } } return node; }; // --- var forEachTextNodeInRange = function ( range, fn ) { range = range.cloneRange(); moveRangeBoundariesDownTree( range ); var startContainer = range.startContainer, endContainer = range.endContainer, root = range.commonAncestorContainer, walker = new TreeWalker( root, SHOW_TEXT, function (/* node */) { return true; }, false ), textnode = walker.currentNode = startContainer; while ( !fn( textnode, range ) && textnode !== endContainer && ( textnode = walker.nextNode() ) ) {} }; var getTextContentInRange = function ( range ) { var textContent = ''; forEachTextNodeInRange( range, function ( textnode, range ) { var value = textnode.data; if ( value && ( /\S/.test( value ) ) ) { if ( textnode === range.endContainer ) { value = value.slice( 0, range.endOffset ); } if ( textnode === range.startContainer ) { value = value.slice( range.startOffset ); } textContent += value; } }); return textContent; }; // --- var insertNodeInRange = function ( range, node ) { // Insert at start. var startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset, parent, children, childCount, afterSplit; // If part way through a text node, split it. if ( startContainer.nodeType === TEXT_NODE ) { parent = startContainer.parentNode; children = parent.childNodes; if ( startOffset === startContainer.length ) { startOffset = indexOf.call( children, startContainer ) + 1; if ( range.collapsed ) { endContainer = parent; endOffset = startOffset; } } else { if ( startOffset ) { afterSplit = startContainer.splitText( startOffset ); if ( endContainer === startContainer ) { endOffset -= startOffset; endContainer = afterSplit; } else if ( endContainer === parent ) { endOffset += 1; } startContainer = afterSplit; } startOffset = indexOf.call( children, startContainer ); } startContainer = parent; } else { children = startContainer.childNodes; } childCount = children.length; if ( startOffset === childCount) { startContainer.appendChild( node ); } else { startContainer.insertBefore( node, children[ startOffset ] ); } if ( startContainer === endContainer ) { endOffset += children.length - childCount; } range.setStart( startContainer, startOffset ); range.setEnd( endContainer, endOffset ); }; var extractContentsOfRange = function ( range, common ) { var startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset; if ( !common ) { common = range.commonAncestorContainer; } if ( common.nodeType === TEXT_NODE ) { common = common.parentNode; } var endNode = split( endContainer, endOffset, common ), startNode = split( startContainer, startOffset, common ), frag = common.ownerDocument.createDocumentFragment(), next, before, after; // End node will be null if at end of child nodes list. while ( startNode !== endNode ) { next = startNode.nextSibling; frag.appendChild( startNode ); startNode = next; } startContainer = common; startOffset = endNode ? indexOf.call( common.childNodes, endNode ) : common.childNodes.length; // Merge text nodes if adjacent. IE10 in particular will not focus // between two text nodes after = common.childNodes[ startOffset ]; before = after && after.previousSibling; if ( before && before.nodeType === TEXT_NODE && after.nodeType === TEXT_NODE ) { startContainer = before; startOffset = before.length; before.appendData( after.data ); detach( after ); } range.setStart( startContainer, startOffset ); range.collapse( true ); fixCursor( common ); return frag; }; var deleteContentsOfRange = function ( range ) { // Move boundaries up as much as possible to reduce need to split. moveRangeBoundariesUpTree( range ); // Remove selected range extractContentsOfRange( range ); // Move boundaries back down tree so that they are inside the blocks. // If we don't do this, the range may be collapsed to a point between // two blocks, so get(Start|End)BlockOfRange will return null. moveRangeBoundariesDownTree( range ); // If we split into two different blocks, merge the blocks. var startBlock = getStartBlockOfRange( range ), endBlock = getEndBlockOfRange( range ); if ( startBlock && endBlock && startBlock !== endBlock ) { mergeWithBlock( startBlock, endBlock, range ); } // Ensure block has necessary children if ( startBlock ) { fixCursor( startBlock ); } // Ensure body has a block-level element in it. var body = range.endContainer.ownerDocument.body, child = body.firstChild; if ( !child || child.nodeName === 'BR' ) { fixCursor( body ); range.selectNodeContents( body.firstChild ); } }; // --- var insertTreeFragmentIntoRange = function ( range, frag ) { // Check if it's all inline content var allInline = true, children = frag.childNodes, l = children.length; while ( l-- ) { if ( !isInline( children[l] ) ) { allInline = false; break; } } // Delete any selected content if ( !range.collapsed ) { deleteContentsOfRange( range ); } // Move range down into text nodes moveRangeBoundariesDownTree( range ); // If inline, just insert at the current position. if ( allInline ) { insertNodeInRange( range, frag ); range.collapse( false ); } // Otherwise, split up to body, insert inline before and after split // and insert block in between split, then merge containers. else { var nodeAfterSplit = split( range.startContainer, range.startOffset, range.startContainer.ownerDocument.body ), nodeBeforeSplit = nodeAfterSplit.previousSibling, startContainer = nodeBeforeSplit, startOffset = startContainer.childNodes.length, endContainer = nodeAfterSplit, endOffset = 0, parent = nodeAfterSplit.parentNode, child, node; while ( ( child = startContainer.lastChild ) && child.nodeType === ELEMENT_NODE && child.nodeName !== 'BR' ) { startContainer = child; startOffset = startContainer.childNodes.length; } while ( ( child = endContainer.firstChild ) && child.nodeType === ELEMENT_NODE && child.nodeName !== 'BR' ) { endContainer = child; } while ( ( child = frag.firstChild ) && isInline( child ) ) { startContainer.appendChild( child ); } while ( ( child = frag.lastChild ) && isInline( child ) ) { endContainer.insertBefore( child, endContainer.firstChild ); endOffset += 1; } // Fix cursor then insert block(s) node = frag; while ( node = getNextBlock( node ) ) { fixCursor( node ); } parent.insertBefore( frag, nodeAfterSplit ); // Remove empty nodes created by split and merge inserted containers // with edges of split node = nodeAfterSplit.previousSibling; if ( !nodeAfterSplit.textContent ) { parent.removeChild( nodeAfterSplit ); } else { mergeContainers( nodeAfterSplit ); } if ( !nodeAfterSplit.parentNode ) { endContainer = node; endOffset = getLength( endContainer ); } if ( !nodeBeforeSplit.textContent) { startContainer = nodeBeforeSplit.nextSibling; startOffset = 0; parent.removeChild( nodeBeforeSplit ); } else { mergeContainers( nodeBeforeSplit ); } range.setStart( startContainer, startOffset ); range.setEnd( endContainer, endOffset ); moveRangeBoundariesDownTree( range ); } }; // --- var isNodeContainedInRange = function ( range, node, partial ) { var nodeRange = node.ownerDocument.createRange(); nodeRange.selectNode( node ); if ( partial ) { // Node must not finish before range starts or start after range // finishes. var nodeEndBeforeStart = ( range.compareBoundaryPoints( END_TO_START, nodeRange ) > -1 ), nodeStartAfterEnd = ( range.compareBoundaryPoints( START_TO_END, nodeRange ) < 1 ); return ( !nodeEndBeforeStart && !nodeStartAfterEnd ); } else { // Node must start after range starts and finish before range // finishes var nodeStartAfterStart = ( range.compareBoundaryPoints( START_TO_START, nodeRange ) < 1 ), nodeEndBeforeEnd = ( range.compareBoundaryPoints( END_TO_END, nodeRange ) > -1 ); return ( nodeStartAfterStart && nodeEndBeforeEnd ); } }; var moveRangeBoundariesDownTree = function ( range ) { var startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset, child; while ( startContainer.nodeType !== TEXT_NODE ) { child = startContainer.childNodes[ startOffset ]; if ( !child || isLeaf( child ) ) { break; } startContainer = child; startOffset = 0; } if ( endOffset ) { while ( endContainer.nodeType !== TEXT_NODE ) { child = endContainer.childNodes[ endOffset - 1 ]; if ( !child || isLeaf( child ) ) { break; } endContainer = child; endOffset = getLength( endContainer ); } } else { while ( endContainer.nodeType !== TEXT_NODE ) { child = endContainer.firstChild; if ( !child || isLeaf( child ) ) { break; } endContainer = child; } } // If collapsed, this algorithm finds the nearest text node positions // *outside* the range rather than inside, but also it flips which is // assigned to which. if ( range.collapsed ) { range.setStart( endContainer, endOffset ); range.setEnd( startContainer, startOffset ); } else { range.setStart( startContainer, startOffset ); range.setEnd( endContainer, endOffset ); } }; var moveRangeBoundariesUpTree = function ( range, common ) { var startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset, parent; if ( !common ) { common = range.commonAncestorContainer; } while ( startContainer !== common && !startOffset ) { parent = startContainer.parentNode; startOffset = indexOf.call( parent.childNodes, startContainer ); startContainer = parent; } while ( endContainer !== common && endOffset === getLength( endContainer ) ) { parent = endContainer.parentNode; endOffset = indexOf.call( parent.childNodes, endContainer ) + 1; endContainer = parent; } range.setStart( startContainer, startOffset ); range.setEnd( endContainer, endOffset ); }; // Returns the first block at least partially contained by the range, // or null if no block is contained by the range. var getStartBlockOfRange = function ( range ) { var container = range.startContainer, block; // If inline, get the containing block. if ( isInline( container ) ) { block = getPreviousBlock( container ); } else if ( isBlock( container ) ) { block = container; } else { block = getNodeBefore( container, range.startOffset ); block = getNextBlock( block ); } // Check the block actually intersects the range return block && isNodeContainedInRange( range, block, true ) ? block : null; }; // Returns the last block at least partially contained by the range, // or null if no block is contained by the range. var getEndBlockOfRange = function ( range ) { var container = range.endContainer, block, child; // If inline, get the containing block. if ( isInline( container ) ) { block = getPreviousBlock( container ); } else if ( isBlock( container ) ) { block = container; } else { block = getNodeAfter( container, range.endOffset ); if ( !block ) { block = container.ownerDocument.body; while ( child = block.lastChild ) { block = child; } } block = getPreviousBlock( block ); } // Check the block actually intersects the range return block && isNodeContainedInRange( range, block, true ) ? block : null; }; var contentWalker = new TreeWalker( null, SHOW_TEXT|SHOW_ELEMENT, function ( node ) { return node.nodeType === TEXT_NODE ? notWS.test( node.data ) : node.nodeName === 'IMG'; } ); var rangeDoesStartAtBlockBoundary = function ( range ) { var startContainer = range.startContainer, startOffset = range.startOffset; // If in the middle or end of a text node, we're not at the boundary. if ( startContainer.nodeType === TEXT_NODE ) { if ( startOffset ) { return false; } contentWalker.currentNode = startContainer; } else { contentWalker.currentNode = getNodeAfter( startContainer, startOffset ); } // Otherwise, look for any previous content in the same block. contentWalker.root = getStartBlockOfRange( range ); return !contentWalker.previousNode(); }; var rangeDoesEndAtBlockBoundary = function ( range ) { var endContainer = range.endContainer, endOffset = range.endOffset, length; // If in a text node with content, and not at the end, we're not // at the boundary if ( endContainer.nodeType === TEXT_NODE ) { length = endContainer.data.length; if ( length && endOffset < length ) { return false; } contentWalker.currentNode = endContainer; } else { contentWalker.currentNode = getNodeBefore( endContainer, endOffset ); } // Otherwise, look for any further content in the same block. contentWalker.root = getEndBlockOfRange( range ); return !contentWalker.nextNode(); }; var expandRangeToBlockBoundaries = function ( range ) { var start = getStartBlockOfRange( range ), end = getEndBlockOfRange( range ), parent; if ( start && end ) { parent = start.parentNode; range.setStart( parent, indexOf.call( parent.childNodes, start ) ); parent = end.parentNode; range.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 ); } }; var instances = []; function getSquireInstance ( doc ) { var l = instances.length, instance; while ( l-- ) { instance = instances[l]; if ( instance._doc === doc ) { return instance; } } return null; } function Squire ( doc ) { var win = doc.defaultView; var body = doc.body; var mutation; this._win = win; this._doc = doc; this._body = body; this._events = {}; this._sel = win.getSelection(); this._lastSelection = null; // IE loses selection state of iframe on blur, so make sure we // cache it just before it loses focus. if ( losesSelectionOnBlur ) { this.addEventListener( 'beforedeactivate', this.getSelection ); } this._hasZWS = false; this._lastAnchorNode = null; this._lastFocusNode = null; this._path = ''; this.addEventListener( 'keyup', this._updatePathOnEvent ); this.addEventListener( 'mouseup', this._updatePathOnEvent ); win.addEventListener( 'focus', this, false ); win.addEventListener( 'blur', this, false ); this._undoIndex = -1; this._undoStack = []; this._undoStackLength = 0; this._isInUndoState = false; this._ignoreChange = false; if ( canObserveMutations ) { mutation = new MutationObserver( this._docWasChanged.bind( this ) ); mutation.observe( body, { childList: true, attributes: true, characterData: true, subtree: true }); this._mutation = mutation; } else { this.addEventListener( 'keyup', this._keyUpDetectChange ); } this.defaultBlockTag = 'DIV'; this.defaultBlockProperties = null; // IE sometimes fires the beforepaste event twice; make sure it is not run // again before our after paste function is called. this._awaitingPaste = false; this.addEventListener( isIElt11 ? 'beforecut' : 'cut', this._onCut ); this.addEventListener( isIElt11 ? 'beforepaste' : 'paste', this._onPaste ); // Opera does not fire keydown repeatedly. this.addEventListener( isPresto ? 'keypress' : 'keydown', this._onKey ); // Fix IE<10's buggy implementation of Text#splitText. // If the split is at the end of the node, it doesn't insert the newly split // node into the document, and sets its value to undefined rather than ''. // And even if the split is not at the end, the original node is removed // from the document and replaced by another, rather than just having its // data shortened. // We used to feature test for this, but then found the feature test would // sometimes pass, but later on the buggy behaviour would still appear. // I think IE10 does not have the same bug, but it doesn't hurt to replace // its native fn too and then we don't need yet another UA category. if ( isIElt11 ) { win.Text.prototype.splitText = function ( offset ) { var afterSplit = this.ownerDocument.createTextNode( this.data.slice( offset ) ), next = this.nextSibling, parent = this.parentNode, toDelete = this.length - offset; if ( next ) { parent.insertBefore( afterSplit, next ); } else { parent.appendChild( afterSplit ); } if ( toDelete ) { this.deleteData( offset, toDelete ); } return afterSplit; }; } body.setAttribute( 'contenteditable', 'true' ); this.setHTML( '' ); // Remove Firefox's built-in controls try { doc.execCommand( 'enableObjectResizing', false, 'false' ); doc.execCommand( 'enableInlineTableEditing', false, 'false' ); } catch ( error ) {} instances.push( this ); } var proto = Squire.prototype; proto.createElement = function ( tag, props, children ) { return createElement( this._doc, tag, props, children ); }; proto.createDefaultBlock = function ( children ) { return fixCursor( this.createElement( this.defaultBlockTag, this.defaultBlockProperties, children ) ); }; proto.didError = function ( error ) { console.log( error ); }; proto.getDocument = function () { return this._doc; }; // --- Events --- // Subscribing to these events won't automatically add a listener to the // document node, since these events are fired in a custom manner by the // editor code. var customEvents = { focus: 1, blur: 1, pathChange: 1, select: 1, input: 1, undoStateChange: 1 }; proto.fireEvent = function ( type, event ) { var handlers = this._events[ type ], i, l, obj; if ( handlers ) { if ( !event ) { event = {}; } if ( event.type !== type ) { event.type = type; } // Clone handlers array, so any handlers added/removed do not affect it. handlers = handlers.slice(); for ( i = 0, l = handlers.length; i < l; i += 1 ) { obj = handlers[i]; try { if ( obj.handleEvent ) { obj.handleEvent( event ); } else { obj.call( this, event ); } } catch ( error ) { error.details = 'Squire: fireEvent error. Event type: ' + type; this.didError( error ); } } } return this; }; proto.destroy = function () { var win = this._win, doc = this._doc, events = this._events, type; win.removeEventListener( 'focus', this, false ); win.removeEventListener( 'blur', this, false ); for ( type in events ) { if ( !customEvents[ type ] ) { doc.removeEventListener( type, this, true ); } } if ( this._mutation ) { this._mutation.disconnect(); } var l = instances.length; while ( l-- ) { if ( instances[l] === this ) { instances.splice( l, 1 ); } } }; proto.handleEvent = function ( event ) { this.fireEvent( event.type, event ); }; proto.addEventListener = function ( type, fn ) { var handlers = this._events[ type ]; if ( !fn ) { this.didError({ name: 'Squire: addEventListener with null or undefined fn', message: 'Event type: ' + type }); return this; } if ( !handlers ) { handlers = this._events[ type ] = []; if ( !customEvents[ type ] ) { this._doc.addEventListener( type, this, true ); } } handlers.push( fn ); return this; }; proto.removeEventListener = function ( type, fn ) { var handlers = this._events[ type ], l; if ( handlers ) { l = handlers.length; while ( l-- ) { if ( handlers[l] === fn ) { handlers.splice( l, 1 ); } } if ( !handlers.length ) { delete this._events[ type ]; if ( !customEvents[ type ] ) { this._doc.removeEventListener( type, this, false ); } } } return this; }; // --- Selection and Path --- proto._createRange = function ( range, startOffset, endContainer, endOffset ) { if ( range instanceof this._win.Range ) { return range.cloneRange(); } var domRange = this._doc.createRange(); domRange.setStart( range, startOffset ); if ( endContainer ) { domRange.setEnd( endContainer, endOffset ); } else { domRange.setEnd( range, startOffset ); } return domRange; }; proto.setSelection = function ( range ) { if ( range ) { // iOS bug: if you don't focus the iframe before setting the // selection, you can end up in a state where you type but the input // doesn't get directed into the contenteditable area but is instead // lost in a black hole. Very strange. if ( isIOS ) { this._win.focus(); } var sel = this._sel; sel.removeAllRanges(); sel.addRange( range ); } return this; }; proto.getSelection = function () { var sel = this._sel, selection, startContainer, endContainer; if ( sel.rangeCount ) { selection = sel.getRangeAt( 0 ).cloneRange(); startContainer = selection.startContainer; endContainer = selection.endContainer; // FF can return the selection as being inside an . WTF? if ( startContainer && isLeaf( startContainer ) ) { selection.setStartBefore( startContainer ); } if ( endContainer && isLeaf( endContainer ) ) { selection.setEndBefore( endContainer ); } this._lastSelection = selection; } else { selection = this._lastSelection; } if ( !selection ) { selection = this._createRange( this._body.firstChild, 0 ); } return selection; }; proto.getSelectedText = function () { return getTextContentInRange( this.getSelection() ); }; proto.getPath = function () { return this._path; }; // --- Workaround for browsers that can't focus empty text nodes --- // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256 var removeZWS = function ( root ) { var walker = new TreeWalker( root, SHOW_TEXT, function () { return true; }, false ), node, index; while ( node = walker.nextNode() ) { while ( ( index = node.data.indexOf( ZWS ) ) > -1 ) { node.deleteData( index, 1 ); } } }; proto._didAddZWS = function () { this._hasZWS = true; }; proto._removeZWS = function () { if ( !this._hasZWS ) { return; } removeZWS( this._body ); this._hasZWS = false; }; // --- Path change events --- proto._updatePath = function ( range, force ) { var anchor = range.startContainer, focus = range.endContainer, newPath; if ( force || anchor !== this._lastAnchorNode || focus !== this._lastFocusNode ) { this._lastAnchorNode = anchor; this._lastFocusNode = focus; newPath = ( anchor && focus ) ? ( anchor === focus ) ? getPath( focus ) : '(selection)' : ''; if ( this._path !== newPath ) { this._path = newPath; this.fireEvent( 'pathChange', { path: newPath } ); } } if ( !range.collapsed ) { this.fireEvent( 'select' ); } }; proto._updatePathOnEvent = function () { this._updatePath( this.getSelection() ); }; // --- Focus --- proto.focus = function () { // FF seems to need the body to be focussed (at least on first load). // Chrome also now needs body to be focussed in order to show the cursor // (otherwise it is focussed, but the cursor doesn't appear). // Opera (Presto-variant) however will lose the selection if you call this! if ( !isPresto ) { this._body.focus(); } this._win.focus(); return this; }; proto.blur = function () { // IE will remove the whole browser window from focus if you call // win.blur() or body.blur(), so instead we call top.focus() to focus // the top frame, thus blurring this frame. This works in everything // except FF, so we need to call body.blur() in that as well. if ( isGecko ) { this._body.blur(); } top.focus(); return this; }; // --- Bookmarking --- var startSelectionId = 'squire-selection-start'; var endSelectionId = 'squire-selection-end'; proto._saveRangeToBookmark = function ( range ) { var startNode = this.createElement( 'INPUT', { id: startSelectionId, type: 'hidden' }), endNode = this.createElement( 'INPUT', { id: endSelectionId, type: 'hidden' }), temp; insertNodeInRange( range, startNode ); range.collapse( false ); insertNodeInRange( range, endNode ); // In a collapsed range, the start is sometimes inserted after the end! if ( startNode.compareDocumentPosition( endNode ) & DOCUMENT_POSITION_PRECEDING ) { startNode.id = endSelectionId; endNode.id = startSelectionId; temp = startNode; startNode = endNode; endNode = temp; } range.setStartAfter( startNode ); range.setEndBefore( endNode ); }; proto._getRangeAndRemoveBookmark = function ( range ) { var doc = this._doc, start = doc.getElementById( startSelectionId ), end = doc.getElementById( endSelectionId ); if ( start && end ) { var startContainer = start.parentNode, endContainer = end.parentNode, collapsed; var _range = { startContainer: startContainer, endContainer: endContainer, startOffset: indexOf.call( startContainer.childNodes, start ), endOffset: indexOf.call( endContainer.childNodes, end ) }; if ( startContainer === endContainer ) { _range.endOffset -= 1; } detach( start ); detach( end ); // Merge any text nodes we split mergeInlines( startContainer, _range ); if ( startContainer !== endContainer ) { mergeInlines( endContainer, _range ); } if ( !range ) { range = doc.createRange(); } range.setStart( _range.startContainer, _range.startOffset ); range.setEnd( _range.endContainer, _range.endOffset ); collapsed = range.collapsed; moveRangeBoundariesDownTree( range ); if ( collapsed ) { range.collapse( true ); } } return range || null; }; // --- Undo --- proto._keyUpDetectChange = function ( event ) { var code = event.keyCode; // Presume document was changed if: // 1. A modifier key (other than shift) wasn't held down // 2. The key pressed is not in range 16<=x<=20 (control keys) // 3. The key pressed is not in range 33<=x<=45 (navigation keys) if ( !event.ctrlKey && !event.metaKey && !event.altKey && ( code < 16 || code > 20 ) && ( code < 33 || code > 45 ) ) { this._docWasChanged(); } }; proto._docWasChanged = function () { if ( canObserveMutations && this._ignoreChange ) { this._ignoreChange = false; return; } if ( this._isInUndoState ) { this._isInUndoState = false; this.fireEvent( 'undoStateChange', { canUndo: true, canRedo: false }); } this.fireEvent( 'input' ); }; // Leaves bookmark proto._recordUndoState = function ( range ) { // Don't record if we're already in an undo state if ( !this._isInUndoState ) { // Advance pointer to new position var undoIndex = this._undoIndex += 1, undoStack = this._undoStack; // Truncate stack if longer (i.e. if has been previously undone) if ( undoIndex < this._undoStackLength) { undoStack.length = this._undoStackLength = undoIndex; } // Write out data if ( range ) { this._saveRangeToBookmark( range ); } undoStack[ undoIndex ] = this._getHTML(); this._undoStackLength += 1; this._isInUndoState = true; } }; proto.undo = function () { // Sanity check: must not be at beginning of the history stack if ( this._undoIndex !== 0 || !this._isInUndoState ) { // Make sure any changes since last checkpoint are saved. this._recordUndoState( this.getSelection() ); this._undoIndex -= 1; this._setHTML( this._undoStack[ this._undoIndex ] ); var range = this._getRangeAndRemoveBookmark(); if ( range ) { this.setSelection( range ); } this._isInUndoState = true; this.fireEvent( 'undoStateChange', { canUndo: this._undoIndex !== 0, canRedo: true }); this.fireEvent( 'input' ); } return this; }; proto.redo = function () { // Sanity check: must not be at end of stack and must be in an undo // state. var undoIndex = this._undoIndex, undoStackLength = this._undoStackLength; if ( undoIndex + 1 < undoStackLength && this._isInUndoState ) { this._undoIndex += 1; this._setHTML( this._undoStack[ this._undoIndex ] ); var range = this._getRangeAndRemoveBookmark(); if ( range ) { this.setSelection( range ); } this.fireEvent( 'undoStateChange', { canUndo: true, canRedo: undoIndex + 2 < undoStackLength }); this.fireEvent( 'input' ); } return this; }; // --- Inline formatting --- // Looks for matching tag and attributes, so won't work // if instead of etc. proto.hasFormat = function ( tag, attributes, range ) { // 1. Normalise the arguments and get selection tag = tag.toUpperCase(); if ( !attributes ) { attributes = {}; } if ( !range && !( range = this.getSelection() ) ) { return false; } // If the common ancestor is inside the tag we require, we definitely // have the format. var root = range.commonAncestorContainer, walker, node; if ( getNearest( root, tag, attributes ) ) { return true; } // If common ancestor is a text node and doesn't have the format, we // definitely don't have it. if ( root.nodeType === TEXT_NODE ) { return false; } // Otherwise, check each text node at least partially contained within // the selection and make sure all of them have the format we want. walker = new TreeWalker( root, SHOW_TEXT, function ( node ) { return isNodeContainedInRange( range, node, true ); }, false ); var seenNode = false; while ( node = walker.nextNode() ) { if ( !getNearest( node, tag, attributes ) ) { return false; } seenNode = true; } return seenNode; }; proto._addFormat = function ( tag, attributes, range ) { // If the range is collapsed we simply insert the node by wrapping // it round the range and focus it. var el, walker, startContainer, endContainer, startOffset, endOffset, textNode, needsFormat; if ( range.collapsed ) { el = fixCursor( this.createElement( tag, attributes ) ); insertNodeInRange( range, el ); range.setStart( el.firstChild, el.firstChild.length ); range.collapse( true ); } // Otherwise we find all the textnodes in the range (splitting // partially selected nodes) and if they're not already formatted // correctly we wrap them in the appropriate tag. else { // We don't want to apply formatting twice so we check each text // node to see if it has an ancestor with the formatting already. // Create an iterator to walk over all the text nodes under this // ancestor which are in the range and not already formatted // correctly. walker = new TreeWalker( range.commonAncestorContainer, SHOW_TEXT, function ( node ) { return isNodeContainedInRange( range, node, true ); }, false ); // Start at the beginning node of the range and iterate through // all the nodes in the range that need formatting. startContainer = range.startContainer; startOffset = range.startOffset; endContainer = range.endContainer; endOffset = range.endOffset; // Make sure we start inside a text node. walker.currentNode = startContainer; if ( startContainer.nodeType !== TEXT_NODE ) { startContainer = walker.nextNode(); startOffset = 0; } do { textNode = walker.currentNode; needsFormat = !getNearest( textNode, tag, attributes ); if ( needsFormat ) { if ( textNode === endContainer && textNode.length > endOffset ) { textNode.splitText( endOffset ); } if ( textNode === startContainer && startOffset ) { textNode = textNode.splitText( startOffset ); if ( endContainer === startContainer ) { endContainer = textNode; endOffset -= startOffset; } startContainer = textNode; startOffset = 0; } el = this.createElement( tag, attributes ); replaceWith( textNode, el ); el.appendChild( textNode ); } } while ( walker.nextNode() ); // Make sure we finish inside a text node. Otherwise offset may have // changed. if ( endContainer.nodeType !== TEXT_NODE ) { endContainer = textNode; endOffset = textNode.length; } // Now set the selection to as it was before range = this._createRange( startContainer, startOffset, endContainer, endOffset ); } return range; }; proto._removeFormat = function ( tag, attributes, range, partial ) { // Add bookmark this._saveRangeToBookmark( range ); // We need a node in the selection to break the surrounding // formatted text. var doc = this._doc, fixer; if ( range.collapsed ) { if ( cantFocusEmptyTextNodes ) { fixer = doc.createTextNode( ZWS ); this._didAddZWS(); } else { fixer = doc.createTextNode( '' ); } insertNodeInRange( range, fixer ); } // Find block-level ancestor of selection var root = range.commonAncestorContainer; while ( isInline( root ) ) { root = root.parentNode; } // Find text nodes inside formatTags that are not in selection and // add an extra tag with the same formatting. var startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset, toWrap = [], examineNode = function ( node, exemplar ) { // If the node is completely contained by the range then // we're going to remove all formatting so ignore it. if ( isNodeContainedInRange( range, node, false ) ) { return; } var isText = ( node.nodeType === TEXT_NODE ), child, next; // If not at least partially contained, wrap entire contents // in a clone of the tag we're removing and we're done. if ( !isNodeContainedInRange( range, node, true ) ) { // Ignore bookmarks and empty text nodes if ( node.nodeName !== 'INPUT' && ( !isText || node.data ) ) { toWrap.push([ exemplar, node ]); } return; } // Split any partially selected text nodes. if ( isText ) { if ( node === endContainer && endOffset !== node.length ) { toWrap.push([ exemplar, node.splitText( endOffset ) ]); } if ( node === startContainer && startOffset ) { node.splitText( startOffset ); toWrap.push([ exemplar, node ]); } } // If not a text node, recurse onto all children. // Beware, the tree may be rewritten with each call // to examineNode, hence find the next sibling first. else { for ( child = node.firstChild; child; child = next ) { next = child.nextSibling; examineNode( child, exemplar ); } } }, formatTags = Array.prototype.filter.call( root.getElementsByTagName( tag ), function ( el ) { return isNodeContainedInRange( range, el, true ) && hasTagAttributes( el, tag, attributes ); } ); if ( !partial ) { formatTags.forEach( function ( node ) { examineNode( node, node ); }); } // Now wrap unselected nodes in the tag toWrap.forEach( function ( item ) { // [ exemplar, node ] tuple var el = item[0].cloneNode( false ), node = item[1]; replaceWith( node, el ); el.appendChild( node ); }); // and remove old formatting tags. formatTags.forEach( function ( el ) { replaceWith( el, empty( el ) ); }); // Merge adjacent inlines: this._getRangeAndRemoveBookmark( range ); if ( fixer ) { range.collapse( false ); } var _range = { startContainer: range.startContainer, startOffset: range.startOffset, endContainer: range.endContainer, endOffset: range.endOffset }; mergeInlines( root, _range ); range.setStart( _range.startContainer, _range.startOffset ); range.setEnd( _range.endContainer, _range.endOffset ); return range; }; proto.changeFormat = function ( add, remove, range, partial ) { // Normalise the arguments and get selection if ( !range && !( range = this.getSelection() ) ) { return; } // Save undo checkpoint this._recordUndoState( range ); this._getRangeAndRemoveBookmark( range ); if ( remove ) { range = this._removeFormat( remove.tag.toUpperCase(), remove.attributes || {}, range, partial ); } if ( add ) { range = this._addFormat( add.tag.toUpperCase(), add.attributes || {}, range ); } this.setSelection( range ); this._updatePath( range, true ); // We're not still in an undo state if ( !canObserveMutations ) { this._docWasChanged(); } return this; }; // --- Block formatting --- var tagAfterSplit = { DT: 'DD', DD: 'DT', LI: 'LI' }; var splitBlock = function ( self, block, node, offset ) { var splitTag = tagAfterSplit[ block.nodeName ], splitProperties = null, nodeAfterSplit = split( node, offset, block.parentNode ); if ( !splitTag ) { splitTag = self.defaultBlockTag; splitProperties = self.defaultBlockProperties; } // Make sure the new node is the correct type. if ( !hasTagAttributes( nodeAfterSplit, splitTag, splitProperties ) ) { block = createElement( nodeAfterSplit.ownerDocument, splitTag, splitProperties ); if ( nodeAfterSplit.dir ) { block.className = nodeAfterSplit.dir === 'rtl' ? 'dir-rtl' : ''; block.dir = nodeAfterSplit.dir; } replaceWith( nodeAfterSplit, block ); block.appendChild( empty( nodeAfterSplit ) ); nodeAfterSplit = block; } return nodeAfterSplit; }; proto.forEachBlock = function ( fn, mutates, range ) { if ( !range && !( range = this.getSelection() ) ) { return this; } // Save undo checkpoint if ( mutates ) { this._recordUndoState( range ); this._getRangeAndRemoveBookmark( range ); } var start = getStartBlockOfRange( range ), end = getEndBlockOfRange( range ); if ( start && end ) { do { if ( fn( start ) || start === end ) { break; } } while ( start = getNextBlock( start ) ); } if ( mutates ) { this.setSelection( range ); // Path may have changed this._updatePath( range, true ); // We're not still in an undo state if ( !canObserveMutations ) { this._docWasChanged(); } } return this; }; proto.modifyBlocks = function ( modify, range ) { if ( !range && !( range = this.getSelection() ) ) { return this; } // 1. Save undo checkpoint and bookmark selection if ( this._isInUndoState ) { this._saveRangeToBookmark( range ); } else { this._recordUndoState( range ); } // 2. Expand range to block boundaries expandRangeToBlockBoundaries( range ); // 3. Remove range. var body = this._body, frag; moveRangeBoundariesUpTree( range, body ); frag = extractContentsOfRange( range, body ); // 4. Modify tree of fragment and reinsert. insertNodeInRange( range, modify.call( this, frag ) ); // 5. Merge containers at edges if ( range.endOffset < range.endContainer.childNodes.length ) { mergeContainers( range.endContainer.childNodes[ range.endOffset ] ); } mergeContainers( range.startContainer.childNodes[ range.startOffset ] ); // 6. Restore selection this._getRangeAndRemoveBookmark( range ); this.setSelection( range ); this._updatePath( range, true ); // 7. We're not still in an undo state if ( !canObserveMutations ) { this._docWasChanged(); } return this; }; var increaseBlockQuoteLevel = function ( frag ) { return this.createElement( 'BLOCKQUOTE', [ frag ]); }; var decreaseBlockQuoteLevel = function ( frag ) { var blockquotes = frag.querySelectorAll( 'blockquote' ); Array.prototype.filter.call( blockquotes, function ( el ) { return !getNearest( el.parentNode, 'BLOCKQUOTE' ); }).forEach( function ( el ) { replaceWith( el, empty( el ) ); }); return frag; }; var removeBlockQuote = function (/* frag */) { return this.createDefaultBlock([ this.createElement( 'INPUT', { id: startSelectionId, type: 'hidden' }), this.createElement( 'INPUT', { id: endSelectionId, type: 'hidden' }) ]); }; var makeList = function ( self, frag, type ) { var walker = getBlockWalker( frag ), node, tag, prev, newLi; while ( node = walker.nextNode() ) { tag = node.parentNode.nodeName; if ( tag !== 'LI' ) { newLi = self.createElement( 'LI', { 'class': node.dir === 'rtl' ? 'dir-rtl' : undefined, dir: node.dir || undefined }); // Have we replaced the previous block with a new