diff --git a/serve.js b/serve.js deleted file mode 100644 index 73c914a..0000000 --- a/serve.js +++ /dev/null @@ -1,13 +0,0 @@ -var static = require('node-static'); -var sys = require('sys'); -var exec = require('child_process').exec; - -var file = new static.Server('./'); - -function puts(error, stdout, stderr) { sys.puts(stdout) }; - -require('http').createServer(function (request, response) { - request.addListener('end', function () { - file.serve(request, response); - }).resume(); -}).listen(8080); \ No newline at end of file diff --git a/source/Constants.js b/source/Constants.js deleted file mode 100644 index 7bb1f24..0000000 --- a/source/Constants.js +++ /dev/null @@ -1,46 +0,0 @@ -/*global doc, navigator */ -/*jshint strict:false */ - -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 FILTER_ACCEPT = 1; // NodeFilter.FILTER_ACCEPT; -var FILTER_SKIP = 3; // NodeFilter.FILTER_SKIP; - -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 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 isIE8or9or10 = /Trident\/[456]\./.test( ua ); -var isIE8 = ( win.ie === 8 ); -var isOpera = !!win.opera; -var isWebKit = /WebKit\//.test( ua ); - -var ctrlKey = isMac ? 'meta-' : 'ctrl-'; - -var useTextFixer = isIE8or9or10 || isOpera; -var cantFocusEmptyTextNodes = isIE8or9or10 || isWebKit; -var losesSelectionOnBlur = isIE8or9or10; -var hasBuggySplit = ( function () { - var div = doc.createElement( 'DIV' ), - text = doc.createTextNode( '12' ); - div.appendChild( text ); - text.splitText( 2 ); - return div.childNodes.length !== 2; -}() ); - -// 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; diff --git a/source/Editor.js b/source/Editor.js deleted file mode 100644 index c0a54d9..0000000 --- a/source/Editor.js +++ /dev/null @@ -1,2352 +0,0 @@ -/*global - DOCUMENT_POSITION_PRECEDING, - ELEMENT_NODE, - TEXT_NODE, - SHOW_ELEMENT, - SHOW_TEXT, - FILTER_ACCEPT, - FILTER_SKIP, - win, - isIOS, - isMac, - isGecko, - isIE8or9or10, - isIE8, - isOpera, - ctrlKey, - useTextFixer, - cantFocusEmptyTextNodes, - losesSelectionOnBlur, - hasBuggySplit, - notWS, - indexOf, - - TreeWalker, - - hasTagAttributes, - isLeaf, - isInline, - isBlock, - isContainer, - getBlockWalker, - getPreviousBlock, - getNextBlock, - getNearest, - getPath, - getLength, - detach, - replaceWith, - empty, - fixCursor, - split, - mergeInlines, - mergeWithBlock, - mergeContainers, - fixContainer, - createElement, - - forEachTextNodeInRange, - getTextContentInRange, - insertNodeInRange, - extractContentsOfRange, - deleteContentsOfRange, - insertTreeFragmentIntoRange, - isNodeContainedInRange, - moveRangeBoundariesDownTree, - moveRangeBoundariesUpTree, - getStartBlockOfRange, - getEndBlockOfRange, - rangeDoesStartAtBlockBoundary, - rangeDoesEndAtBlockBoundary, - expandRangeToBlockBoundaries, - - top, - console, - setTimeout -*/ -/*jshint strict:false */ - -function Squire ( doc ) { - var win = doc.defaultView; - var body = doc.body; - this._win = win; - this._doc = doc; - this._body = body; - - this._events = {}; - - 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.defaultBlockProperties = undefined; - - this.addEventListener( 'keyup', this._docWasChanged ); - - // 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( isIE8or9or10 ? 'beforecut' : 'cut', this._onCut ); - this.addEventListener( isIE8or9or10 ? 'beforepaste' : 'paste', this._onPaste ); - - if ( isIE8 ) { - this.addEventListener( 'keyup', this._ieSelAllClean ); - } - // Opera does not fire keydown repeatedly. - this.addEventListener( isOpera ? 'keypress' : 'keydown', this._onKey ); - - // Fix IE8/9'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. - if ( hasBuggySplit ) { - 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 ) {} -} - -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( 'DIV', 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.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, false ); - } - } - 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._win.getSelection(); - sel.removeAllRanges(); - sel.addRange( range ); - } - return this; -}; - -proto.getSelection = function () { - var sel = this._win.getSelection(), - selection, startContainer, endContainer; - if ( sel.rangeCount ) { - selection = sel.getRangeAt( 0 ).cloneRange(); - startContainer = selection.startContainer; - endContainer = selection.endContainer; - // FF sometimes throws an error reading the isLeaf property. Let's - // catch and log it to see if we can find what's going on. - try { - // FF can return the selection as being inside an . WTF? - if ( startContainer && isLeaf( startContainer ) ) { - selection.setStartBefore( startContainer ); - } - if ( endContainer && isLeaf( endContainer ) ) { - selection.setEndBefore( endContainer ); - } - } catch ( error ) { - this.didError({ - name: 'Squire#getSelection error', - message: 'Starts: ' + startContainer.nodeName + - '\nEnds: ' + endContainer.nodeName - }); - } - 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 - -proto._didAddZWS = function () { - this._hasZWS = true; -}; -proto._removeZWS = function () { - if ( !this._hasZWS ) { - return; - } - var walker = new TreeWalker( this._body, SHOW_TEXT, function () { - return FILTER_ACCEPT; - }, false ), - node, index; - while ( node = walker.nextNode() ) { - while ( ( index = node.data.indexOf( '\u200B' ) ) > -1 ) { - node.deleteData( index, 1 ); - } - } - 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 ( anchor !== focus ) { - 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). - if ( isGecko ) { - 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._docWasChanged = function ( event ) { - var code = event && 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 || ( !event.ctrlKey && !event.metaKey && !event.altKey && - ( code < 16 || code > 20 ) && - ( code < 33 || code > 45 ) ) ) { - 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 ) ? - FILTER_ACCEPT : FILTER_SKIP; - }, 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 ) ? - FILTER_ACCEPT : FILTER_SKIP; - }, - false - ); - - // Start at the beginning node of the range and iterate through - // all the nodes in the range that need formatting. - startOffset = 0; - endOffset = 0; - textnode = walker.currentNode = range.startContainer; - - if ( textnode.nodeType !== TEXT_NODE ) { - textnode = walker.nextNode(); - } - - do { - needsFormat = !getNearest( textnode, tag, attributes ); - if ( textnode === range.endContainer ) { - if ( needsFormat && textnode.length > range.endOffset ) { - textnode.splitText( range.endOffset ); - } else { - endOffset = range.endOffset; - } - } - if ( textnode === range.startContainer ) { - if ( needsFormat && range.startOffset ) { - textnode = textnode.splitText( range.startOffset ); - } else { - startOffset = range.startOffset; - } - } - if ( needsFormat ) { - el = this.createElement( tag, attributes ); - replaceWith( textnode, el ); - el.appendChild( textnode ); - endOffset = textnode.length; - } - endContainer = textnode; - if ( !startContainer ) { startContainer = endContainer; } - } while ( textnode = walker.nextNode() ); - - // 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( '\u200B' ); - 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 - this._docWasChanged(); - - return this; -}; - -// --- Block formatting --- - -var tagAfterSplit = { - DIV: 'DIV', - PRE: 'DIV', - H1: 'DIV', - H2: 'DIV', - H3: 'DIV', - H4: 'DIV', - H5: 'DIV', - H6: 'DIV', - P: 'DIV', - DT: 'DD', - DD: 'DT', - LI: 'LI' -}; - -var splitBlock = function ( block, node, offset ) { - var splitTag = tagAfterSplit[ block.nodeName ], - nodeAfterSplit = split( node, offset, block.parentNode ); - - // Make sure the new node is the correct type. - if ( nodeAfterSplit.nodeName !== splitTag ) { - block = createElement( nodeAfterSplit.ownerDocument, splitTag ); - 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 - 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 - 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