diff --git a/Makefile b/Makefile index 816c814..22034d1 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,11 @@ build/ie8.js: source/ie8types.js source/ie8dom.js source/ie8range.js mkdir -p $(@D) uglifyjs $^ -c -m -o $@ -build/squire.js: source/UA.js source/TreeWalker.js source/Node.js source/Range.js source/Editor.js +build/squire-raw.js: source/intro.js source/Constants.js source/TreeWalker.js source/Node.js source/Range.js source/Editor.js source/outro.js mkdir -p $(@D) + cat $^ >$@ + +build/squire.js: build/squire-raw.js uglifyjs $^ -c -m -o $@ build/document.html: source/document.html diff --git a/build/squire-raw.js b/build/squire-raw.js new file mode 100644 index 0000000..fe28ed3 --- /dev/null +++ b/build/squire-raw.js @@ -0,0 +1,3200 @@ +/* Copyright © 2011-2013 by Neil Jenkins. MIT Licensed. */ + +( function ( doc ) { + +"use strict"; +/*global doc, navigator */ + +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 body = doc.body; + +var ua = navigator.userAgent; +var isGecko = /Gecko\//.test( ua ); +var isIE = /Trident\//.test( ua ); +var isIE8 = ( win.ie === 8 ); +var isIOS = /iP(?:ad|hone|od)/.test( ua ); +var isOpera = !!win.opera; +var isWebKit = /WebKit\//.test( ua ); + +var useTextFixer = isIE || isOpera; +var cantFocusEmptyTextNodes = isIE || isWebKit; +var losesSelectionOnBlur = isIE; + +var notWS = /\S/; + +var indexOf = Array.prototype.indexOf; +/*global FILTER_ACCEPT */ +/*jshint strict:false */ + +/* + 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 ) === FILTER_ACCEPT ) { + 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 ) === FILTER_ACCEPT ) { + this.currentNode = node; + return node; + } + current = node; + } +}; +/*global + ELEMENT_NODE, + TEXT_NODE, + SHOW_ELEMENT, + FILTER_ACCEPT, + FILTER_SKIP, + doc, + isOpera, + useTextFixer, + cantFocusEmptyTextNodes, + + TreeWalker, + + Text, + + setPlaceholderTextNode +*/ +/*jshint strict:false */ + +var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TRONG|AMP)|U)$/; + +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 acceptIfBlock ( el ) { + return isBlock( el ) ? FILTER_ACCEPT : FILTER_SKIP; +} +function getBlockWalker ( node ) { + var doc = node.ownerDocument, + walker = new TreeWalker( + doc.body, SHOW_ELEMENT, acceptIfBlock, 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 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, + fixer, child; + + if ( node.nodeName === 'BODY' ) { + if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) { + fixer = doc.createElement( 'DIV' ); + if ( child ) { + node.replaceChild( fixer, child ); + } + else { + node.appendChild( fixer ); + } + node = fixer; + fixer = null; + } + } + + if ( isInline( node ) ) { + if ( !node.firstChild ) { + if ( cantFocusEmptyTextNodes ) { + fixer = doc.createTextNode( '\u200B' ); + setPlaceholderTextNode( fixer ); + } 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 = doc.createElement( 'BR' ); + while ( ( child = node.lastElementChild ) && !isInline( child ) ) { + node = child; + } + } + } + if ( fixer ) { + node.appendChild( fixer ); + } + + return node; +} + +function split ( node, offset, stopNode ) { + var nodeType = node.nodeType, + parent, clone, next; + if ( nodeType === TEXT_NODE ) { + if ( node === stopNode ) { + return offset; + } + 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; + } + + // 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 node; +} + +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.replace( /\u200B/g, '' ) ); + } + 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 ( isOpera && ( last = block.lastChild ) && last.nodeName === 'BR' ) { + block.removeChild( last ); + } +} + +function mergeContainers ( node ) { + var prev = node.previousSibling, + first = node.firstChild; + if ( prev && areAlike( prev, node ) && isContainer( prev ) ) { + detach( node ); + prev.appendChild( empty( node ) ); + if ( first ) { + mergeContainers( first ); + } + } +} + +function createElement ( tag, props, children ) { + var el = doc.createElement( tag ), + attr, i, l; + if ( props instanceof Array ) { + children = props; + props = null; + } + if ( props ) { + for ( attr in props ) { + el.setAttribute( attr, props[ attr ] ); + } + } + if ( children ) { + for ( i = 0, l = children.length; i < l; i += 1 ) { + el.appendChild( children[i] ); + } + } + return el; +} + +// 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 ( function () { + var div = doc.createElement( 'div' ), + text = doc.createTextNode( '12' ); + div.appendChild( text ); + text.splitText( 2 ); + return div.childNodes.length !== 2; +}() ) { + 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; + }; +} +/*global + ELEMENT_NODE, + TEXT_NODE, + SHOW_TEXT, + FILTER_ACCEPT, + START_TO_START, + START_TO_END, + END_TO_END, + END_TO_START, + indexOf, + + TreeWalker, + + isLeaf, + isInline, + isBlock, + getPreviousBlock, + getNextBlock, + getLength, + fixCursor, + split, + mergeWithBlock, + mergeContainers, + + Range +*/ +/*jshint strict:false */ + +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 RangePrototype = Range.prototype; + +RangePrototype.forEachTextNode = function ( fn ) { + var range = this.cloneRange(); + range.moveBoundariesDownTree(); + + var startContainer = range.startContainer, + endContainer = range.endContainer, + root = range.commonAncestorContainer, + walker = new TreeWalker( + root, SHOW_TEXT, function ( node ) { + return FILTER_ACCEPT; + }, false ), + textnode = walker.currentNode = startContainer; + + while ( !fn( textnode, range ) && + textnode !== endContainer && + ( textnode = walker.nextNode() ) ) {} +}; + +RangePrototype.getTextContent = function () { + var textContent = ''; + this.forEachTextNode( 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; +}; + +// --- + +RangePrototype._insertNode = function ( node ) { + // Insert at start. + var startContainer = this.startContainer, + startOffset = this.startOffset, + endContainer = this.endContainer, + endOffset = this.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 ( this.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; + } + + this.setStart( startContainer, startOffset ); + this.setEnd( endContainer, endOffset ); + + return this; +}; + +RangePrototype._extractContents = function ( common ) { + var startContainer = this.startContainer, + startOffset = this.startOffset, + endContainer = this.endContainer, + endOffset = this.endOffset; + + if ( !common ) { + common = this.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; + + // End node will be null if at end of child nodes list. + while ( startNode !== endNode ) { + next = startNode.nextSibling; + frag.appendChild( startNode ); + startNode = next; + } + + this.setStart( common, endNode ? + indexOf.call( common.childNodes, endNode ) : + common.childNodes.length ); + this.collapse( true ); + + fixCursor( common ); + + return frag; +}; + +RangePrototype._deleteContents = function () { + // Move boundaries up as much as possible to reduce need to split. + this.moveBoundariesUpTree(); + + // Remove selected range + this._extractContents(); + + // If we split into two different blocks, merge the blocks. + var startBlock = this.getStartBlock(), + endBlock = this.getEndBlock(); + if ( startBlock && endBlock && startBlock !== endBlock ) { + mergeWithBlock( startBlock, endBlock, this ); + } + + // Ensure block has necessary children + if ( startBlock ) { + fixCursor( startBlock ); + } + + // Ensure body has a block-level element in it. + var body = this.endContainer.ownerDocument.body, + child = body.firstChild; + if ( !child || child.nodeName === 'BR' ) { + fixCursor( body ); + this.selectNodeContents( body.firstChild ); + } + + // Ensure valid range (must have only block or inline containers) + var isCollapsed = this.collapsed; + this.moveBoundariesDownTree(); + if ( isCollapsed ) { + // Collapse + this.collapse( true ); + } + + return this; +}; + +// --- + +RangePrototype.insertTreeFragment = function ( 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 ( !this.collapsed ) { + this._deleteContents(); + } + + // Move range down into text ndoes + this.moveBoundariesDownTree(); + + // If inline, just insert at the current position. + if ( allInline ) { + this._insertNode( frag ); + this.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( this.startContainer, this.startOffset, + this.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 ); + } + + this.setStart( startContainer, startOffset ); + this.setEnd( endContainer, endOffset ); + this.moveBoundariesDownTree(); + } +}; + +// --- + +RangePrototype.containsNode = function ( node, partial ) { + var range = this, + 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 ); + } +}; + +RangePrototype.moveBoundariesDownTree = function () { + var startContainer = this.startContainer, + startOffset = this.startOffset, + endContainer = this.endContainer, + endOffset = this.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 ( this.collapsed ) { + this.setStart( endContainer, endOffset ); + this.setEnd( startContainer, startOffset ); + } else { + this.setStart( startContainer, startOffset ); + this.setEnd( endContainer, endOffset ); + } + + return this; +}; + +RangePrototype.moveBoundariesUpTree = function ( common ) { + var startContainer = this.startContainer, + startOffset = this.startOffset, + endContainer = this.endContainer, + endOffset = this.endOffset, + parent; + + if ( !common ) { + common = this.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; + } + + this.setStart( startContainer, startOffset ); + this.setEnd( endContainer, endOffset ); + + return this; +}; + +// Returns the first block at least partially contained by the range, +// or null if no block is contained by the range. +RangePrototype.getStartBlock = function () { + var container = this.startContainer, + block; + + // If inline, get the containing block. + if ( isInline( container ) ) { + block = getPreviousBlock( container ); + } else if ( isBlock( container ) ) { + block = container; + } else { + block = getNodeBefore( container, this.startOffset ); + block = getNextBlock( block ); + } + // Check the block actually intersects the range + return block && this.containsNode( 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. +RangePrototype.getEndBlock = function () { + var container = this.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, this.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 && this.containsNode( block, true ) ? block : null; +}; + +RangePrototype.startsAtBlockBoundary = function () { + var startContainer = this.startContainer, + startOffset = this.startOffset, + parent, child; + + while ( isInline( startContainer ) ) { + if ( startOffset ) { + return false; + } + parent = startContainer.parentNode; + startOffset = indexOf.call( parent.childNodes, startContainer ); + startContainer = parent; + } + // Skip empty text nodes and
s. + while ( startOffset && + ( child = startContainer.childNodes[ startOffset - 1 ] ) && + ( child.data === '' || child.nodeName === 'BR' ) ) { + startOffset -= 1; + } + return !startOffset; +}; + +RangePrototype.endsAtBlockBoundary = function () { + var endContainer = this.endContainer, + endOffset = this.endOffset, + length = getLength( endContainer ), + parent, child; + + while ( isInline( endContainer ) ) { + if ( endOffset !== length ) { + return false; + } + parent = endContainer.parentNode; + endOffset = indexOf.call( parent.childNodes, endContainer ) + 1; + endContainer = parent; + length = endContainer.childNodes.length; + } + // Skip empty text nodes and
s. + while ( endOffset < length && + ( child = endContainer.childNodes[ endOffset ] ) && + ( child.data === '' || child.nodeName === 'BR' ) ) { + endOffset += 1; + } + return endOffset === length; +}; + +RangePrototype.expandToBlockBoundaries = function () { + var start = this.getStartBlock(), + end = this.getEndBlock(), + parent; + + if ( start && end ) { + parent = start.parentNode; + this.setStart( parent, indexOf.call( parent.childNodes, start ) ); + parent = end.parentNode; + this.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 ); + } + + return this; +}; +/*global + DOCUMENT_POSITION_PRECEDING, + ELEMENT_NODE, + TEXT_NODE, + SHOW_ELEMENT, + SHOW_TEXT, + FILTER_ACCEPT, + FILTER_SKIP, + doc, + win, + body, + isGecko, + isIE, + isIE8, + isIOS, + isOpera, + useTextFixer, + cantFocusEmptyTextNodes, + losesSelectionOnBlur, + notWS, + indexOf, + + TreeWalker, + + hasTagAttributes, + isLeaf, + isInline, + isBlock, + isContainer, + getPreviousBlock, + getNextBlock, + getNearest, + getPath, + getLength, + detach, + replaceWith, + empty, + fixCursor, + split, + mergeInlines, + mergeWithBlock, + mergeContainers, + createElement, + + Range, + top, + console, + setTimeout +*/ +/*jshint strict:false */ + +var editor; + +// --- Events.js --- + +// 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 +}; + +var events = {}; + +var fireEvent = function ( type, event ) { + var handlers = events[ type ], + i, l, obj; + if ( handlers ) { + if ( !event ) { + event = {}; + } + if ( event.type !== type ) { + event.type = type; + } + for ( i = 0, l = handlers.length; i < l; i += 1 ) { + obj = handlers[i]; + try { + if ( obj.handleEvent ) { + obj.handleEvent( event ); + } else { + obj( event ); + } + } catch ( error ) { + editor.didError( error ); + } + } + } +}; + +var propagateEvent = function ( event ) { + fireEvent( event.type, event ); +}; + +var addEventListener = function ( type, fn ) { + var handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + if ( !customEvents[ type ] ) { + doc.addEventListener( type, propagateEvent, false ); + } + } + handlers.push( fn ); +}; + +var removeEventListener = function ( type, fn ) { + var handlers = events[ type ], + l; + if ( handlers ) { + l = handlers.length; + while ( l-- ) { + if ( handlers[l] === fn ) { + handlers.splice( l, 1 ); + } + } + if ( !handlers.length ) { + delete events[ type ]; + if ( !customEvents[ type ] ) { + doc.removeEventListener( type, propagateEvent, false ); + } + } + } +}; + +// --- Selection and Path --- + +var createRange = function ( range, startOffset, endContainer, endOffset ) { + if ( range instanceof Range ) { + return range.cloneRange(); + } + var domRange = doc.createRange(); + domRange.setStart( range, startOffset ); + if ( endContainer ) { + domRange.setEnd( endContainer, endOffset ); + } else { + domRange.setEnd( range, startOffset ); + } + return domRange; +}; + +var sel = win.getSelection(); +var lastSelection = null; + +var 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 ) { + win.focus(); + } + sel.removeAllRanges(); + sel.addRange( range ); + } +}; + +var getSelection = function () { + if ( sel.rangeCount ) { + lastSelection = sel.getRangeAt( 0 ).cloneRange(); + var startContainer = lastSelection.startContainer, + endContainer = lastSelection.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 ) ) { + lastSelection.setStartBefore( startContainer ); + } + if ( endContainer && isLeaf( endContainer ) ) { + lastSelection.setEndBefore( endContainer ); + } + } catch ( error ) { + editor.didError({ + name: 'Squire#getSelection error', + message: 'Starts: ' + startContainer.nodeName + + '\nEnds: ' + endContainer.nodeName + }); + } + } + return lastSelection; +}; + +// IE loses selection state of iframe on blur, so make sure we +// cache it just before it loses focus. +if ( losesSelectionOnBlur ) { + win.addEventListener( 'beforedeactivate', getSelection, true ); +} + +// --- Workaround for browsers that can't focus empty text nodes --- + +// WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256 + +var placeholderTextNode = null; +var mayRemovePlaceholder = true; +var willEnablePlaceholderRemoval = false; + +var enablePlaceholderRemoval = function () { + mayRemovePlaceholder = true; + willEnablePlaceholderRemoval = false; +}; + +var removePlaceholderTextNode = function () { + if ( !mayRemovePlaceholder ) { return; } + + var node = placeholderTextNode, + index; + + placeholderTextNode = null; + + if ( node.parentNode ) { + while ( ( index = node.data.indexOf( '\u200B' ) ) > -1 ) { + node.deleteData( index, 1 ); + } + if ( !node.data && !node.nextSibling && !node.previousSibling && + isInline( node.parentNode ) ) { + detach( node.parentNode ); + } + } +}; + +var setPlaceholderTextNode = function ( node ) { + if ( placeholderTextNode ) { + mayRemovePlaceholder = true; + removePlaceholderTextNode(); + } + if ( !willEnablePlaceholderRemoval ) { + setTimeout( enablePlaceholderRemoval, 0 ); + willEnablePlaceholderRemoval = true; + } + mayRemovePlaceholder = false; + placeholderTextNode = node; +}; + +// --- Path change events --- + +var lastAnchorNode; +var lastFocusNode; +var path = ''; + +var updatePath = function ( range, force ) { + if ( placeholderTextNode && !force ) { + removePlaceholderTextNode( range ); + } + var anchor = range.startContainer, + focus = range.endContainer, + newPath; + if ( force || anchor !== lastAnchorNode || focus !== lastFocusNode ) { + lastAnchorNode = anchor; + lastFocusNode = focus; + newPath = ( anchor && focus ) ? ( anchor === focus ) ? + getPath( focus ) : '(selection)' : ''; + if ( path !== newPath ) { + path = newPath; + fireEvent( 'pathChange', { path: newPath } ); + } + } + if ( anchor !== focus ) { + fireEvent( 'select' ); + } +}; +var updatePathOnEvent = function () { + updatePath( getSelection() ); +}; +addEventListener( 'keyup', updatePathOnEvent ); +addEventListener( 'mouseup', updatePathOnEvent ); + +// --- Focus --- + +var focus = function () { + // FF seems to need the body to be focussed + // (at least on first load). + if ( isGecko ) { + body.focus(); + } + win.focus(); +}; + +var 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 ) { + body.blur(); + } + top.focus(); +}; + +win.addEventListener( 'focus', propagateEvent, false ); +win.addEventListener( 'blur', propagateEvent, false ); + +// --- Get/Set data --- + +var getHTML = function () { + return body.innerHTML; +}; + +var setHTML = function ( html ) { + var node = body; + node.innerHTML = html; + do { + fixCursor( node ); + } while ( node = getNextBlock( node ) ); +}; + +var insertElement = function ( el, range ) { + if ( !range ) { range = getSelection(); } + range.collapse( true ); + range._insertNode( el ); + range.setStartAfter( el ); + setSelection( range ); + updatePath( range ); +}; + +// --- Bookmarking --- + +var startSelectionId = 'squire-selection-start'; +var endSelectionId = 'squire-selection-end'; + +var saveRangeToBookmark = function ( range ) { + var startNode = createElement( 'INPUT', { + id: startSelectionId, + type: 'hidden' + }), + endNode = createElement( 'INPUT', { + id: endSelectionId, + type: 'hidden' + }), + temp; + + range._insertNode( startNode ); + range.collapse( false ); + range._insertNode( 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 ); +}; + +var getRangeAndRemoveBookmark = function ( range ) { + var 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; + + range.moveBoundariesDownTree(); + if ( collapsed ) { + range.collapse( true ); + } + } + return range || null; +}; + +// --- Undo --- + +// These values are initialised in the editor.setHTML method, +// which is always called on initialisation. +var undoIndex; // = -1, +var undoStack; // = [], +var undoStackLength; // = 0, +var isInUndoState; // = false, +var docWasChanged = function () { + if ( isInUndoState ) { + isInUndoState = false; + fireEvent( 'undoStateChange', { + canUndo: true, + canRedo: false + }); + } + fireEvent( 'input' ); +}; + +addEventListener( 'keyup', 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 ) ) { + docWasChanged(); + } +}); + +// Leaves bookmark +var recordUndoState = function ( range ) { + // Don't record if we're already in an undo state + if ( !isInUndoState ) { + // Advance pointer to new position + undoIndex += 1; + + // Truncate stack if longer (i.e. if has been previously undone) + if ( undoIndex < undoStackLength) { + undoStack.length = undoStackLength = undoIndex; + } + + // Write out data + if ( range ) { + saveRangeToBookmark( range ); + } + undoStack[ undoIndex ] = getHTML(); + undoStackLength += 1; + isInUndoState = true; + } +}; + +var undo = function () { + // Sanity check: must not be at beginning of the history stack + if ( undoIndex !== 0 || !isInUndoState ) { + // Make sure any changes since last checkpoint are saved. + recordUndoState( getSelection() ); + + undoIndex -= 1; + setHTML( undoStack[ undoIndex ] ); + var range = getRangeAndRemoveBookmark(); + if ( range ) { + setSelection( range ); + } + isInUndoState = true; + fireEvent( 'undoStateChange', { + canUndo: undoIndex !== 0, + canRedo: true + }); + fireEvent( 'input' ); + } +}; + +var redo = function () { + // Sanity check: must not be at end of stack and must be in an undo + // state. + if ( undoIndex + 1 < undoStackLength && isInUndoState ) { + undoIndex += 1; + setHTML( undoStack[ undoIndex ] ); + var range = getRangeAndRemoveBookmark(); + if ( range ) { + setSelection( range ); + } + fireEvent( 'undoStateChange', { + canUndo: true, + canRedo: undoIndex + 1 < undoStackLength + }); + fireEvent( 'input' ); + } +}; + +// --- Inline formatting --- + +// Looks for matching tag and attributes, so won't work +// if instead of etc. +var hasFormat = function ( tag, attributes, range ) { + // 1. Normalise the arguments and get selection + tag = tag.toUpperCase(); + if ( !attributes ) { attributes = {}; } + if ( !range && !( range = 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 range.containsNode( 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; +}; + +var 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( createElement( tag, attributes ) ); + range._insertNode( 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 range.containsNode( 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 = 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 = createRange( + startContainer, startOffset, endContainer, endOffset ); + } + return range; +}; + +var removeFormat = function ( tag, attributes, range, partial ) { + // Add bookmark + saveRangeToBookmark( range ); + + // We need a node in the selection to break the surrounding + // formatted text. + var fixer; + if ( range.collapsed ) { + if ( cantFocusEmptyTextNodes ) { + fixer = doc.createTextNode( '\u200B' ); + setPlaceholderTextNode( fixer ); + } else { + fixer = doc.createTextNode( '' ); + } + range._insertNode( 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 ( range.containsNode( 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 ( !range.containsNode( 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 range.containsNode( 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: + 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; +}; + +var changeFormat = function ( add, remove, range, partial ) { + // Normalise the arguments and get selection + if ( !range && !( range = getSelection() ) ) { + return; + } + + // Save undo checkpoint + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + + if ( remove ) { + range = removeFormat( remove.tag.toUpperCase(), + remove.attributes || {}, range, partial ); + } + if ( add ) { + range = addFormat( add.tag.toUpperCase(), + add.attributes || {}, range ); + } + + setSelection( range ); + updatePath( range, true ); + + // We're not still in an undo state + docWasChanged(); +}; + +// --- 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( splitTag ); + block.className = nodeAfterSplit.dir === 'rtl' ? 'dir-rtl' : ''; + block.dir = nodeAfterSplit.dir; + replaceWith( nodeAfterSplit, block ); + block.appendChild( empty( nodeAfterSplit ) ); + nodeAfterSplit = block; + } + return nodeAfterSplit; +}; + +var forEachBlock = function ( fn, mutates, range ) { + if ( !range && !( range = getSelection() ) ) { + return; + } + + // Save undo checkpoint + if ( mutates ) { + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + } + + var start = range.getStartBlock(), + end = range.getEndBlock(); + if ( start && end ) { + do { + if ( fn( start ) || start === end ) { break; } + } while ( start = getNextBlock( start ) ); + } + + if ( mutates ) { + setSelection( range ); + + // Path may have changed + updatePath( range, true ); + + // We're not still in an undo state + docWasChanged(); + } +}; + +var modifyBlocks = function ( modify, range ) { + if ( !range && !( range = getSelection() ) ) { + return; + } + // 1. Stop firefox adding an extra
to + // if we remove everything. Don't want to do this in Opera + // as it can cause focus problems. + if ( !isOpera ) { + body.setAttribute( 'contenteditable', 'false' ); + } + + // 2. Save undo checkpoint and bookmark selection + if ( isInUndoState ) { + saveRangeToBookmark( range ); + } else { + recordUndoState( range ); + } + + // 3. Expand range to block boundaries + range.expandToBlockBoundaries(); + + // 4. Remove range. + range.moveBoundariesUpTree( body ); + var frag = range._extractContents( body ); + + // 5. Modify tree of fragment and reinsert. + range._insertNode( modify( frag ) ); + + // 6. Merge containers at edges + if ( range.endOffset < range.endContainer.childNodes.length ) { + mergeContainers( range.endContainer.childNodes[ range.endOffset ] ); + } + mergeContainers( range.startContainer.childNodes[ range.startOffset ] ); + + // 7. Make it editable again + if ( !isOpera ) { + body.setAttribute( 'contenteditable', 'true' ); + } + + // 8. Restore selection + getRangeAndRemoveBookmark( range ); + setSelection( range ); + updatePath( range, true ); + + // 9. We're not still in an undo state + docWasChanged(); +}; + +var increaseBlockQuoteLevel = function ( frag ) { + return 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 ) { + var blockquotes = frag.querySelectorAll( 'blockquote' ), + l = blockquotes.length, + bq; + while ( l-- ) { + bq = blockquotes[l]; + replaceWith( bq, empty( bq ) ); + } + return frag; +}; + +var makeList = function ( nodes, type ) { + var i, l, node, tag, prev, replacement; + for ( i = 0, l = nodes.length; i < l; i += 1 ) { + node = nodes[i]; + tag = node.nodeName; + if ( isBlock( node ) ) { + if ( tag !== 'LI' ) { + replacement = createElement( 'LI', { + 'class': node.dir === 'rtl' ? 'dir-rtl' : '', + dir: node.dir + }, [ + empty( node ) + ]); + if ( node.parentNode.nodeName === type ) { + replaceWith( node, replacement ); + } + else if ( ( prev = node.previousSibling ) && + prev.nodeName === type ) { + prev.appendChild( replacement ); + detach( node ); + i -= 1; + l -= 1; + } + else { + replaceWith( + node, + createElement( type, [ + replacement + ]) + ); + } + } + } else if ( isContainer( node ) ) { + if ( tag !== type && ( /^[DOU]L$/.test( tag ) ) ) { + replaceWith( node, createElement( type, [ empty( node ) ] ) ); + } else { + makeList( node.childNodes, type ); + } + } + } +}; + +var makeUnorderedList = function ( frag ) { + makeList( frag.childNodes, 'UL' ); + return frag; +}; + +var makeOrderedList = function ( frag ) { + makeList( frag.childNodes, 'OL' ); + return frag; +}; + +var decreaseListLevel = function ( frag ) { + var lists = frag.querySelectorAll( 'UL, OL' ); + Array.prototype.filter.call( lists, function ( el ) { + return !getNearest( el.parentNode, 'UL' ) && + !getNearest( el.parentNode, 'OL' ); + }).forEach( function ( el ) { + var frag = empty( el ), + children = frag.childNodes, + l = children.length, + child; + while ( l-- ) { + child = children[l]; + if ( child.nodeName === 'LI' ) { + frag.replaceChild( createElement( 'DIV', { + 'class': child.dir === 'rtl' ? 'dir-rtl' : '', + dir: child.dir + }, [ + empty( child ) + ]), child ); + } + } + replaceWith( el, frag ); + }); + return frag; +}; + +// --- Clean --- + +var linkRegExp = /\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])|(?:[\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,4}))/i; + +var addLinks = function ( frag ) { + var doc = frag.ownerDocument, + walker = new TreeWalker( frag, SHOW_TEXT, + function ( node ) { + return getNearest( node, 'A' ) ? FILTER_SKIP : FILTER_ACCEPT; + }, false ), + node, parts, i, l, text, parent, next; + while ( node = walker.nextNode() ) { + parts = node.data.split( linkRegExp ); + l = parts.length; + if ( l > 1 ) { + parent = node.parentNode; + next = node.nextSibling; + for ( i = 0; i < l; i += 1 ) { + text = parts[i]; + if ( i ) { + if ( i % 2 ) { + node = doc.createElement( 'A' ); + node.textContent = text; + node.href = /@/.test( text ) ? 'mailto:' + text : + /^(?:ht|f)tps?:/.test( text ) ? + text : 'http://' + text; + } else { + node = doc.createTextNode( text ); + } + if ( next ) { + parent.insertBefore( node, next ); + } else { + parent.appendChild( node ); + } + } else { + node.data = text; + } + } + walker.currentNode = node; + } + } +}; + +var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/; + +var fontSizes = { + 1: 10, + 2: 13, + 3: 16, + 4: 18, + 5: 24, + 6: 32, + 7: 48 +}; + +var spanToSemantic = { + backgroundColor: { + regexp: notWS, + replace: function ( colour ) { + return createElement( 'SPAN', { + 'class': 'highlight', + style: 'background-color: ' + colour + }); + } + }, + color: { + regexp: notWS, + replace: function ( colour ) { + return createElement( 'SPAN', { + 'class': 'colour', + style: 'color:' + colour + }); + } + }, + fontWeight: { + regexp: /^bold/i, + replace: function () { + return createElement( 'B' ); + } + }, + fontStyle: { + regexp: /^italic/i, + replace: function () { + return createElement( 'I' ); + } + }, + fontFamily: { + regexp: notWS, + replace: function ( family ) { + return createElement( 'SPAN', { + 'class': 'font', + style: 'font-family:' + family + }); + } + }, + fontSize: { + regexp: notWS, + replace: function ( size ) { + return createElement( 'SPAN', { + 'class': 'size', + style: 'font-size:' + size + }); + } + } +}; + +var stylesRewriters = { + SPAN: function ( span, parent ) { + var style = span.style, + attr, converter, css, newTreeBottom, newTreeTop, el; + + for ( attr in spanToSemantic ) { + converter = spanToSemantic[ attr ]; + css = style[ attr ]; + if ( css && converter.regexp.test( css ) ) { + el = converter.replace( css ); + if ( newTreeBottom ) { + newTreeBottom.appendChild( el ); + } + newTreeBottom = el; + if ( !newTreeTop ) { + newTreeTop = el; + } + } + } + + if ( newTreeTop ) { + newTreeBottom.appendChild( empty( span ) ); + parent.replaceChild( newTreeTop, span ); + } + + return newTreeBottom || span; + }, + STRONG: function ( node, parent ) { + var el = createElement( 'B' ); + parent.replaceChild( el, node ); + el.appendChild( empty( node ) ); + return el; + }, + EM: function ( node, parent ) { + var el = createElement( 'I' ); + parent.replaceChild( el, node ); + el.appendChild( empty( node ) ); + return el; + }, + FONT: function ( node, parent ) { + var face = node.face, + size = node.size, + fontSpan, sizeSpan, + newTreeBottom, newTreeTop; + if ( face ) { + fontSpan = createElement( 'SPAN', { + 'class': 'font', + style: 'font-family:' + face + }); + } + if ( size ) { + sizeSpan = createElement( 'SPAN', { + 'class': 'size', + style: 'font-size:' + fontSizes[ size ] + 'px' + }); + if ( fontSpan ) { + fontSpan.appendChild( sizeSpan ); + } + } + newTreeTop = fontSpan || sizeSpan || createElement( 'SPAN' ); + newTreeBottom = sizeSpan || fontSpan || newTreeTop; + parent.replaceChild( newTreeTop, node ); + newTreeBottom.appendChild( empty( node ) ); + return newTreeBottom; + }, + TT: function ( node, parent ) { + var el = createElement( 'SPAN', { + 'class': 'font', + style: 'font-family:menlo,consolas,"courier new",monospace' + }); + parent.replaceChild( el, node ); + el.appendChild( empty( node ) ); + return el; + } +}; + +var removeEmptyInlines = function ( root ) { + var children = root.childNodes, + l = children.length, + child; + while ( l-- ) { + child = children[l]; + if ( child.nodeType === ELEMENT_NODE ) { + removeEmptyInlines( child ); + if ( isInline( child ) && !child.firstChild ) { + root.removeChild( child ); + } + } + } +}; + +/* + Two purposes: + + 1. Remove nodes we don't want, such as weird tags, comment nodes + and whitespace nodes. + 2. Convert inline tags into our preferred format. +*/ +var cleanTree = function ( node, allowStyles ) { + var children = node.childNodes, + i, l, child, nodeName, nodeType, rewriter, childLength; + for ( i = 0, l = children.length; i < l; i += 1 ) { + child = children[i]; + nodeName = child.nodeName; + nodeType = child.nodeType; + rewriter = stylesRewriters[ nodeName ]; + if ( nodeType === ELEMENT_NODE ) { + childLength = child.childNodes.length; + if ( rewriter ) { + child = rewriter( child, node ); + } else if ( !allowedBlock.test( nodeName ) && + !isInline( child ) ) { + i -= 1; + l += childLength - 1; + node.replaceChild( empty( child ), child ); + continue; + } else if ( !allowStyles && child.style.cssText ) { + child.removeAttribute( 'style' ); + } + if ( childLength ) { + cleanTree( child, allowStyles ); + } + } else if ( nodeType !== TEXT_NODE || ( + !( notWS.test( child.data ) ) && + !( i > 0 && isInline( children[ i - 1 ] ) ) && + !( i + 1 < l && isInline( children[ i + 1 ] ) ) + ) ) { + node.removeChild( child ); + i -= 1; + l -= 1; + } + } + return node; +}; + +var wrapTopLevelInline = function ( root, tag ) { + var children = root.childNodes, + 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( tag ); } + wrapper.appendChild( child ); + i -= 1; + l -= 1; + } else if ( isBR || wrapper ) { + if ( !wrapper ) { wrapper = createElement( tag ); } + fixCursor( wrapper ); + if ( isBR ) { + root.replaceChild( wrapper, child ); + } else { + root.insertBefore( wrapper, child ); + i += 1; + l += 1; + } + wrapper = null; + } + } + if ( wrapper ) { + root.appendChild( fixCursor( wrapper ) ); + } + return root; +}; + +var notWSTextNode = function ( node ) { + return ( node.nodeType === ELEMENT_NODE ? + node.nodeName === 'BR' : + notWS.test( node.data ) ) ? + FILTER_ACCEPT : FILTER_SKIP; +}; +var isLineBreak = function ( br ) { + var block = br.parentNode, + walker; + while ( isInline( block ) ) { + block = block.parentNode; + } + walker = new TreeWalker( + block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode ); + walker.currentNode = br; + return !!walker.nextNode(); +}; + +//
elements are treated specially, and differently depending on the +// browser, when in rich text editor mode. When adding HTML from external +// sources, we must remove them, replacing the ones that actually affect +// line breaks with a split of the block element containing it (and wrapping +// any not inside a block). Browsers that want
elements at the end of +// each block will then have them added back in a later fixCursor method +// call. +var cleanupBRs = function ( root ) { + var brs = root.querySelectorAll( 'BR' ), + brBreaksLine = [], + l = brs.length, + i, br, block; + + // Must calculate whether the
breaks a line first, because if we + // have two
s next to each other, after the first one is converted + // to a block split, the second will be at the end of a block and + // therefore seem to not be a line break. But in its original context it + // was, so we should also convert it to a block split. + for ( i = 0; i < l; i += 1 ) { + brBreaksLine[i] = isLineBreak( brs[i] ); + } + while ( l-- ) { + br = brs[l]; + // Cleanup may have removed it + block = br.parentNode; + if ( !block ) { continue; } + while ( isInline( block ) ) { + block = block.parentNode; + } + // If this is not inside a block, replace it by wrapping + // inlines in DIV. + if ( !isBlock( block ) || !tagAfterSplit[ block.nodeName ] ) { + wrapTopLevelInline( block, 'DIV' ); + } + // If in a block we can split, split it instead, but only if there + // is actual text content in the block. Otherwise, the
is a + // placeholder to stop the block from collapsing, so we must leave + // it. + else { + if ( brBreaksLine[l] ) { + splitBlock( block, br.parentNode, br ); + } + detach( br ); + } + } +}; + +// --- Cut and Paste --- + +var afterCut = function () { + try { + // If all content removed, ensure div at start of body. + fixCursor( body ); + } catch ( error ) { + editor.didError( error ); + } +}; + +addEventListener( isIE ? 'beforecut' : 'cut', function () { + // Save undo checkpoint + var range = getSelection(); + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + setSelection( range ); + setTimeout( afterCut, 0 ); +}); + +// IE sometimes fires the beforepaste event twice; make sure it is not run +// again before our after paste function is called. +var awaitingPaste = false; + +addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { + if ( awaitingPaste ) { return; } + + // Treat image paste as a drop of an image file. + var clipboardData = event.clipboardData, + items = clipboardData && clipboardData.items, + fireDrop = false, + l; + if ( items ) { + l = items.length; + while ( l-- ) { + if ( /^image\/.*/.test( items[l].type ) ) { + event.preventDefault(); + fireEvent( 'dragover', { + dataTransfer: clipboardData, + /*jshint loopfunc: true */ + preventDefault: function () { + fireDrop = true; + } + /*jshint loopfunc: false */ + }); + if ( fireDrop ) { + fireEvent( 'drop', { + dataTransfer: clipboardData + }); + } + return; + } + } + } + + awaitingPaste = true; + + var range = getSelection(), + startContainer = range.startContainer, + startOffset = range.startOffset, + endContainer = range.endContainer, + endOffset = range.endOffset; + + var pasteArea = createElement( 'DIV', { + style: 'position: absolute; overflow: hidden; top:' + + (body.scrollTop + 30) + 'px; left: 0; width: 1px; height: 1px;' + }); + body.appendChild( pasteArea ); + range.selectNodeContents( pasteArea ); + setSelection( range ); + + // A setTimeout of 0 means this is added to the back of the + // single javascript thread, so it will be executed after the + // paste event. + setTimeout( function () { + try { + // Get the pasted content and clean + var frag = empty( detach( pasteArea ) ), + first = frag.firstChild, + range = createRange( + startContainer, startOffset, endContainer, endOffset ); + + // Was anything actually pasted? + if ( first ) { + // Safari and IE like putting extra divs around things. + if ( first === frag.lastChild && + first.nodeName === 'DIV' ) { + frag.replaceChild( empty( first ), first ); + } + + frag.normalize(); + addLinks( frag ); + cleanTree( frag, false ); + cleanupBRs( frag ); + removeEmptyInlines( frag ); + + var node = frag, + doPaste = true; + while ( node = getNextBlock( node ) ) { + fixCursor( node ); + } + + fireEvent( 'willPaste', { + fragment: frag, + preventDefault: function () { + doPaste = false; + } + }); + + // Insert pasted data + if ( doPaste ) { + range.insertTreeFragment( frag ); + docWasChanged(); + + range.collapse( false ); + } + } + + setSelection( range ); + updatePath( range, true ); + + awaitingPaste = false; + } catch ( error ) { + editor.didError( error ); + } + }, 0 ); +}); + +// --- Keyboard interaction --- + +var keys = { + 8: 'backspace', + 9: 'tab', + 13: 'enter', + 32: 'space', + 46: 'delete' +}; + +var mapKeyTo = function ( fn ) { + return function ( event ) { + event.preventDefault(); + fn(); + }; +}; + +var mapKeyToFormat = function ( tag ) { + return function ( event ) { + event.preventDefault(); + var range = getSelection(); + if ( hasFormat( tag, null, range ) ) { + changeFormat( null, { tag: tag }, range ); + } else { + changeFormat( { tag: tag }, null, range ); + } + }; +}; + +// If you delete the content inside a span with a font styling, Webkit will +// replace it with a tag (!). If you delete all the text inside a +// link in Opera, it won't delete the link. Let's make things consistent. If +// you delete all text inside an inline tag, remove the inline tag. +var afterDelete = function () { + try { + var range = getSelection(), + node = range.startContainer, + parent; + if ( node.nodeType === TEXT_NODE ) { + node = node.parentNode; + } + // If focussed in empty inline element + if ( isInline( node ) && !node.textContent ) { + do { + parent = node.parentNode; + } while ( isInline( parent ) && + !parent.textContent && ( node = parent ) ); + range.setStart( parent, + indexOf.call( parent.childNodes, node ) ); + range.collapse( true ); + parent.removeChild( node ); + if ( !isBlock( parent ) ) { + parent = getPreviousBlock( parent ); + } + fixCursor( parent ); + range.moveBoundariesDownTree(); + setSelection( range ); + updatePath( range ); + } + } catch ( error ) { + editor.didError( error ); + } +}; + +// If you select all in IE8 then type, it makes a P; replace it with +// a DIV. +if ( isIE8 ) { + addEventListener( 'keyup', function () { + var firstChild = body.firstChild; + if ( firstChild.nodeName === 'P' ) { + saveRangeToBookmark( getSelection() ); + replaceWith( firstChild, createElement( 'DIV', [ + empty( firstChild ) + ]) ); + setSelection( getRangeAndRemoveBookmark() ); + } + }); +} + +var keyHandlers = { + enter: function ( event ) { + // We handle this ourselves + event.preventDefault(); + + // Must have some form of selection + var range = getSelection(); + if ( !range ) { return; } + + // Save undo checkpoint and add any links in the preceding section. + recordUndoState( range ); + addLinks( range.startContainer ); + getRangeAndRemoveBookmark( range ); + + // Selected text is overwritten, therefore delete the contents + // to collapse selection. + if ( !range.collapsed ) { + range._deleteContents(); + } + + var block = range.getStartBlock(), + tag = block ? block.nodeName : 'DIV', + splitTag = tagAfterSplit[ tag ], + nodeAfterSplit; + + // If this is a malformed bit of document, just play it safe + // and insert a
. + if ( !block ) { + range._insertNode( createElement( 'BR' ) ); + range.collapse( false ); + setSelection( range ); + updatePath( range, true ); + docWasChanged(); + return; + } + + // We need to wrap the contents in divs. + var splitNode = range.startContainer, + splitOffset = range.startOffset, + replacement; + if ( !splitTag ) { + // If the selection point is inside the block, we're going to + // rewrite it so our saved referece points won't be valid. + // Pick a node at a deeper point in the tree to avoid this. + if ( splitNode === block ) { + splitNode = splitOffset ? + splitNode.childNodes[ splitOffset - 1 ] : null; + splitOffset = 0; + if ( splitNode ) { + if ( splitNode.nodeName === 'BR' ) { + splitNode = splitNode.nextSibling; + } else { + splitOffset = getLength( splitNode ); + } + if ( !splitNode || splitNode.nodeName === 'BR' ) { + replacement = fixCursor( createElement( 'DIV' ) ); + if ( splitNode ) { + block.replaceChild( replacement, splitNode ); + } else { + block.appendChild( replacement ); + } + splitNode = replacement; + } + } + } + wrapTopLevelInline( block, 'DIV' ); + splitTag = 'DIV'; + if ( !splitNode ) { + splitNode = block.firstChild; + } + range.setStart( splitNode, splitOffset ); + range.setEnd( splitNode, splitOffset ); + block = range.getStartBlock(); + } + + if ( !block.textContent ) { + // Break list + if ( getNearest( block, 'UL' ) || getNearest( block, 'OL' ) ) { + return modifyBlocks( decreaseListLevel, range ); + } + // Break blockquote + else if ( getNearest( block, 'BLOCKQUOTE' ) ) { + return modifyBlocks( removeBlockQuote, range ); + } + } + + // Otherwise, split at cursor point. + nodeAfterSplit = splitBlock( block, splitNode, splitOffset ); + + // Focus cursor + // If there's a / etc. at the beginning of the split + // make sure we focus inside it. + while ( nodeAfterSplit.nodeType === ELEMENT_NODE ) { + var child = nodeAfterSplit.firstChild, + next; + + // Don't continue links over a block break; unlikely to be the + // desired outcome. + if ( nodeAfterSplit.nodeName === 'A' ) { + replaceWith( nodeAfterSplit, empty( nodeAfterSplit ) ); + nodeAfterSplit = child; + continue; + } + + while ( child && child.nodeType === TEXT_NODE && !child.data ) { + next = child.nextSibling; + if ( !next || next.nodeName === 'BR' ) { + break; + } + detach( child ); + child = next; + } + + // 'BR's essentially don't count; they're a browser hack. + // If you try to select the contents of a 'BR', FF will not let + // you type anything! + if ( !child || child.nodeName === 'BR' || + ( child.nodeType === TEXT_NODE && !isOpera ) ) { + break; + } + nodeAfterSplit = child; + } + range = createRange( nodeAfterSplit, 0 ); + setSelection( range ); + updatePath( range, true ); + + // Scroll into view + if ( nodeAfterSplit.nodeType === TEXT_NODE ) { + nodeAfterSplit = nodeAfterSplit.parentNode; + } + if ( nodeAfterSplit.offsetTop + nodeAfterSplit.offsetHeight > + ( doc.documentElement.scrollTop || body.scrollTop ) + + body.offsetHeight ) { + nodeAfterSplit.scrollIntoView( false ); + } + + // We're not still in an undo state + docWasChanged(); + }, + backspace: function ( event ) { + var range = getSelection(); + // If not collapsed, delete contents + if ( !range.collapsed ) { + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + event.preventDefault(); + range._deleteContents(); + setSelection( range ); + updatePath( range, true ); + } + // If at beginning of block, merge with previous + else if ( range.startsAtBlockBoundary() ) { + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + event.preventDefault(); + var current = range.getStartBlock(), + previous = current && getPreviousBlock( current ); + // Must not be at the very beginning of the text area. + if ( previous ) { + // If not editable, just delete whole block. + if ( !previous.isContentEditable ) { + detach( previous ); + return; + } + // Otherwise merge. + mergeWithBlock( previous, current, range ); + // If deleted line between containers, merge newly adjacent + // containers. + current = previous.parentNode; + while ( current && !current.nextSibling ) { + current = current.parentNode; + } + if ( current && ( current = current.nextSibling ) ) { + mergeContainers( current ); + } + setSelection( range ); + } + // If at very beginning of text area, allow backspace + // to break lists/blockquote. + else { + // Break list + if ( getNearest( current, 'UL' ) || + getNearest( current, 'OL' ) ) { + return modifyBlocks( decreaseListLevel, range ); + } + // Break blockquote + else if ( getNearest( current, 'BLOCKQUOTE' ) ) { + return modifyBlocks( decreaseBlockQuoteLevel, range ); + } + setSelection( range ); + updatePath( range, true ); + } + } + // Otherwise, leave to browser but check afterwards whether it has + // left behind an empty inline tag. + else { + var text = range.startContainer.data || ''; + if ( !notWS.test( text.charAt( range.startOffset - 1 ) ) ) { + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + setSelection( range ); + } + setTimeout( afterDelete, 0 ); + } + }, + 'delete': function ( event ) { + var range = getSelection(); + // If not collapsed, delete contents + if ( !range.collapsed ) { + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + event.preventDefault(); + range._deleteContents(); + setSelection( range ); + updatePath( range, true ); + } + // If at end of block, merge next into this block + else if ( range.endsAtBlockBoundary() ) { + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + event.preventDefault(); + var current = range.getStartBlock(), + next = current && getNextBlock( current ); + // Must not be at the very end of the text area. + if ( next ) { + // If not editable, just delete whole block. + if ( !next.isContentEditable ) { + detach( next ); + return; + } + // Otherwise merge. + mergeWithBlock( current, next, range ); + // If deleted line between containers, merge newly adjacent + // containers. + next = current.parentNode; + while ( next && !next.nextSibling ) { + next = next.parentNode; + } + if ( next && ( next = next.nextSibling ) ) { + mergeContainers( next ); + } + setSelection( range ); + updatePath( range, true ); + } + } + // Otherwise, leave to browser but check afterwards whether it has + // left behind an empty inline tag. + else { + // Record undo point if deleting whitespace + var text = range.startContainer.data || ''; + if ( !notWS.test( text.charAt( range.startOffset ) ) ) { + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + setSelection( range ); + } + setTimeout( afterDelete, 0 ); + } + }, + space: function () { + var range = getSelection(); + recordUndoState( range ); + addLinks( range.startContainer ); + getRangeAndRemoveBookmark( range ); + setSelection( range ); + }, + 'ctrl-b': mapKeyToFormat( 'B' ), + 'ctrl-i': mapKeyToFormat( 'I' ), + 'ctrl-u': mapKeyToFormat( 'U' ), + 'ctrl-y': mapKeyTo( redo ), + 'ctrl-z': mapKeyTo( undo ), + 'ctrl-shift-z': mapKeyTo( redo ) +}; + +// Ref: http://unixpapa.com/js/key.html +// Opera does not fire keydown repeatedly. +addEventListener( isOpera ? 'keypress' : 'keydown', + function ( event ) { + var code = event.keyCode, + key = keys[ code ] || String.fromCharCode( code ).toLowerCase(), + modifiers = ''; + + // On keypress, delete and '.' both have event.keyCode 46 + // Must check event.which to differentiate. + if ( isOpera && event.which === 46 ) { + key = '.'; + } + + // Function keys + if ( 111 < code && code < 124 ) { + key = 'f' + ( code - 111 ); + } + + if ( event.altKey ) { modifiers += 'alt-'; } + if ( event.ctrlKey || event.metaKey ) { modifiers += 'ctrl-'; } + if ( event.shiftKey ) { modifiers += 'shift-'; } + + key = modifiers + key; + + if ( keyHandlers[ key ] ) { + keyHandlers[ key ]( event ); + } +}); + +// --- Export --- + +var chain = function ( fn ) { + return function () { + fn.apply( null, arguments ); + return this; + }; +}; + +var command = function ( fn, arg, arg2 ) { + return function () { + fn( arg, arg2 ); + focus(); + return this; + }; +}; + +editor = win.editor = { + + didError: function ( error ) { + console.log( error ); + }, + + addEventListener: chain( addEventListener ), + removeEventListener: chain( removeEventListener ), + + focus: chain( focus ), + blur: chain( blur ), + + getDocument: function () { + return doc; + }, + + addStyles: function ( styles ) { + if ( styles ) { + var head = doc.documentElement.firstChild, + style = createElement( 'STYLE', { + type: 'text/css' + }); + if ( style.styleSheet ) { + // IE8: must append to document BEFORE adding styles + // or you get the IE7 CSS parser! + head.appendChild( style ); + style.styleSheet.cssText = styles; + } else { + // Everyone else + style.appendChild( doc.createTextNode( styles ) ); + head.appendChild( style ); + } + } + return this; + }, + + getHTML: function ( withBookMark ) { + var brs = [], + node, fixer, html, l, range; + if ( withBookMark && ( range = getSelection() ) ) { + saveRangeToBookmark( range ); + } + if ( useTextFixer ) { + node = body; + while ( node = getNextBlock( node ) ) { + if ( !node.textContent && !node.querySelector( 'BR' ) ) { + fixer = createElement( 'BR' ); + node.appendChild( fixer ); + brs.push( fixer ); + } + } + } + html = getHTML(); + if ( useTextFixer ) { + l = brs.length; + while ( l-- ) { + detach( brs[l] ); + } + } + if ( range ) { + getRangeAndRemoveBookmark( range ); + } + return html; + }, + setHTML: function ( html ) { + var frag = doc.createDocumentFragment(), + div = createElement( 'DIV' ), + child; + + // Parse HTML into DOM tree + div.innerHTML = html; + frag.appendChild( empty( div ) ); + + cleanTree( frag, true ); + cleanupBRs( frag ); + + wrapTopLevelInline( frag, 'DIV' ); + + // Fix cursor + var node = frag; + while ( node = getNextBlock( node ) ) { + fixCursor( node ); + } + + // Remove existing body children + while ( child = body.lastChild ) { + body.removeChild( child ); + } + + // And insert new content + body.appendChild( frag ); + fixCursor( body ); + + // Reset the undo stack + undoIndex = -1; + undoStack = []; + undoStackLength = 0; + isInUndoState = false; + + // Record undo state + var range = getRangeAndRemoveBookmark() || + createRange( body.firstChild, 0 ); + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + // IE will also set focus when selecting text so don't use + // setSelection. Instead, just store it in lastSelection, so if + // anything calls getSelection before first focus, we have a range + // to return. + if ( losesSelectionOnBlur ) { + lastSelection = range; + } else { + setSelection( range ); + } + updatePath( range, true ); + + return this; + }, + + getSelectedText: function () { + return getSelection().getTextContent(); + }, + + insertElement: chain( insertElement ), + insertImage: function ( src ) { + var img = createElement( 'IMG', { + src: src + }); + insertElement( img ); + return img; + }, + + getPath: function () { + return path; + }, + getSelection: getSelection, + setSelection: chain( setSelection ), + + undo: chain( undo ), + redo: chain( redo ), + + hasFormat: hasFormat, + changeFormat: chain( changeFormat ), + + bold: command( changeFormat, { tag: 'B' } ), + italic: command( changeFormat, { tag: 'I' } ), + underline: command( changeFormat, { tag: 'U' } ), + + removeBold: command( changeFormat, null, { tag: 'B' } ), + removeItalic: command( changeFormat, null, { tag: 'I' } ), + removeUnderline: command( changeFormat, null, { tag: 'U' } ), + + makeLink: function ( url ) { + url = encodeURI( url ); + var range = getSelection(); + if ( range.collapsed ) { + var protocolEnd = url.indexOf( ':' ) + 1; + if ( protocolEnd ) { + while ( url[ protocolEnd ] === '/' ) { protocolEnd += 1; } + } + range._insertNode( + doc.createTextNode( url.slice( protocolEnd ) ) + ); + } + changeFormat({ + tag: 'A', + attributes: { + href: url + } + }, { + tag: 'A' + }, range ); + focus(); + return this; + }, + + removeLink: function () { + changeFormat( null, { + tag: 'A' + }, getSelection(), true ); + focus(); + return this; + }, + + setFontFace: function ( name ) { + changeFormat({ + tag: 'SPAN', + attributes: { + 'class': 'font', + style: 'font-family: ' + name + ', sans-serif;' + } + }, { + tag: 'SPAN', + attributes: { 'class': 'font' } + }); + focus(); + return this; + }, + setFontSize: function ( size ) { + changeFormat({ + tag: 'SPAN', + attributes: { + 'class': 'size', + style: 'font-size: ' + + ( typeof size === 'number' ? size + 'px' : size ) + } + }, { + tag: 'SPAN', + attributes: { 'class': 'size' } + }); + focus(); + return this; + }, + + setTextColour: function ( colour ) { + changeFormat({ + tag: 'SPAN', + attributes: { + 'class': 'colour', + style: 'color: ' + colour + } + }, { + tag: 'SPAN', + attributes: { 'class': 'colour' } + }); + focus(); + return this; + }, + + setHighlightColour: function ( colour ) { + changeFormat({ + tag: 'SPAN', + attributes: { + 'class': 'highlight', + style: 'background-color: ' + colour + } + }, { + tag: 'SPAN', + attributes: { 'class': 'highlight' } + }); + focus(); + return this; + }, + + setTextAlignment: function ( alignment ) { + forEachBlock( function ( block ) { + block.className = ( block.className + .split( /\s+/ ) + .filter( function ( klass ) { + return !( /align/.test( klass ) ); + }) + .join( ' ' ) + + ' align-' + alignment ).trim(); + block.style.textAlign = alignment; + }, true ); + focus(); + return this; + }, + + setTextDirection: function ( direction ) { + forEachBlock( function ( block ) { + block.className = ( block.className + .split( /\s+/ ) + .filter( function ( klass ) { + return !( /dir/.test( klass ) ); + }) + .join( ' ' ) + + ' dir-' + direction ).trim(); + block.dir = direction; + }, true ); + focus(); + return this; + }, + + forEachBlock: chain( forEachBlock ), + modifyBlocks: chain( modifyBlocks ), + + increaseQuoteLevel: command( modifyBlocks, increaseBlockQuoteLevel ), + decreaseQuoteLevel: command( modifyBlocks, decreaseBlockQuoteLevel ), + + makeUnorderedList: command( modifyBlocks, makeUnorderedList ), + makeOrderedList: command( modifyBlocks, makeOrderedList ), + removeList: command( modifyBlocks, decreaseListLevel ) +}; + +// --- Initialise --- + +body.setAttribute( 'contenteditable', 'true' ); +editor.setHTML( '' ); + +if ( win.onEditorLoad ) { + win.onEditorLoad( win.editor ); + win.onEditorLoad = null; +} +}( document ) ); diff --git a/build/squire.js b/build/squire.js index 1352800..70eace1 100644 --- a/build/squire.js +++ b/build/squire.js @@ -1 +1 @@ -var UA=function(e){"use strict";var t=navigator.userAgent,n=!!e.opera,r=/Trident\//.test(t),i=/WebKit\//.test(t);return{isOpera:n,isIE8:8===e.ie,isIE:r,isGecko:/Gecko\//.test(t),isWebKit:i,isIOS:/iP(?:ad|hone|od)/.test(t),useTextFixer:r||n,cantFocusEmptyTextNodes:r||i,losesSelectionOnBlur:r}}(window),DOMTreeWalker=function(){"use strict";var e={1:1,2:2,3:4,8:128,9:256,11:1024},t=1,n=function(e,t,n){this.root=this.currentNode=e,this.nodeType=t,this.filter=n};return n.prototype.nextNode=function(){for(var n,r=this.currentNode,i=this.root,o=this.nodeType,s=this.filter;;){for(n=r.firstChild;!n&&r&&r!==i;)n=r.nextSibling,n||(r=r.parentNode);if(!n)return null;if(e[n.nodeType]&o&&s(n)===t)return this.currentNode=n,n;r=n}},n.prototype.previousNode=function(){for(var n,r=this.currentNode,i=this.root,o=this.nodeType,s=this.filter;;){if(r===i)return null;if(n=r.previousSibling)for(;r=n.lastChild;)n=r;else n=r.parentNode;if(!n)return null;if(e[n.nodeType]&o&&s(n)===t)return this.currentNode=n,n;r=n}},n}();(function(e,t){"use strict";var n=function(e,t){for(var n,r,i=e.length;i--;){n=e[i].prototype;for(r in t)n[r]=t[r]}},r=function(e,t){for(var n=e.length;n--;)if(!t(e[n]))return!1;return!0},i=function(){return!1},o=function(){return!0},s=/^(?:A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TRONG|AMP)|U)$/,a={BR:1,IMG:1,INPUT:1},l=function(e,t){var n=t.parentNode;return n&&n.replaceChild(e,t),e},d=1,c=3,f=1,u=1,h=3,p=function(e){return e.isBlock()?u:h};n(window.Node?[Node]:[Text,Element,HTMLDocument],{isLeaf:i,isInline:i,isBlock:i,isContainer:i,getPath:function(){var e=this.parentNode;return e?e.getPath():""},detach:function(){var e=this.parentNode;return e&&e.removeChild(this),this},replaceWith:function(e){return l(e,this),this},replaces:function(e){return l(this,e),this},nearest:function(e,t){var n=this.parentNode;return n?n.nearest(e,t):null},getPreviousBlock:function(){var e=this.ownerDocument,n=new t(e.body,f,p,!1);return n.currentNode=this,n.previousNode()},getNextBlock:function(){var e=this.ownerDocument,n=new t(e.body,f,p,!1);return n.currentNode=this,n.nextNode()},split:function(e){return e},mergeContainers:function(){}}),n([Text],{isInline:o,getLength:function(){return this.length},isLike:function(e){return e.nodeType===c},split:function(e,t){var n=this;return n===t?e:n.parentNode.split(n.splitText(e),t)}}),n([Element],{isLeaf:function(){return!!a[this.nodeName]},isInline:function(){return s.test(this.nodeName)},isBlock:function(){return!this.isInline()&&r(this.childNodes,function(e){return e.isInline()})},isContainer:function(){return!this.isInline()&&!this.isBlock()},getLength:function(){return this.childNodes.length},getPath:function(){var e,t,n,r,i=this.parentNode;return i?(e=i.getPath(),e+=(e?">":"")+this.nodeName,(t=this.id)&&(e+="#"+t),(n=this.className.trim())&&(r=n.split(/\s\s*/),r.sort(),e+=".",e+=r.join(".")),e):""},wraps:function(e){return l(this,e).appendChild(e),this},empty:function(){for(var e=this.ownerDocument.createDocumentFragment(),t=this.childNodes.length;t--;)e.appendChild(this.firstChild);return e},is:function(e,t){if(this.nodeName!==e)return!1;var n;for(n in t)if(this.getAttribute(n)!==t[n])return!1;return!0},nearest:function(e,t){var n=this;do if(n.is(e,t))return n;while((n=n.parentNode)&&n.nodeType===d);return null},isLike:function(e){return e.nodeType===d&&e.nodeName===this.nodeName&&e.className===this.className&&e.style.cssText===this.style.cssText},mergeInlines:function(e){for(var t,n,r,i=this.childNodes,o=i.length,s=[];o--;)if(t=i[o],n=o&&i[o-1],o&&t.isInline()&&t.isLike(n)&&!a[t.nodeName])e.startContainer===t&&(e.startContainer=n,e.startOffset+=n.getLength()),e.endContainer===t&&(e.endContainer=n,e.endOffset+=n.getLength()),e.startContainer===this&&(e.startOffset>o?e.startOffset-=1:e.startOffset===o&&(e.startContainer=n,e.startOffset=n.getLength())),e.endContainer===this&&(e.endOffset>o?e.endOffset-=1:e.endOffset===o&&(e.endContainer=n,e.endOffset=n.getLength())),t.detach(),t.nodeType===c?n.appendData(t.data.replace(/\u200B/g,"")):s.push(t.empty());else if(t.nodeType===d){for(r=s.length;r--;)t.appendChild(s.pop());t.mergeInlines(e)}},mergeWithBlock:function(e,t){for(var n,r,i,o=this,s=e;1===s.parentNode.childNodes.length;)s=s.parentNode;s.detach(),r=o.childNodes.length,n=o.lastChild,n&&"BR"===n.nodeName&&(o.removeChild(n),r-=1),i={startContainer:o,startOffset:r,endContainer:o,endOffset:r},o.appendChild(e.empty()),o.mergeInlines(i),t.setStart(i.startContainer,i.startOffset),t.collapse(!0),window.opera&&(n=o.lastChild)&&"BR"===n.nodeName&&o.removeChild(n)},mergeContainers:function(){var e=this.previousSibling,t=this.firstChild;e&&e.isLike(this)&&e.isContainer()&&(e.appendChild(this.detach().empty()),t&&t.mergeContainers())},split:function(e,t){var n=this;if("number"==typeof e&&(e=n.childNodes.length>e?n.childNodes[e]:null),n===t)return e;for(var r,i=n.parentNode,o=n.cloneNode(!1);e;)r=e.nextSibling,o.appendChild(e),e=r;return n.fixCursor(),o.fixCursor(),(r=n.nextSibling)?i.insertBefore(o,r):i.appendChild(o),i.split(o,t)},fixCursor:function(){var t,n,r=this,i=r.ownerDocument;if("BODY"===r.nodeName&&((n=r.firstChild)&&"BR"!==n.nodeName||(t=i.createElement("DIV"),n?r.replaceChild(t,n):r.appendChild(t),r=t,t=null)),r.isInline())r.firstChild||(e.cantFocusEmptyTextNodes?(t=i.createTextNode("​"),editor._setPlaceholderTextNode(t)):t=i.createTextNode(""));else if(e.useTextFixer){for(;r.nodeType!==c&&!r.isLeaf();){if(n=r.firstChild,!n){t=i.createTextNode("");break}r=n}r.nodeType===c?/^ +$/.test(r.data)&&(r.data=""):r.isLeaf()&&r.parentNode.insertBefore(i.createTextNode(""),r)}else if(!r.querySelector("BR"))for(t=i.createElement("BR");(n=r.lastElementChild)&&!n.isInline();)r=n;return t&&r.appendChild(t),this}}),function(){var e=document.createElement("div"),t=document.createTextNode("12");return e.appendChild(t),t.splitText(2),2!==e.childNodes.length}()&&(Text.prototype.splitText=function(e){var t=this.ownerDocument.createTextNode(this.data.slice(e)),n=this.nextSibling,r=this.parentNode,i=this.length-e;return n?r.insertBefore(t,n):r.appendChild(t),i&&this.deleteData(e,i),t})})(UA,DOMTreeWalker),function(e){"use strict";var t,n=Array.prototype.indexOf,r=1,i=3,o=4,s=1,a=0,l=1,d=2,c=3,f=function(e,t){for(var n=e.childNodes;t&&e.nodeType===r;)e=n[t-1],n=e.childNodes,t=n.length;return e},u=function(e,t){if(e.nodeType===r){var n=e.childNodes;if(n.length>t)e=n[t];else{for(;e&&!e.nextSibling;)e=e.parentNode;e&&(e=e.nextSibling)}}return e},h={forEachTextNode:function(t){var n=this.cloneRange();n.moveBoundariesDownTree();for(var r=n.startContainer,i=n.endContainer,a=n.commonAncestorContainer,l=new e(a,o,function(){return s},!1),d=l.currentNode=r;!t(d,n)&&d!==i&&(d=l.nextNode()););},getTextContent:function(){var e="";return this.forEachTextNode(function(t,n){var r=t.data;r&&/\S/.test(r)&&(t===n.endContainer&&(r=r.slice(0,n.endOffset)),t===n.startContainer&&(r=r.slice(n.startOffset)),e+=r)}),e},_insertNode:function(e){var t,r,o,s,a=this.startContainer,l=this.startOffset,d=this.endContainer,c=this.endOffset;return a.nodeType===i?(t=a.parentNode,r=t.childNodes,l===a.length?(l=n.call(r,a)+1,this.collapsed&&(d=t,c=l)):(l&&(s=a.splitText(l),d===a?(c-=l,d=s):d===t&&(c+=1),a=s),l=n.call(r,a)),a=t):r=a.childNodes,o=r.length,l===o?a.appendChild(e):a.insertBefore(e,r[l]),a===d&&(c+=r.length-o),this.setStart(a,l),this.setEnd(d,c),this},_extractContents:function(e){var t=this.startContainer,r=this.startOffset,o=this.endContainer,s=this.endOffset;e||(e=this.commonAncestorContainer),e.nodeType===i&&(e=e.parentNode);for(var a,l=o.split(s,e),d=t.split(r,e),c=e.ownerDocument.createDocumentFragment();d!==l;)a=d.nextSibling,c.appendChild(d),d=a;return this.setStart(e,l?n.call(e.childNodes,l):e.childNodes.length),this.collapse(!0),e.fixCursor(),c},_deleteContents:function(){this.moveBoundariesUpTree(),this._extractContents();var e=this.getStartBlock(),t=this.getEndBlock();e&&t&&e!==t&&e.mergeWithBlock(t,this),e&&e.fixCursor();var n=this.endContainer.ownerDocument.body,r=n.firstChild;r&&"BR"!==r.nodeName||(n.fixCursor(),this.selectNodeContents(n.firstChild));var i=this.collapsed;return this.moveBoundariesDownTree(),i&&this.collapse(!0),this},insertTreeFragment:function(e){for(var t=!0,n=e.childNodes,i=n.length;i--;)if(!n[i].isInline()){t=!1;break}if(this.collapsed||this._deleteContents(),this.moveBoundariesDownTree(),t)this._insertNode(e),this.collapse(!1);else{for(var o,s,a=this.startContainer.split(this.startOffset,this.startContainer.ownerDocument.body),l=a.previousSibling,d=l,c=d.childNodes.length,f=a,u=0,h=a.parentNode;(o=d.lastChild)&&o.nodeType===r&&"BR"!==o.nodeName;)d=o,c=d.childNodes.length;for(;(o=f.firstChild)&&o.nodeType===r&&"BR"!==o.nodeName;)f=o;for(;(o=e.firstChild)&&o.isInline();)d.appendChild(o);for(;(o=e.lastChild)&&o.isInline();)f.insertBefore(o,f.firstChild),u+=1;for(s=e;s=s.getNextBlock();)s.fixCursor();h.insertBefore(e,a),s=a.previousSibling,a.textContent?a.mergeContainers():h.removeChild(a),a.parentNode||(f=s,u=f.getLength()),l.textContent?l.mergeContainers():(d=l.nextSibling,c=0,h.removeChild(l)),this.setStart(d,c),this.setEnd(f,u),this.moveBoundariesDownTree()}},containsNode:function(e,t){var n=this,r=e.ownerDocument.createRange();if(r.selectNode(e),t){var i=n.compareBoundaryPoints(c,r)>-1,o=1>n.compareBoundaryPoints(l,r);return!i&&!o}var s=1>n.compareBoundaryPoints(a,r),f=n.compareBoundaryPoints(d,r)>-1;return s&&f},moveBoundariesDownTree:function(){for(var e,t=this.startContainer,n=this.startOffset,r=this.endContainer,o=this.endOffset;t.nodeType!==i&&(e=t.childNodes[n],e&&!e.isLeaf());)t=e,n=0;if(o)for(;r.nodeType!==i&&(e=r.childNodes[o-1],e&&!e.isLeaf());)r=e,o=r.getLength();else for(;r.nodeType!==i&&(e=r.firstChild,e&&!e.isLeaf());)r=e;return this.collapsed?(this.setStart(r,o),this.setEnd(t,n)):(this.setStart(t,n),this.setEnd(r,o)),this},moveBoundariesUpTree:function(e){var t,r=this.startContainer,i=this.startOffset,o=this.endContainer,s=this.endOffset;for(e||(e=this.commonAncestorContainer);r!==e&&!i;)t=r.parentNode,i=n.call(t.childNodes,r),r=t;for(;o!==e&&s===o.getLength();)t=o.parentNode,s=n.call(t.childNodes,o)+1,o=t;return this.setStart(r,i),this.setEnd(o,s),this},getStartBlock:function(){var e,t=this.startContainer;return t.isInline()?e=t.getPreviousBlock():t.isBlock()?e=t:(e=f(t,this.startOffset),e=e.getNextBlock()),e&&this.containsNode(e,!0)?e:null},getEndBlock:function(){var e,t,n=this.endContainer;if(n.isInline())e=n.getPreviousBlock();else if(n.isBlock())e=n;else{if(e=u(n,this.endOffset),!e)for(e=n.ownerDocument.body;t=e.lastChild;)e=t;e=e.getPreviousBlock()}return e&&this.containsNode(e,!0)?e:null},startsAtBlockBoundary:function(){for(var e,t,r=this.startContainer,i=this.startOffset;r.isInline();){if(i)return!1;e=r.parentNode,i=n.call(e.childNodes,r),r=e}for(;i&&(t=r.childNodes[i-1])&&(""===t.data||"BR"===t.nodeName);)i-=1;return!i},endsAtBlockBoundary:function(){for(var e,t,r=this.endContainer,i=this.endOffset,o=r.getLength();r.isInline();){if(i!==o)return!1;e=r.parentNode,i=n.call(e.childNodes,r)+1,r=e,o=r.childNodes.length}for(;o>i&&(t=r.childNodes[i])&&(""===t.data||"BR"===t.nodeName);)i+=1;return i===o},expandToBlockBoundaries:function(){var e,t=this.getStartBlock(),r=this.getEndBlock();return t&&r&&(e=t.parentNode,this.setStart(e,n.call(e.childNodes,t)),e=r.parentNode,this.setEnd(e,n.call(e.childNodes,r)+1)),this}};for(t in h)Range.prototype[t]=h[t]}(DOMTreeWalker),function(e,t,n){"use strict";var r,i=2,o=1,s=3,a=1,l=4,d=1,c=3,f=e.defaultView,u=e.body,h=t.isOpera,p=t.isGecko,N=t.isIOS,g=t.isIE,C=t.isIE8,m=t.cantFocusEmptyTextNodes,v=t.losesSelectionOnBlur,y=t.useTextFixer,T=/\S/,B=function(t,n,r){var i,o,s,a=e.createElement(t);if(n instanceof Array&&(r=n,n=null),n)for(i in n)a.setAttribute(i,n[i]);if(r)for(o=0,s=r.length;s>o;o+=1)a.appendChild(r[o]);return a},x={focus:1,blur:1,pathChange:1,select:1,input:1,undoStateChange:1},O={},S=function(e,t){var n,i,o,s=O[e];if(s)for(t||(t={}),t.type!==e&&(t.type=e),n=0,i=s.length;i>n;n+=1){o=s[n];try{o.handleEvent?o.handleEvent(t):o(t)}catch(a){r.didError(a)}}},I=function(e){S(e.type,e)},E=function(t,n){var r=O[t];r||(r=O[t]=[],x[t]||e.addEventListener(t,I,!1)),r.push(n)},D=function(t,n){var r,i=O[t];if(i){for(r=i.length;r--;)i[r]===n&&i.splice(r,1);i.length||(delete O[t],x[t]||e.removeEventListener(t,I,!1))}},k=function(t,n,r,i){if(t instanceof Range)return t.cloneRange();var o=e.createRange();return o.setStart(t,n),r?o.setEnd(r,i):o.setEnd(t,n),o},b=f.getSelection(),L=null,A=function(e){e&&(N&&f.focus(),b.removeAllRanges(),b.addRange(e))},w=function(){if(b.rangeCount){L=b.getRangeAt(0).cloneRange();var e=L.startContainer,t=L.endContainer;e&&e.isLeaf()&&L.setStartBefore(e),t&&t.isLeaf()&&L.setEndBefore(t)}return L};v&&f.addEventListener("beforedeactivate",w,!0);var P,R,U=null,V=!0,F=!1,H=function(){V=!0,F=!1},W=function(e){U&&(V=!0,_()),F||(setTimeout(H,0),F=!0),V=!1,U=e},_=function(){if(V){var e,t=U;if(U=null,t.parentNode){for(;(e=t.data.indexOf("​"))>-1;)t.deleteData(e,1);t.data||t.nextSibling||t.previousSibling||!t.parentNode.isInline()||t.parentNode.detach()}}},M="",z=function(e,t){U&&!t&&_(e);var n,r=e.startContainer,i=e.endContainer;(t||r!==P||i!==R)&&(P=r,R=i,n=r&&i?r===i?i.getPath():"(selection)":"",M!==n&&(M=n,S("pathChange",{path:n}))),r!==i&&S("select")},K=function(){z(w())};E("keyup",K),E("mouseup",K);var q=function(){p&&u.focus(),f.focus()},G=function(){p&&u.blur(),top.focus()};f.addEventListener("focus",I,!1),f.addEventListener("blur",I,!1);var Q,Y,$,j,Z=function(){return u.innerHTML},J=function(e){var t=u;t.innerHTML=e;do t.fixCursor();while(t=t.getNextBlock())},X=function(e,t){t||(t=w()),t.collapse(!0),t._insertNode(e),t.setStartAfter(e),A(t),z(t)},et=Array.prototype.indexOf,tt="squire-selection-start",nt="squire-selection-end",rt=function(e){var t,n=B("INPUT",{id:tt,type:"hidden"}),r=B("INPUT",{id:nt,type:"hidden"});e._insertNode(n),e.collapse(!1),e._insertNode(r),n.compareDocumentPosition(r)&i&&(n.id=nt,r.id=tt,t=n,n=r,r=t),e.setStartAfter(n),e.setEndBefore(r)},it=function(t){var n=e.getElementById(tt),r=e.getElementById(nt);if(n&&r){var i,o=n.parentNode,s=r.parentNode,a={startContainer:o,endContainer:s,startOffset:et.call(o.childNodes,n),endOffset:et.call(s.childNodes,r)};o===s&&(a.endOffset-=1),n.detach(),r.detach(),o.mergeInlines(a),o!==s&&s.mergeInlines(a),t||(t=e.createRange()),t.setStart(a.startContainer,a.startOffset),t.setEnd(a.endContainer,a.endOffset),i=t.collapsed,t.moveBoundariesDownTree(),i&&t.collapse(!0)}return t||null},ot=function(){j&&(j=!1,S("undoStateChange",{canUndo:!0,canRedo:!1})),S("input")};E("keyup",function(e){var t=e.keyCode;e.ctrlKey||e.metaKey||e.altKey||!(16>t||t>20)||!(33>t||t>45)||ot()});var st=function(e){j||(Q+=1,$>Q&&(Y.length=$=Q),e&&rt(e),Y[Q]=Z(),$+=1,j=!0)},at=function(){if(0!==Q||!j){st(w()),Q-=1,J(Y[Q]);var e=it();e&&A(e),j=!0,S("undoStateChange",{canUndo:0!==Q,canRedo:!0}),S("input")}},lt=function(){if($>Q+1&&j){Q+=1,J(Y[Q]);var e=it();e&&A(e),S("undoStateChange",{canUndo:!0,canRedo:$>Q+1}),S("input")}},dt=function(e,t,r){if(e=e.toUpperCase(),t||(t={}),!r&&!(r=w()))return!1;var i,o,a=r.commonAncestorContainer;if(a.nearest(e,t))return!0;if(a.nodeType===s)return!1;i=new n(a,l,function(e){return r.containsNode(e,!0)?d:c},!1);for(var f=!1;o=i.nextNode();){if(!o.nearest(e,t))return!1;f=!0}return f},ct=function(e,t,r){if(r.collapsed){var i=B(e,t).fixCursor();r._insertNode(i),r.setStart(i.firstChild,i.firstChild.length),r.collapse(!0)}else{var o,a,f,u=new n(r.commonAncestorContainer,l,function(e){return r.containsNode(e,!0)?d:c},!1),h=0,p=0,N=u.currentNode=r.startContainer;N.nodeType!==s&&(N=u.nextNode());do f=!N.nearest(e,t),N===r.endContainer&&(f&&N.length>r.endOffset?N.splitText(r.endOffset):p=r.endOffset),N===r.startContainer&&(f&&r.startOffset?N=N.splitText(r.startOffset):h=r.startOffset),f&&(B(e,t).wraps(N),p=N.length),a=N,o||(o=a);while(N=u.nextNode());r=k(o,h,a,p)}return r},ft=function(t,n,r,i){rt(r);var o;r.collapsed&&(m?(o=e.createTextNode("​"),W(o)):o=e.createTextNode(""),r._insertNode(o));for(var a=r.commonAncestorContainer;a.isInline();)a=a.parentNode;var l=r.startContainer,d=r.startOffset,c=r.endContainer,f=r.endOffset,u=[],h=function(e,t){if(!r.containsNode(e,!1)){var n,i,o=e.nodeType===s;if(!r.containsNode(e,!0))return"INPUT"===e.nodeName||o&&!e.data||u.push([t,e]),void 0;if(o)e===c&&f!==e.length&&u.push([t,e.splitText(f)]),e===l&&d&&(e.splitText(d),u.push([t,e]));else for(n=e.firstChild;n;n=i)i=n.nextSibling,h(n,t)}},p=Array.prototype.filter.call(a.getElementsByTagName(t),function(e){return r.containsNode(e,!0)&&e.is(t,n)});i||p.forEach(function(e){h(e,e)}),u.forEach(function(e){e[0].cloneNode(!1).wraps(e[1])}),p.forEach(function(e){e.replaceWith(e.empty())}),it(r),o&&r.collapse(!1);var N={startContainer:r.startContainer,startOffset:r.startOffset,endContainer:r.endContainer,endOffset:r.endOffset};return a.mergeInlines(N),r.setStart(N.startContainer,N.startOffset),r.setEnd(N.endContainer,N.endOffset),r},ut=function(e,t,n,r){(n||(n=w()))&&(st(n),it(n),t&&(n=ft(t.tag.toUpperCase(),t.attributes||{},n,r)),e&&(n=ct(e.tag.toUpperCase(),e.attributes||{},n)),A(n),z(n,!0),ot())},ht={DIV:"DIV",PRE:"DIV",H1:"DIV",H2:"DIV",H3:"DIV",H4:"DIV",H5:"DIV",H6:"DIV",P:"DIV",DT:"DD",DD:"DT",LI:"LI"},pt=function(e,t,n){var r=ht[e.nodeName],i=t.split(n,e.parentNode);return i.nodeName!==r&&(e=B(r),e.className="rtl"===i.dir?"dir-rtl":"",e.dir=i.dir,e.replaces(i).appendChild(i.empty()),i=e),i},Nt=function(e,t,n){if(n||(n=w())){t&&(st(n),it(n));var r=n.getStartBlock(),i=n.getEndBlock();if(r&&i)do if(e(r)||r===i)break;while(r=r.getNextBlock());t&&(A(n),z(n,!0),ot())}},gt=function(e,t){if(t||(t=w())){h||u.setAttribute("contenteditable","false"),j?rt(t):st(t),t.expandToBlockBoundaries(),t.moveBoundariesUpTree(u);var n=t._extractContents(u);t._insertNode(e(n)),t.endOffsetn;n+=1)i=e[n],o=i.nodeName,i.isBlock()?"LI"!==o&&(a=B("LI",{"class":"rtl"===i.dir?"dir-rtl":"",dir:i.dir},[i.empty()]),i.parentNode.nodeName===t?i.replaceWith(a):(s=i.previousSibling)&&s.nodeName===t?(s.appendChild(a),i.detach(),n-=1,r-=1):i.replaceWith(B(t,[a]))):i.isContainer()&&(o!==t&&/^[DOU]L$/.test(o)?i.replaceWith(B(t,[i.empty()])):yt(i.childNodes,t))},Tt=function(e){return yt(e.childNodes,"UL"),e},Bt=function(e){return yt(e.childNodes,"OL"),e},xt=function(e){var t=e.querySelectorAll("UL, OL");return Array.prototype.filter.call(t,function(e){return!e.parentNode.nearest("UL")&&!e.parentNode.nearest("OL")}).forEach(function(e){for(var t,n=e.empty(),r=n.childNodes,i=r.length;i--;)t=r[i],"LI"===t.nodeName&&n.replaceChild(B("DIV",{"class":"rtl"===t.dir?"dir-rtl":"",dir:t.dir},[t.empty()]),t);e.replaceWith(n)}),e},Ot=/\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])|(?:[\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,4}))/i,St=function(e){for(var t,r,i,o,s,a,f,u=e.ownerDocument,h=new n(e,l,function(e){return e.nearest("A")?c:d},!1);t=h.nextNode();)if(r=t.data.split(Ot),o=r.length,o>1){for(a=t.parentNode,f=t.nextSibling,i=0;o>i;i+=1)s=r[i],i?(i%2?(t=u.createElement("A"),t.textContent=s,t.href=/@/.test(s)?"mailto:"+s:/^(?:ht|f)tps?:/.test(s)?s:"http://"+s):t=u.createTextNode(s),f?a.insertBefore(t,f):a.appendChild(t)):t.data=s;h.currentNode=t}},It=/^(?:A(?:DDRESS|RTICLE|SIDE)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/,Et={1:10,2:13,3:16,4:18,5:24,6:32,7:48},Dt={backgroundColor:{regexp:T,replace:function(e){return B("SPAN",{"class":"highlight",style:"background-color: "+e})}},color:{regexp:T,replace:function(e){return B("SPAN",{"class":"colour",style:"color:"+e})}},fontWeight:{regexp:/^bold/i,replace:function(){return B("B")}},fontStyle:{regexp:/^italic/i,replace:function(){return B("I")}},fontFamily:{regexp:T,replace:function(e){return B("SPAN",{"class":"font",style:"font-family:"+e})}},fontSize:{regexp:T,replace:function(e){return B("SPAN",{"class":"size",style:"font-size:"+e})}}},kt={SPAN:function(e,t){var n,r,i,o,s,a,l=e.style;for(n in Dt)r=Dt[n],i=l[n],i&&r.regexp.test(i)&&(a=r.replace(i),o&&o.appendChild(a),o=a,s||(s=a));return s&&(o.appendChild(e.empty()),t.replaceChild(s,e)),o||e},STRONG:function(e,t){var n=B("B");return t.replaceChild(n,e),n.appendChild(e.empty()),n},EM:function(e,t){var n=B("I");return t.replaceChild(n,e),n.appendChild(e.empty()),n},FONT:function(e,t){var n,r,i,o,s=e.face,a=e.size;return s&&(n=B("SPAN",{"class":"font",style:"font-family:"+s})),a&&(r=B("SPAN",{"class":"size",style:"font-size:"+Et[a]+"px"}),n&&n.appendChild(r)),o=n||r||B("SPAN"),i=r||n||o,t.replaceChild(o,e),i.appendChild(e.empty()),i},TT:function(e,t){var n=B("SPAN",{"class":"font",style:'font-family:menlo,consolas,"courier new",monospace'});return t.replaceChild(n,e),n.appendChild(e.empty()),n}},bt=function(e){for(var t,n=e.childNodes,r=n.length;r--;)t=n[r],t.nodeType===o&&(bt(t),t.isInline()&&!t.firstChild&&e.removeChild(t))},Lt=function(e,t){var n,r,i,a,l,d,c,f=e.childNodes;for(n=0,r=f.length;r>n;n+=1)if(i=f[n],a=i.nodeName,l=i.nodeType,d=kt[a],l===o){if(c=i.childNodes.length,d)i=d(i,e);else{if(!It.test(a)&&!i.isInline()){n-=1,r+=c-1,e.replaceChild(i.empty(),i);continue}!t&&i.style.cssText&&i.removeAttribute("style")}c&&Lt(i,t)}else l===s&&(T.test(i.data)||n>0&&f[n-1].isInline()||r>n+1&&f[n+1].isInline())||(e.removeChild(i),n-=1,r-=1);return e},At=function(e,t){var n,r,i,o,s=e.childNodes,a=null;for(n=0,r=s.length;r>n;n+=1)i=s[n],o="BR"===i.nodeName,!o&&i.isInline()?(a||(a=B(t)),a.appendChild(i),n-=1,r-=1):(o||a)&&(a||(a=B(t)),a.fixCursor(),o?e.replaceChild(a,i):(e.insertBefore(a,i),n+=1,r+=1),a=null);return a&&e.appendChild(a.fixCursor()),e},wt=function(e){return(e.nodeType===o?"BR"===e.nodeName:T.test(e.data))?d:c},Pt=function(e){for(var t,r=e.parentNode;r.isInline();)r=r.parentNode;return t=new n(r,a|l,wt),t.currentNode=e,!!t.nextNode()},Rt=function(e){var t,n,r,i=e.querySelectorAll("BR"),o=[],s=i.length;for(t=0;s>t;t+=1)o[t]=Pt(i[t]);for(;s--;)if(n=i[s],r=n.parentNode){for(;r.isInline();)r=r.parentNode;r.isBlock()&&ht[r.nodeName]?(o[s]&&pt(r,n.parentNode,n),n.detach()):At(r,"DIV")}},Ut=function(){try{u.fixCursor()}catch(e){r.didError(e)}};E(g?"beforecut":"cut",function(){var e=w();st(e),it(e),A(e),setTimeout(Ut,0)});var Vt=!1;E(g?"beforepaste":"paste",function(e){if(!Vt){var t,n=e.clipboardData,i=n&&n.items,o=!1;if(i)for(t=i.length;t--;)if(/^image\/.*/.test(i[t].type))return e.preventDefault(),S("dragover",{dataTransfer:n,preventDefault:function(){o=!0}}),o&&S("drop",{dataTransfer:n}),void 0;Vt=!0;var s=w(),a=s.startContainer,l=s.startOffset,d=s.endContainer,c=s.endOffset,f=B("DIV",{style:"position: absolute; overflow: hidden; top:"+(u.scrollTop+30)+"px; left: 0; width: 1px; height: 1px;"});u.appendChild(f),s.selectNodeContents(f),A(s),setTimeout(function(){try{var e=f.detach().empty(),t=e.firstChild,n=k(a,l,d,c);if(t){t===e.lastChild&&"DIV"===t.nodeName&&e.replaceChild(t.empty(),t),e.normalize(),St(e),Lt(e,!1),Rt(e),bt(e);for(var i=e,o=!0;i=i.getNextBlock();)i.fixCursor();S("willPaste",{fragment:e,preventDefault:function(){o=!1}}),o&&(n.insertTreeFragment(e),ot(),n.collapse(!1))}A(n),z(n,!0),Vt=!1}catch(s){r.didError(s)}},0)}});var Ft={8:"backspace",9:"tab",13:"enter",32:"space",46:"delete"},Ht=function(e){return function(t){t.preventDefault(),e()}},Wt=function(e){return function(t){t.preventDefault();var n=w();dt(e,null,n)?ut(null,{tag:e},n):ut({tag:e},null,n)}},_t=function(){try{var e,t=w(),n=t.startContainer;if(n.nodeType===s&&(n=n.parentNode),n.isInline()&&!n.textContent){do e=n.parentNode;while(e.isInline()&&!e.textContent&&(n=e));t.setStart(e,et.call(e.childNodes,n)),t.collapse(!0),e.removeChild(n),e.isBlock()||(e=e.getPreviousBlock()),e.fixCursor(),t.moveBoundariesDownTree(),A(t),z(t)}}catch(i){r.didError(i)}};C&&E("keyup",function(){var e=u.firstChild;"P"===e.nodeName&&(rt(w()),e.replaceWith(B("DIV",[e.empty()])),A(it()))});var Mt={enter:function(t){t.preventDefault();var n=w();if(n){st(n),St(n.startContainer),it(n),n.collapsed||n._deleteContents();var r,i=n.getStartBlock(),a=i?i.nodeName:"DIV",l=ht[a];if(!i)return n._insertNode(B("BR")),n.collapse(!1),A(n),z(n,!0),ot(),void 0;var d,c=n.startContainer,f=n.startOffset;if(l||(c===i&&(c=f?c.childNodes[f-1]:null,f=0,c&&("BR"===c.nodeName?c=c.nextSibling:f=c.getLength(),c&&"BR"!==c.nodeName||(d=B("DIV").fixCursor(),c?i.replaceChild(d,c):i.appendChild(d),c=d))),At(i,"DIV"),l="DIV",c||(c=i.firstChild),n.setStart(c,f),n.setEnd(c,f),i=n.getStartBlock()),!i.textContent){if(i.nearest("UL")||i.nearest("OL"))return gt(xt,n);if(i.nearest("BLOCKQUOTE"))return gt(vt,n)}for(r=pt(i,c,f);r.nodeType===o;){var p,N=r.firstChild;if("A"!==r.nodeName){for(;N&&N.nodeType===s&&!N.data&&(p=N.nextSibling,p&&"BR"!==p.nodeName);)N.detach(),N=p;if(!N||"BR"===N.nodeName||N.nodeType===s&&!h)break;r=N}else r.replaceWith(r.empty()),r=N}n=k(r,0),A(n),z(n,!0),r.nodeType===s&&(r=r.parentNode),r.offsetTop+r.offsetHeight>(e.documentElement.scrollTop||u.scrollTop)+u.offsetHeight&&r.scrollIntoView(!1),ot()}},backspace:function(e){var t=w();if(t.collapsed)if(t.startsAtBlockBoundary()){st(t),it(t),e.preventDefault();var n=t.getStartBlock(),r=n.getPreviousBlock();if(r){if(!r.isContentEditable)return r.detach(),void 0;for(r.mergeWithBlock(n,t),n=r.parentNode;n&&!n.nextSibling;)n=n.parentNode;n&&(n=n.nextSibling)&&n.mergeContainers(),A(t)}else{if(n.nearest("UL")||n.nearest("OL"))return gt(xt,t);if(n.nearest("BLOCKQUOTE"))return gt(mt,t);A(t),z(t,!0)}}else{var i=t.startContainer.data||"";T.test(i.charAt(t.startOffset-1))||(st(t),it(t),A(t)),setTimeout(_t,0)}else st(t),it(t),e.preventDefault(),t._deleteContents(),A(t),z(t,!0)},"delete":function(e){var t=w();if(t.collapsed)if(t.endsAtBlockBoundary()){st(t),it(t),e.preventDefault();var n=t.getStartBlock(),r=n.getNextBlock();if(r){if(!r.isContentEditable)return r.detach(),void 0;for(n.mergeWithBlock(r,t),r=n.parentNode;r&&!r.nextSibling;)r=r.parentNode;r&&(r=r.nextSibling)&&r.mergeContainers(),A(t),z(t,!0)}}else{var i=t.startContainer.data||"";T.test(i.charAt(t.startOffset))||(st(t),it(t),A(t)),setTimeout(_t,0)}else st(t),it(t),e.preventDefault(),t._deleteContents(),A(t),z(t,!0)},space:function(){var e=w();st(e),St(e.startContainer),it(e),A(e)},"ctrl-b":Wt("B"),"ctrl-i":Wt("I"),"ctrl-u":Wt("U"),"ctrl-y":Ht(lt),"ctrl-z":Ht(at),"ctrl-shift-z":Ht(lt)};E(h?"keypress":"keydown",function(e){var t=e.keyCode,n=Ft[t]||String.fromCharCode(t).toLowerCase(),r="";h&&46===e.which&&(n="."),t>111&&124>t&&(n="f"+(t-111)),e.altKey&&(r+="alt-"),(e.ctrlKey||e.metaKey)&&(r+="ctrl-"),e.shiftKey&&(r+="shift-"),n=r+n,Mt[n]&&Mt[n](e)});var zt=function(e){return function(){return e.apply(null,arguments),this}},Kt=function(e,t,n){return function(){return e(t,n),q(),this}};f.editor=r={didError:function(e){console.log(e)},_setPlaceholderTextNode:W,addEventListener:zt(E),removeEventListener:zt(D),focus:zt(q),blur:zt(G),getDocument:function(){return e},addStyles:function(t){if(t){var n=e.documentElement.firstChild,r=B("STYLE",{type:"text/css"});r.styleSheet?(n.appendChild(r),r.styleSheet.cssText=t):(r.appendChild(e.createTextNode(t)),n.appendChild(r))}return this},getHTML:function(e){var t,n,r,i,o,s=[];if(e&&(o=w())&&rt(o),y)for(t=u;t=t.getNextBlock();)t.textContent||t.querySelector("BR")||(n=B("BR"),t.appendChild(n),s.push(n));if(r=Z(),y)for(i=s.length;i--;)s[i].detach();return o&&it(o),r},setHTML:function(t){var n,r=e.createDocumentFragment(),i=B("DIV");i.innerHTML=t,r.appendChild(i.empty()),Lt(r,!0),Rt(r),At(r,"DIV");for(var o=r;o=o.getNextBlock();)o.fixCursor();for(;n=u.lastChild;)u.removeChild(n);u.appendChild(r),u.fixCursor(),Q=-1,Y=[],$=0,j=!1;var s=it()||k(u.firstChild,0);return st(s),it(s),v?L=s:A(s),z(s,!0),this},getSelectedText:function(){return w().getTextContent()},insertElement:zt(X),insertImage:function(e){var t=B("IMG",{src:e});return X(t),t},getPath:function(){return M},getSelection:w,setSelection:zt(A),undo:zt(at),redo:zt(lt),hasFormat:dt,changeFormat:zt(ut),bold:Kt(ut,{tag:"B"}),italic:Kt(ut,{tag:"I"}),underline:Kt(ut,{tag:"U"}),removeBold:Kt(ut,null,{tag:"B"}),removeItalic:Kt(ut,null,{tag:"I"}),removeUnderline:Kt(ut,null,{tag:"U"}),makeLink:function(t){t=encodeURI(t);var n=w();if(n.collapsed){var r=t.indexOf(":")+1;if(r)for(;"/"===t[r];)r+=1;n._insertNode(e.createTextNode(t.slice(r)))}return ut({tag:"A",attributes:{href:t}},{tag:"A"},n),q(),this},removeLink:function(){return ut(null,{tag:"A"},w(),!0),q(),this},setFontFace:function(e){return ut({tag:"SPAN",attributes:{"class":"font",style:"font-family: "+e+", sans-serif;"}},{tag:"SPAN",attributes:{"class":"font"}}),q(),this},setFontSize:function(e){return ut({tag:"SPAN",attributes:{"class":"size",style:"font-size: "+("number"==typeof e?e+"px":e)}},{tag:"SPAN",attributes:{"class":"size"}}),q(),this},setTextColour:function(e){return ut({tag:"SPAN",attributes:{"class":"colour",style:"color: "+e}},{tag:"SPAN",attributes:{"class":"colour"}}),q(),this},setHighlightColour:function(e){return ut({tag:"SPAN",attributes:{"class":"highlight",style:"background-color: "+e}},{tag:"SPAN",attributes:{"class":"highlight"}}),q(),this},setTextAlignment:function(e){return Nt(function(t){t.className=(t.className.split(/\s+/).filter(function(e){return!/align/.test(e)}).join(" ")+" align-"+e).trim(),t.style.textAlign=e},!0),q(),this},setTextDirection:function(e){return Nt(function(t){t.className=(t.className.split(/\s+/).filter(function(e){return!/dir/.test(e)}).join(" ")+" dir-"+e).trim(),t.dir=e},!0),q(),this},forEachBlock:zt(Nt),modifyBlocks:zt(gt),increaseQuoteLevel:Kt(gt,Ct),decreaseQuoteLevel:Kt(gt,mt),makeUnorderedList:Kt(gt,Tt),makeOrderedList:Kt(gt,Bt),removeList:Kt(gt,xt)},u.setAttribute("contenteditable","true"),r.setHTML(""),f.onEditorLoad&&(f.onEditorLoad(f.editor),f.onEditorLoad=null)}(document,UA,DOMTreeWalker); \ No newline at end of file +(function(e){"use strict";function t(e,t,n){this.root=this.currentNode=e,this.nodeType=t,this.filter=n}function n(e,t){for(var n=e.length;n--;)if(!t(e[n]))return!1;return!0}function r(e,t,n){if(e.nodeName!==t)return!1;for(var r in n)if(e.getAttribute(r)!==n[r])return!1;return!0}function o(e,t){return e.nodeType===t.nodeType&&e.nodeName===t.nodeName&&e.className===t.className&&(!e.style&&!t.style||e.style.cssText===t.style.cssText)}function i(e){return e.nodeType===E&&!!J[e.nodeName]}function a(e){return Z.test(e.nodeName)}function s(e){return e.nodeType===E&&!a(e)&&n(e.childNodes,a)}function d(e){return e.nodeType===E&&!a(e)&&!s(e)}function l(e){return s(e)?I:L}function f(e){var n=e.ownerDocument,r=new t(n.body,b,l,!1);return r.currentNode=e,r}function c(e){return f(e).previousNode()}function u(e){return f(e).nextNode()}function h(e,t,n){do if(r(e,t,n))return e;while(e=e.parentNode);return null}function p(e){var t,n,r,o,i=e.parentNode;return i&&e.nodeType===E?(t=p(i),t+=(t?">":"")+e.nodeName,(n=e.id)&&(t+="#"+n),(r=e.className.trim())&&(o=r.split(/\s\s*/),o.sort(),t+=".",t+=o.join("."))):t=i?p(i):"",t}function N(e){var t=e.nodeType;return t===E?e.childNodes.length:e.length||0}function C(e){var t=e.parentNode;return t&&t.removeChild(e),e}function v(e,t){var n=e.parentNode;n&&n.replaceChild(t,e)}function g(e){for(var t=e.ownerDocument.createDocumentFragment(),n=e.childNodes,r=n?n.length:0;r--;)t.appendChild(e.firstChild);return t}function m(e){var t,n,r=e.ownerDocument;if("BODY"===e.nodeName&&((n=e.firstChild)&&"BR"!==n.nodeName||(t=r.createElement("DIV"),n?e.replaceChild(t,n):e.appendChild(t),e=t,t=null)),a(e))e.firstChild||(Q?(t=r.createTextNode("​"),Tt(t)):t=r.createTextNode(""));else if(G){for(;e.nodeType!==D&&!i(e);){if(n=e.firstChild,!n){t=r.createTextNode("");break}e=n}e.nodeType===D?/^ +$/.test(e.data)&&(e.data=""):i(e)&&e.parentNode.insertBefore(r.createTextNode(""),e)}else if(!e.querySelector("BR"))for(t=r.createElement("BR");(n=e.lastElementChild)&&!a(n);)e=n;return t&&e.appendChild(t),e}function y(e,t,n){var r,o,i,a=e.nodeType;if(a===D)return e===n?t:y(e.parentNode,e.splitText(t),n);if(a===E){if("number"==typeof t&&(t=e.childNodes.length>t?e.childNodes[t]:null),e===n)return t;for(r=e.parentNode,o=e.cloneNode(!1);t;)i=t.nextSibling,o.appendChild(t),t=i;return m(e),m(o),(i=e.nextSibling)?r.insertBefore(o,i):r.appendChild(o),y(r,o,n)}return e}function T(e,t){if(e.nodeType===E)for(var n,r,i,s=e.childNodes,d=s.length,l=[];d--;)if(n=s[d],r=d&&s[d-1],d&&a(n)&&o(n,r)&&!J[n.nodeName])t.startContainer===n&&(t.startContainer=r,t.startOffset+=N(r)),t.endContainer===n&&(t.endContainer=r,t.endOffset+=N(r)),t.startContainer===e&&(t.startOffset>d?t.startOffset-=1:t.startOffset===d&&(t.startContainer=r,t.startOffset=N(r))),t.endContainer===e&&(t.endOffset>d?t.endOffset-=1:t.endOffset===d&&(t.endContainer=r,t.endOffset=N(r))),C(n),n.nodeType===D?r.appendData(n.data.replace(/\u200B/g,"")):l.push(g(n));else if(n.nodeType===E){for(i=l.length;i--;)n.appendChild(l.pop());T(n,t)}}function S(e,t,n){for(var r,o,i,a=t;1===a.parentNode.childNodes.length;)a=a.parentNode;C(a),o=e.childNodes.length,r=e.lastChild,r&&"BR"===r.nodeName&&(e.removeChild(r),o-=1),i={startContainer:e,startOffset:o,endContainer:e,endOffset:o},e.appendChild(g(t)),T(e,i),n.setStart(i.startContainer,i.startOffset),n.collapse(!0),M&&(r=e.lastChild)&&"BR"===r.nodeName&&e.removeChild(r)}function B(e){var t=e.previousSibling,n=e.firstChild;t&&o(t,e)&&d(t)&&(C(e),t.appendChild(g(e)),n&&B(n))}function O(t,n,r){var o,i,a,s=e.createElement(t);if(n instanceof Array&&(r=n,n=null),n)for(o in n)s.setAttribute(o,n[o]);if(r)for(i=0,a=r.length;a>i;i+=1)s.appendChild(r[i]);return s}var x=2,E=1,D=3,b=1,A=4,I=1,L=3,R=0,k=1,w=2,P=3,U=e.defaultView,V=e.body,H=navigator.userAgent,_=/Gecko\//.test(H),F=/Trident\//.test(H),z=8===U.ie,K=/iP(?:ad|hone|od)/.test(H),M=!!U.opera,q=/WebKit\//.test(H),G=F||M,Q=F||q,Y=F,$=/\S/,j=Array.prototype.indexOf,W={1:1,2:2,3:4,8:128,9:256,11:1024};t.prototype.nextNode=function(){for(var e,t=this.currentNode,n=this.root,r=this.nodeType,o=this.filter;;){for(e=t.firstChild;!e&&t&&t!==n;)e=t.nextSibling,e||(t=t.parentNode);if(!e)return null;if(W[e.nodeType]&r&&o(e)===I)return this.currentNode=e,e;t=e}},t.prototype.previousNode=function(){for(var e,t=this.currentNode,n=this.root,r=this.nodeType,o=this.filter;;){if(t===n)return null;if(e=t.previousSibling)for(;t=e.lastChild;)e=t;else e=t.parentNode;if(!e)return null;if(W[e.nodeType]&r&&o(e)===I)return this.currentNode=e,e;t=e}};var Z=/^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TRONG|AMP)|U)$/,J={BR:1,IMG:1,INPUT:1};(function(){var t=e.createElement("div"),n=e.createTextNode("12");return t.appendChild(n),n.splitText(2),2!==t.childNodes.length})()&&(Text.prototype.splitText=function(e){var t=this.ownerDocument.createTextNode(this.data.slice(e)),n=this.nextSibling,r=this.parentNode,o=this.length-e;return n?r.insertBefore(t,n):r.appendChild(t),o&&this.deleteData(e,o),t});var X=function(e,t){for(var n=e.childNodes;t&&e.nodeType===E;)e=n[t-1],n=e.childNodes,t=n.length;return e},et=function(e,t){if(e.nodeType===E){var n=e.childNodes;if(n.length>t)e=n[t];else{for(;e&&!e.nextSibling;)e=e.parentNode;e&&(e=e.nextSibling)}}return e},tt=Range.prototype;tt.forEachTextNode=function(e){var n=this.cloneRange();n.moveBoundariesDownTree();for(var r=n.startContainer,o=n.endContainer,i=n.commonAncestorContainer,a=new t(i,A,function(){return I},!1),s=a.currentNode=r;!e(s,n)&&s!==o&&(s=a.nextNode()););},tt.getTextContent=function(){var e="";return this.forEachTextNode(function(t,n){var r=t.data;r&&/\S/.test(r)&&(t===n.endContainer&&(r=r.slice(0,n.endOffset)),t===n.startContainer&&(r=r.slice(n.startOffset)),e+=r)}),e},tt._insertNode=function(e){var t,n,r,o,i=this.startContainer,a=this.startOffset,s=this.endContainer,d=this.endOffset;return i.nodeType===D?(t=i.parentNode,n=t.childNodes,a===i.length?(a=j.call(n,i)+1,this.collapsed&&(s=t,d=a)):(a&&(o=i.splitText(a),s===i?(d-=a,s=o):s===t&&(d+=1),i=o),a=j.call(n,i)),i=t):n=i.childNodes,r=n.length,a===r?i.appendChild(e):i.insertBefore(e,n[a]),i===s&&(d+=n.length-r),this.setStart(i,a),this.setEnd(s,d),this},tt._extractContents=function(e){var t=this.startContainer,n=this.startOffset,r=this.endContainer,o=this.endOffset;e||(e=this.commonAncestorContainer),e.nodeType===D&&(e=e.parentNode);for(var i,a=y(r,o,e),s=y(t,n,e),d=e.ownerDocument.createDocumentFragment();s!==a;)i=s.nextSibling,d.appendChild(s),s=i;return this.setStart(e,a?j.call(e.childNodes,a):e.childNodes.length),this.collapse(!0),m(e),d},tt._deleteContents=function(){this.moveBoundariesUpTree(),this._extractContents();var e=this.getStartBlock(),t=this.getEndBlock();e&&t&&e!==t&&S(e,t,this),e&&m(e);var n=this.endContainer.ownerDocument.body,r=n.firstChild;r&&"BR"!==r.nodeName||(m(n),this.selectNodeContents(n.firstChild));var o=this.collapsed;return this.moveBoundariesDownTree(),o&&this.collapse(!0),this},tt.insertTreeFragment=function(e){for(var t=!0,n=e.childNodes,r=n.length;r--;)if(!a(n[r])){t=!1;break}if(this.collapsed||this._deleteContents(),this.moveBoundariesDownTree(),t)this._insertNode(e),this.collapse(!1);else{for(var o,i,s=y(this.startContainer,this.startOffset,this.startContainer.ownerDocument.body),d=s.previousSibling,l=d,f=l.childNodes.length,c=s,h=0,p=s.parentNode;(o=l.lastChild)&&o.nodeType===E&&"BR"!==o.nodeName;)l=o,f=l.childNodes.length;for(;(o=c.firstChild)&&o.nodeType===E&&"BR"!==o.nodeName;)c=o;for(;(o=e.firstChild)&&a(o);)l.appendChild(o);for(;(o=e.lastChild)&&a(o);)c.insertBefore(o,c.firstChild),h+=1;for(i=e;i=u(i);)m(i);p.insertBefore(e,s),i=s.previousSibling,s.textContent?B(s):p.removeChild(s),s.parentNode||(c=i,h=N(c)),d.textContent?B(d):(l=d.nextSibling,f=0,p.removeChild(d)),this.setStart(l,f),this.setEnd(c,h),this.moveBoundariesDownTree()}},tt.containsNode=function(e,t){var n=this,r=e.ownerDocument.createRange();if(r.selectNode(e),t){var o=n.compareBoundaryPoints(P,r)>-1,i=1>n.compareBoundaryPoints(k,r);return!o&&!i}var a=1>n.compareBoundaryPoints(R,r),s=n.compareBoundaryPoints(w,r)>-1;return a&&s},tt.moveBoundariesDownTree=function(){for(var e,t=this.startContainer,n=this.startOffset,r=this.endContainer,o=this.endOffset;t.nodeType!==D&&(e=t.childNodes[n],e&&!i(e));)t=e,n=0;if(o)for(;r.nodeType!==D&&(e=r.childNodes[o-1],e&&!i(e));)r=e,o=N(r);else for(;r.nodeType!==D&&(e=r.firstChild,e&&!i(e));)r=e;return this.collapsed?(this.setStart(r,o),this.setEnd(t,n)):(this.setStart(t,n),this.setEnd(r,o)),this},tt.moveBoundariesUpTree=function(e){var t,n=this.startContainer,r=this.startOffset,o=this.endContainer,i=this.endOffset;for(e||(e=this.commonAncestorContainer);n!==e&&!r;)t=n.parentNode,r=j.call(t.childNodes,n),n=t;for(;o!==e&&i===N(o);)t=o.parentNode,i=j.call(t.childNodes,o)+1,o=t;return this.setStart(n,r),this.setEnd(o,i),this},tt.getStartBlock=function(){var e,t=this.startContainer;return a(t)?e=c(t):s(t)?e=t:(e=X(t,this.startOffset),e=u(e)),e&&this.containsNode(e,!0)?e:null},tt.getEndBlock=function(){var e,t,n=this.endContainer;if(a(n))e=c(n);else if(s(n))e=n;else{if(e=et(n,this.endOffset),!e)for(e=n.ownerDocument.body;t=e.lastChild;)e=t;e=c(e)}return e&&this.containsNode(e,!0)?e:null},tt.startsAtBlockBoundary=function(){for(var e,t,n=this.startContainer,r=this.startOffset;a(n);){if(r)return!1;e=n.parentNode,r=j.call(e.childNodes,n),n=e}for(;r&&(t=n.childNodes[r-1])&&(""===t.data||"BR"===t.nodeName);)r-=1;return!r},tt.endsAtBlockBoundary=function(){for(var e,t,n=this.endContainer,r=this.endOffset,o=N(n);a(n);){if(r!==o)return!1;e=n.parentNode,r=j.call(e.childNodes,n)+1,n=e,o=n.childNodes.length}for(;o>r&&(t=n.childNodes[r])&&(""===t.data||"BR"===t.nodeName);)r+=1;return r===o},tt.expandToBlockBoundaries=function(){var e,t=this.getStartBlock(),n=this.getEndBlock();return t&&n&&(e=t.parentNode,this.setStart(e,j.call(e.childNodes,t)),e=n.parentNode,this.setEnd(e,j.call(e.childNodes,n)+1)),this};var nt,rt={focus:1,blur:1,pathChange:1,select:1,input:1,undoStateChange:1},ot={},it=function(e,t){var n,r,o,i=ot[e];if(i)for(t||(t={}),t.type!==e&&(t.type=e),n=0,r=i.length;r>n;n+=1){o=i[n];try{o.handleEvent?o.handleEvent(t):o(t)}catch(a){nt.didError(a)}}},at=function(e){it(e.type,e)},st=function(t,n){var r=ot[t];r||(r=ot[t]=[],rt[t]||e.addEventListener(t,at,!1)),r.push(n)},dt=function(t,n){var r,o=ot[t];if(o){for(r=o.length;r--;)o[r]===n&&o.splice(r,1);o.length||(delete ot[t],rt[t]||e.removeEventListener(t,at,!1))}},lt=function(t,n,r,o){if(t instanceof Range)return t.cloneRange();var i=e.createRange();return i.setStart(t,n),r?i.setEnd(r,o):i.setEnd(t,n),i},ft=U.getSelection(),ct=null,ut=function(e){e&&(K&&U.focus(),ft.removeAllRanges(),ft.addRange(e))},ht=function(){if(ft.rangeCount){ct=ft.getRangeAt(0).cloneRange();var e=ct.startContainer,t=ct.endContainer;try{e&&i(e)&&ct.setStartBefore(e),t&&i(t)&&ct.setEndBefore(t)}catch(n){nt.didError({name:"Squire#getSelection error",message:"Starts: "+e.nodeName+"\nEnds: "+t.nodeName})}}return ct};Y&&U.addEventListener("beforedeactivate",ht,!0);var pt,Nt,Ct=null,vt=!0,gt=!1,mt=function(){vt=!0,gt=!1},yt=function(){if(vt){var e,t=Ct;if(Ct=null,t.parentNode){for(;(e=t.data.indexOf("​"))>-1;)t.deleteData(e,1);t.data||t.nextSibling||t.previousSibling||!a(t.parentNode)||C(t.parentNode)}}},Tt=function(e){Ct&&(vt=!0,yt()),gt||(setTimeout(mt,0),gt=!0),vt=!1,Ct=e},St="",Bt=function(e,t){Ct&&!t&&yt(e);var n,r=e.startContainer,o=e.endContainer;(t||r!==pt||o!==Nt)&&(pt=r,Nt=o,n=r&&o?r===o?p(o):"(selection)":"",St!==n&&(St=n,it("pathChange",{path:n}))),r!==o&&it("select")},Ot=function(){Bt(ht())};st("keyup",Ot),st("mouseup",Ot);var xt=function(){_&&V.focus(),U.focus()},Et=function(){_&&V.blur(),top.focus()};U.addEventListener("focus",at,!1),U.addEventListener("blur",at,!1);var Dt,bt,At,It,Lt=function(){return V.innerHTML},Rt=function(e){var t=V;t.innerHTML=e;do m(t);while(t=u(t))},kt=function(e,t){t||(t=ht()),t.collapse(!0),t._insertNode(e),t.setStartAfter(e),ut(t),Bt(t)},wt="squire-selection-start",Pt="squire-selection-end",Ut=function(e){var t,n=O("INPUT",{id:wt,type:"hidden"}),r=O("INPUT",{id:Pt,type:"hidden"});e._insertNode(n),e.collapse(!1),e._insertNode(r),n.compareDocumentPosition(r)&x&&(n.id=Pt,r.id=wt,t=n,n=r,r=t),e.setStartAfter(n),e.setEndBefore(r)},Vt=function(t){var n=e.getElementById(wt),r=e.getElementById(Pt);if(n&&r){var o,i=n.parentNode,a=r.parentNode,s={startContainer:i,endContainer:a,startOffset:j.call(i.childNodes,n),endOffset:j.call(a.childNodes,r)};i===a&&(s.endOffset-=1),C(n),C(r),T(i,s),i!==a&&T(a,s),t||(t=e.createRange()),t.setStart(s.startContainer,s.startOffset),t.setEnd(s.endContainer,s.endOffset),o=t.collapsed,t.moveBoundariesDownTree(),o&&t.collapse(!0)}return t||null},Ht=function(){It&&(It=!1,it("undoStateChange",{canUndo:!0,canRedo:!1})),it("input")};st("keyup",function(e){var t=e.keyCode;e.ctrlKey||e.metaKey||e.altKey||!(16>t||t>20)||!(33>t||t>45)||Ht()});var _t=function(e){It||(Dt+=1,At>Dt&&(bt.length=At=Dt),e&&Ut(e),bt[Dt]=Lt(),At+=1,It=!0)},Ft=function(){if(0!==Dt||!It){_t(ht()),Dt-=1,Rt(bt[Dt]);var e=Vt();e&&ut(e),It=!0,it("undoStateChange",{canUndo:0!==Dt,canRedo:!0}),it("input")}},zt=function(){if(At>Dt+1&&It){Dt+=1,Rt(bt[Dt]);var e=Vt();e&&ut(e),it("undoStateChange",{canUndo:!0,canRedo:At>Dt+1}),it("input")}},Kt=function(e,n,r){if(e=e.toUpperCase(),n||(n={}),!r&&!(r=ht()))return!1;var o,i,a=r.commonAncestorContainer;if(h(a,e,n))return!0;if(a.nodeType===D)return!1;o=new t(a,A,function(e){return r.containsNode(e,!0)?I:L},!1);for(var s=!1;i=o.nextNode();){if(!h(i,e,n))return!1;s=!0}return s},Mt=function(e,n,r){var o,i,a,s,d,l,f,c;if(r.collapsed)o=m(O(e,n)),r._insertNode(o),r.setStart(o.firstChild,o.firstChild.length),r.collapse(!0);else{i=new t(r.commonAncestorContainer,A,function(e){return r.containsNode(e,!0)?I:L},!1),d=0,l=0,f=i.currentNode=r.startContainer,f.nodeType!==D&&(f=i.nextNode());do c=!h(f,e,n),f===r.endContainer&&(c&&f.length>r.endOffset?f.splitText(r.endOffset):l=r.endOffset),f===r.startContainer&&(c&&r.startOffset?f=f.splitText(r.startOffset):d=r.startOffset),c&&(o=O(e,n),v(f,o),o.appendChild(f),l=f.length),s=f,a||(a=s);while(f=i.nextNode());r=lt(a,d,s,l)}return r},qt=function(t,n,o,i){Ut(o);var s;o.collapsed&&(Q?(s=e.createTextNode("​"),Tt(s)):s=e.createTextNode(""),o._insertNode(s));for(var d=o.commonAncestorContainer;a(d);)d=d.parentNode;var l=o.startContainer,f=o.startOffset,c=o.endContainer,u=o.endOffset,h=[],p=function(e,t){if(!o.containsNode(e,!1)){var n,r,i=e.nodeType===D;if(!o.containsNode(e,!0))return"INPUT"===e.nodeName||i&&!e.data||h.push([t,e]),void 0;if(i)e===c&&u!==e.length&&h.push([t,e.splitText(u)]),e===l&&f&&(e.splitText(f),h.push([t,e]));else for(n=e.firstChild;n;n=r)r=n.nextSibling,p(n,t)}},N=Array.prototype.filter.call(d.getElementsByTagName(t),function(e){return o.containsNode(e,!0)&&r(e,t,n)});i||N.forEach(function(e){p(e,e)}),h.forEach(function(e){var t=e[0].cloneNode(!1),n=e[1];v(n,t),t.appendChild(n)}),N.forEach(function(e){v(e,g(e))}),Vt(o),s&&o.collapse(!1);var C={startContainer:o.startContainer,startOffset:o.startOffset,endContainer:o.endContainer,endOffset:o.endOffset};return T(d,C),o.setStart(C.startContainer,C.startOffset),o.setEnd(C.endContainer,C.endOffset),o},Gt=function(e,t,n,r){(n||(n=ht()))&&(_t(n),Vt(n),t&&(n=qt(t.tag.toUpperCase(),t.attributes||{},n,r)),e&&(n=Mt(e.tag.toUpperCase(),e.attributes||{},n)),ut(n),Bt(n,!0),Ht())},Qt={DIV:"DIV",PRE:"DIV",H1:"DIV",H2:"DIV",H3:"DIV",H4:"DIV",H5:"DIV",H6:"DIV",P:"DIV",DT:"DD",DD:"DT",LI:"LI"},Yt=function(e,t,n){var r=Qt[e.nodeName],o=y(t,n,e.parentNode);return o.nodeName!==r&&(e=O(r),e.className="rtl"===o.dir?"dir-rtl":"",e.dir=o.dir,v(o,e),e.appendChild(g(o)),o=e),o},$t=function(e,t,n){if(n||(n=ht())){t&&(_t(n),Vt(n));var r=n.getStartBlock(),o=n.getEndBlock();if(r&&o)do if(e(r)||r===o)break;while(r=u(r));t&&(ut(n),Bt(n,!0),Ht())}},jt=function(e,t){if(t||(t=ht())){M||V.setAttribute("contenteditable","false"),It?Ut(t):_t(t),t.expandToBlockBoundaries(),t.moveBoundariesUpTree(V);var n=t._extractContents(V);t._insertNode(e(n)),t.endOffsetn;n+=1)o=e[n],i=o.nodeName,s(o)?"LI"!==i&&(l=O("LI",{"class":"rtl"===o.dir?"dir-rtl":"",dir:o.dir},[g(o)]),o.parentNode.nodeName===t?v(o,l):(a=o.previousSibling)&&a.nodeName===t?(a.appendChild(l),C(o),n-=1,r-=1):v(o,O(t,[l]))):d(o)&&(i!==t&&/^[DOU]L$/.test(i)?v(o,O(t,[g(o)])):Xt(o.childNodes,t))},en=function(e){return Xt(e.childNodes,"UL"),e},tn=function(e){return Xt(e.childNodes,"OL"),e},nn=function(e){var t=e.querySelectorAll("UL, OL");return Array.prototype.filter.call(t,function(e){return!h(e.parentNode,"UL")&&!h(e.parentNode,"OL")}).forEach(function(e){for(var t,n=g(e),r=n.childNodes,o=r.length;o--;)t=r[o],"LI"===t.nodeName&&n.replaceChild(O("DIV",{"class":"rtl"===t.dir?"dir-rtl":"",dir:t.dir},[g(t)]),t);v(e,n)}),e},rn=/\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])|(?:[\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,4}))/i,on=function(e){for(var n,r,o,i,a,s,d,l=e.ownerDocument,f=new t(e,A,function(e){return h(e,"A")?L:I},!1);n=f.nextNode();)if(r=n.data.split(rn),i=r.length,i>1){for(s=n.parentNode,d=n.nextSibling,o=0;i>o;o+=1)a=r[o],o?(o%2?(n=l.createElement("A"),n.textContent=a,n.href=/@/.test(a)?"mailto:"+a:/^(?:ht|f)tps?:/.test(a)?a:"http://"+a):n=l.createTextNode(a),d?s.insertBefore(n,d):s.appendChild(n)):n.data=a;f.currentNode=n}},an=/^(?:A(?:DDRESS|RTICLE|SIDE)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/,sn={1:10,2:13,3:16,4:18,5:24,6:32,7:48},dn={backgroundColor:{regexp:$,replace:function(e){return O("SPAN",{"class":"highlight",style:"background-color: "+e})}},color:{regexp:$,replace:function(e){return O("SPAN",{"class":"colour",style:"color:"+e})}},fontWeight:{regexp:/^bold/i,replace:function(){return O("B")}},fontStyle:{regexp:/^italic/i,replace:function(){return O("I")}},fontFamily:{regexp:$,replace:function(e){return O("SPAN",{"class":"font",style:"font-family:"+e})}},fontSize:{regexp:$,replace:function(e){return O("SPAN",{"class":"size",style:"font-size:"+e})}}},ln={SPAN:function(e,t){var n,r,o,i,a,s,d=e.style;for(n in dn)r=dn[n],o=d[n],o&&r.regexp.test(o)&&(s=r.replace(o),i&&i.appendChild(s),i=s,a||(a=s));return a&&(i.appendChild(g(e)),t.replaceChild(a,e)),i||e},STRONG:function(e,t){var n=O("B");return t.replaceChild(n,e),n.appendChild(g(e)),n},EM:function(e,t){var n=O("I");return t.replaceChild(n,e),n.appendChild(g(e)),n},FONT:function(e,t){var n,r,o,i,a=e.face,s=e.size;return a&&(n=O("SPAN",{"class":"font",style:"font-family:"+a})),s&&(r=O("SPAN",{"class":"size",style:"font-size:"+sn[s]+"px"}),n&&n.appendChild(r)),i=n||r||O("SPAN"),o=r||n||i,t.replaceChild(i,e),o.appendChild(g(e)),o},TT:function(e,t){var n=O("SPAN",{"class":"font",style:'font-family:menlo,consolas,"courier new",monospace'});return t.replaceChild(n,e),n.appendChild(g(e)),n}},fn=function(e){for(var t,n=e.childNodes,r=n.length;r--;)t=n[r],t.nodeType===E&&(fn(t),a(t)&&!t.firstChild&&e.removeChild(t))},cn=function(e,t){var n,r,o,i,s,d,l,f=e.childNodes;for(n=0,r=f.length;r>n;n+=1)if(o=f[n],i=o.nodeName,s=o.nodeType,d=ln[i],s===E){if(l=o.childNodes.length,d)o=d(o,e);else{if(!an.test(i)&&!a(o)){n-=1,r+=l-1,e.replaceChild(g(o),o);continue}!t&&o.style.cssText&&o.removeAttribute("style")}l&&cn(o,t)}else s===D&&($.test(o.data)||n>0&&a(f[n-1])||r>n+1&&a(f[n+1]))||(e.removeChild(o),n-=1,r-=1);return e},un=function(e,t){var n,r,o,i,s=e.childNodes,d=null;for(n=0,r=s.length;r>n;n+=1)o=s[n],i="BR"===o.nodeName,!i&&a(o)?(d||(d=O(t)),d.appendChild(o),n-=1,r-=1):(i||d)&&(d||(d=O(t)),m(d),i?e.replaceChild(d,o):(e.insertBefore(d,o),n+=1,r+=1),d=null);return d&&e.appendChild(m(d)),e},hn=function(e){return(e.nodeType===E?"BR"===e.nodeName:$.test(e.data))?I:L},pn=function(e){for(var n,r=e.parentNode;a(r);)r=r.parentNode;return n=new t(r,b|A,hn),n.currentNode=e,!!n.nextNode()},Nn=function(e){var t,n,r,o=e.querySelectorAll("BR"),i=[],d=o.length;for(t=0;d>t;t+=1)i[t]=pn(o[t]);for(;d--;)if(n=o[d],r=n.parentNode){for(;a(r);)r=r.parentNode;s(r)&&Qt[r.nodeName]?(i[d]&&Yt(r,n.parentNode,n),C(n)):un(r,"DIV")}},Cn=function(){try{m(V)}catch(e){nt.didError(e)}};st(F?"beforecut":"cut",function(){var e=ht();_t(e),Vt(e),ut(e),setTimeout(Cn,0)});var vn=!1;st(F?"beforepaste":"paste",function(e){if(!vn){var t,n=e.clipboardData,r=n&&n.items,o=!1;if(r)for(t=r.length;t--;)if(/^image\/.*/.test(r[t].type))return e.preventDefault(),it("dragover",{dataTransfer:n,preventDefault:function(){o=!0}}),o&&it("drop",{dataTransfer:n}),void 0;vn=!0;var i=ht(),a=i.startContainer,s=i.startOffset,d=i.endContainer,l=i.endOffset,f=O("DIV",{style:"position: absolute; overflow: hidden; top:"+(V.scrollTop+30)+"px; left: 0; width: 1px; height: 1px;"});V.appendChild(f),i.selectNodeContents(f),ut(i),setTimeout(function(){try{var e=g(C(f)),t=e.firstChild,n=lt(a,s,d,l);if(t){t===e.lastChild&&"DIV"===t.nodeName&&e.replaceChild(g(t),t),e.normalize(),on(e),cn(e,!1),Nn(e),fn(e);for(var r=e,o=!0;r=u(r);)m(r);it("willPaste",{fragment:e,preventDefault:function(){o=!1}}),o&&(n.insertTreeFragment(e),Ht(),n.collapse(!1))}ut(n),Bt(n,!0),vn=!1}catch(i){nt.didError(i)}},0)}});var gn={8:"backspace",9:"tab",13:"enter",32:"space",46:"delete"},mn=function(e){return function(t){t.preventDefault(),e()}},yn=function(e){return function(t){t.preventDefault();var n=ht();Kt(e,null,n)?Gt(null,{tag:e},n):Gt({tag:e},null,n)}},Tn=function(){try{var e,t=ht(),n=t.startContainer;if(n.nodeType===D&&(n=n.parentNode),a(n)&&!n.textContent){do e=n.parentNode;while(a(e)&&!e.textContent&&(n=e));t.setStart(e,j.call(e.childNodes,n)),t.collapse(!0),e.removeChild(n),s(e)||(e=c(e)),m(e),t.moveBoundariesDownTree(),ut(t),Bt(t)}}catch(r){nt.didError(r)}};z&&st("keyup",function(){var e=V.firstChild;"P"===e.nodeName&&(Ut(ht()),v(e,O("DIV",[g(e)])),ut(Vt()))});var Sn={enter:function(t){t.preventDefault();var n=ht();if(n){_t(n),on(n.startContainer),Vt(n),n.collapsed||n._deleteContents();var r,o=n.getStartBlock(),i=o?o.nodeName:"DIV",a=Qt[i];if(!o)return n._insertNode(O("BR")),n.collapse(!1),ut(n),Bt(n,!0),Ht(),void 0;var s,d=n.startContainer,l=n.startOffset;if(a||(d===o&&(d=l?d.childNodes[l-1]:null,l=0,d&&("BR"===d.nodeName?d=d.nextSibling:l=N(d),d&&"BR"!==d.nodeName||(s=m(O("DIV")),d?o.replaceChild(s,d):o.appendChild(s),d=s))),un(o,"DIV"),a="DIV",d||(d=o.firstChild),n.setStart(d,l),n.setEnd(d,l),o=n.getStartBlock()),!o.textContent){if(h(o,"UL")||h(o,"OL"))return jt(nn,n);if(h(o,"BLOCKQUOTE"))return jt(Jt,n)}for(r=Yt(o,d,l);r.nodeType===E;){var f,c=r.firstChild;if("A"!==r.nodeName){for(;c&&c.nodeType===D&&!c.data&&(f=c.nextSibling,f&&"BR"!==f.nodeName);)C(c),c=f;if(!c||"BR"===c.nodeName||c.nodeType===D&&!M)break;r=c}else v(r,g(r)),r=c}n=lt(r,0),ut(n),Bt(n,!0),r.nodeType===D&&(r=r.parentNode),r.offsetTop+r.offsetHeight>(e.documentElement.scrollTop||V.scrollTop)+V.offsetHeight&&r.scrollIntoView(!1),Ht()}},backspace:function(e){var t=ht();if(t.collapsed)if(t.startsAtBlockBoundary()){_t(t),Vt(t),e.preventDefault();var n=t.getStartBlock(),r=n&&c(n);if(r){if(!r.isContentEditable)return C(r),void 0;for(S(r,n,t),n=r.parentNode;n&&!n.nextSibling;)n=n.parentNode;n&&(n=n.nextSibling)&&B(n),ut(t)}else{if(h(n,"UL")||h(n,"OL"))return jt(nn,t);if(h(n,"BLOCKQUOTE"))return jt(Zt,t);ut(t),Bt(t,!0)}}else{var o=t.startContainer.data||"";$.test(o.charAt(t.startOffset-1))||(_t(t),Vt(t),ut(t)),setTimeout(Tn,0)}else _t(t),Vt(t),e.preventDefault(),t._deleteContents(),ut(t),Bt(t,!0)},"delete":function(e){var t=ht();if(t.collapsed)if(t.endsAtBlockBoundary()){_t(t),Vt(t),e.preventDefault();var n=t.getStartBlock(),r=n&&u(n);if(r){if(!r.isContentEditable)return C(r),void 0;for(S(n,r,t),r=n.parentNode;r&&!r.nextSibling;)r=r.parentNode;r&&(r=r.nextSibling)&&B(r),ut(t),Bt(t,!0)}}else{var o=t.startContainer.data||"";$.test(o.charAt(t.startOffset))||(_t(t),Vt(t),ut(t)),setTimeout(Tn,0)}else _t(t),Vt(t),e.preventDefault(),t._deleteContents(),ut(t),Bt(t,!0)},space:function(){var e=ht();_t(e),on(e.startContainer),Vt(e),ut(e)},"ctrl-b":yn("B"),"ctrl-i":yn("I"),"ctrl-u":yn("U"),"ctrl-y":mn(zt),"ctrl-z":mn(Ft),"ctrl-shift-z":mn(zt)};st(M?"keypress":"keydown",function(e){var t=e.keyCode,n=gn[t]||String.fromCharCode(t).toLowerCase(),r="";M&&46===e.which&&(n="."),t>111&&124>t&&(n="f"+(t-111)),e.altKey&&(r+="alt-"),(e.ctrlKey||e.metaKey)&&(r+="ctrl-"),e.shiftKey&&(r+="shift-"),n=r+n,Sn[n]&&Sn[n](e)});var Bn=function(e){return function(){return e.apply(null,arguments),this}},On=function(e,t,n){return function(){return e(t,n),xt(),this}};nt=U.editor={didError:function(e){console.log(e)},addEventListener:Bn(st),removeEventListener:Bn(dt),focus:Bn(xt),blur:Bn(Et),getDocument:function(){return e},addStyles:function(t){if(t){var n=e.documentElement.firstChild,r=O("STYLE",{type:"text/css"});r.styleSheet?(n.appendChild(r),r.styleSheet.cssText=t):(r.appendChild(e.createTextNode(t)),n.appendChild(r))}return this},getHTML:function(e){var t,n,r,o,i,a=[];if(e&&(i=ht())&&Ut(i),G)for(t=V;t=u(t);)t.textContent||t.querySelector("BR")||(n=O("BR"),t.appendChild(n),a.push(n));if(r=Lt(),G)for(o=a.length;o--;)C(a[o]);return i&&Vt(i),r},setHTML:function(t){var n,r=e.createDocumentFragment(),o=O("DIV");o.innerHTML=t,r.appendChild(g(o)),cn(r,!0),Nn(r),un(r,"DIV");for(var i=r;i=u(i);)m(i);for(;n=V.lastChild;)V.removeChild(n);V.appendChild(r),m(V),Dt=-1,bt=[],At=0,It=!1;var a=Vt()||lt(V.firstChild,0);return _t(a),Vt(a),Y?ct=a:ut(a),Bt(a,!0),this},getSelectedText:function(){return ht().getTextContent()},insertElement:Bn(kt),insertImage:function(e){var t=O("IMG",{src:e});return kt(t),t},getPath:function(){return St},getSelection:ht,setSelection:Bn(ut),undo:Bn(Ft),redo:Bn(zt),hasFormat:Kt,changeFormat:Bn(Gt),bold:On(Gt,{tag:"B"}),italic:On(Gt,{tag:"I"}),underline:On(Gt,{tag:"U"}),removeBold:On(Gt,null,{tag:"B"}),removeItalic:On(Gt,null,{tag:"I"}),removeUnderline:On(Gt,null,{tag:"U"}),makeLink:function(t){t=encodeURI(t);var n=ht();if(n.collapsed){var r=t.indexOf(":")+1;if(r)for(;"/"===t[r];)r+=1;n._insertNode(e.createTextNode(t.slice(r)))}return Gt({tag:"A",attributes:{href:t}},{tag:"A"},n),xt(),this},removeLink:function(){return Gt(null,{tag:"A"},ht(),!0),xt(),this},setFontFace:function(e){return Gt({tag:"SPAN",attributes:{"class":"font",style:"font-family: "+e+", sans-serif;"}},{tag:"SPAN",attributes:{"class":"font"}}),xt(),this},setFontSize:function(e){return Gt({tag:"SPAN",attributes:{"class":"size",style:"font-size: "+("number"==typeof e?e+"px":e)}},{tag:"SPAN",attributes:{"class":"size"}}),xt(),this},setTextColour:function(e){return Gt({tag:"SPAN",attributes:{"class":"colour",style:"color: "+e}},{tag:"SPAN",attributes:{"class":"colour"}}),xt(),this},setHighlightColour:function(e){return Gt({tag:"SPAN",attributes:{"class":"highlight",style:"background-color: "+e}},{tag:"SPAN",attributes:{"class":"highlight"}}),xt(),this},setTextAlignment:function(e){return $t(function(t){t.className=(t.className.split(/\s+/).filter(function(e){return!/align/.test(e)}).join(" ")+" align-"+e).trim(),t.style.textAlign=e},!0),xt(),this},setTextDirection:function(e){return $t(function(t){t.className=(t.className.split(/\s+/).filter(function(e){return!/dir/.test(e)}).join(" ")+" dir-"+e).trim(),t.dir=e},!0),xt(),this},forEachBlock:Bn($t),modifyBlocks:Bn(jt),increaseQuoteLevel:On(jt,Wt),decreaseQuoteLevel:On(jt,Zt),makeUnorderedList:On(jt,en),makeOrderedList:On(jt,tn),removeList:On(jt,nn)},V.setAttribute("contenteditable","true"),nt.setHTML(""),U.onEditorLoad&&(U.onEditorLoad(U.editor),U.onEditorLoad=null)})(document); \ No newline at end of file diff --git a/source/Constants.js b/source/Constants.js new file mode 100644 index 0000000..167810a --- /dev/null +++ b/source/Constants.js @@ -0,0 +1,33 @@ +/*global doc, navigator */ + +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 body = doc.body; + +var ua = navigator.userAgent; +var isGecko = /Gecko\//.test( ua ); +var isIE = /Trident\//.test( ua ); +var isIE8 = ( win.ie === 8 ); +var isIOS = /iP(?:ad|hone|od)/.test( ua ); +var isOpera = !!win.opera; +var isWebKit = /WebKit\//.test( ua ); + +var useTextFixer = isIE || isOpera; +var cantFocusEmptyTextNodes = isIE || isWebKit; +var losesSelectionOnBlur = isIE; + +var notWS = /\S/; + +var indexOf = Array.prototype.indexOf; diff --git a/source/Editor.js b/source/Editor.js index 27017db..b7b66ba 100644 --- a/source/Editor.js +++ b/source/Editor.js @@ -1,2120 +1,2118 @@ -/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license. */ +/*global + DOCUMENT_POSITION_PRECEDING, + ELEMENT_NODE, + TEXT_NODE, + SHOW_ELEMENT, + SHOW_TEXT, + FILTER_ACCEPT, + FILTER_SKIP, + doc, + win, + body, + isGecko, + isIE, + isIE8, + isIOS, + isOpera, + useTextFixer, + cantFocusEmptyTextNodes, + losesSelectionOnBlur, + notWS, + indexOf, -/*global UA, DOMTreeWalker, Range, top, document, setTimeout, console */ + TreeWalker, -( function ( doc, UA, TreeWalker ) { + hasTagAttributes, + isLeaf, + isInline, + isBlock, + isContainer, + getPreviousBlock, + getNextBlock, + getNearest, + getPath, + getLength, + detach, + replaceWith, + empty, + fixCursor, + split, + mergeInlines, + mergeWithBlock, + mergeContainers, + createElement, - "use strict"; + Range, + top, + console, + setTimeout +*/ +/*jshint strict:false */ - // --- Constants --- +var editor; - var DOCUMENT_POSITION_PRECEDING = 2, // Node.DOCUMENT_POSITION_PRECEDING - ELEMENT_NODE = 1, // Node.ELEMENT_NODE, - TEXT_NODE = 3, // Node.TEXT_NODE, - SHOW_ELEMENT = 1, // NodeFilter.SHOW_ELEMENT, - SHOW_TEXT = 4, // NodeFilter.SHOW_TEXT, - FILTER_ACCEPT = 1, // NodeFilter.FILTER_ACCEPT, - FILTER_SKIP = 3; // NodeFilter.FILTER_SKIP; +// --- Events.js --- - var win = doc.defaultView; - var body = doc.body; - var editor; +// 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 +}; - var isOpera = UA.isOpera; - var isGecko = UA.isGecko; - var isIOS = UA.isIOS; - var isIE = UA.isIE; - var isIE8 = UA.isIE8; +var events = {}; - var cantFocusEmptyTextNodes = UA.cantFocusEmptyTextNodes; - var losesSelectionOnBlur = UA.losesSelectionOnBlur; - var useTextFixer = UA.useTextFixer; - - var notWS = /\S/; - - // --- DOM Sugar --- - - var createElement = function ( tag, props, children ) { - var el = doc.createElement( tag ), - attr, i, l; - if ( props instanceof Array ) { - children = props; - props = null; +var fireEvent = function ( type, event ) { + var handlers = events[ type ], + i, l, obj; + if ( handlers ) { + if ( !event ) { + event = {}; } - if ( props ) { - for ( attr in props ) { - el.setAttribute( attr, props[ attr ] ); - } + if ( event.type !== type ) { + event.type = type; } - if ( children ) { - for ( i = 0, l = children.length; i < l; i += 1 ) { - el.appendChild( children[i] ); - } - } - return el; - }; - - // --- 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 - }; - - var events = {}; - - var fireEvent = function ( type, event ) { - var handlers = events[ type ], - i, l, obj; - if ( handlers ) { - if ( !event ) { - event = {}; - } - if ( event.type !== type ) { - event.type = type; - } - for ( i = 0, l = handlers.length; i < l; i += 1 ) { - obj = handlers[i]; - try { - if ( obj.handleEvent ) { - obj.handleEvent( event ); - } else { - obj( event ); - } - } catch ( error ) { - editor.didError( error ); - } - } - } - }; - - var propagateEvent = function ( event ) { - fireEvent( event.type, event ); - }; - - var addEventListener = function ( type, fn ) { - var handlers = events[ type ]; - if ( !handlers ) { - handlers = events[ type ] = []; - if ( !customEvents[ type ] ) { - doc.addEventListener( type, propagateEvent, false ); - } - } - handlers.push( fn ); - }; - - var removeEventListener = function ( type, fn ) { - var handlers = events[ type ], - l; - if ( handlers ) { - l = handlers.length; - while ( l-- ) { - if ( handlers[l] === fn ) { - handlers.splice( l, 1 ); - } - } - if ( !handlers.length ) { - delete events[ type ]; - if ( !customEvents[ type ] ) { - doc.removeEventListener( type, propagateEvent, false ); - } - } - } - }; - - // --- Selection and Path --- - - var createRange = function ( range, startOffset, endContainer, endOffset ) { - if ( range instanceof Range ) { - return range.cloneRange(); - } - var domRange = doc.createRange(); - domRange.setStart( range, startOffset ); - if ( endContainer ) { - domRange.setEnd( endContainer, endOffset ); - } else { - domRange.setEnd( range, startOffset ); - } - return domRange; - }; - - var sel = win.getSelection(); - var lastSelection = null; - - var 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 ) { - win.focus(); - } - sel.removeAllRanges(); - sel.addRange( range ); - } - }; - - var getSelection = function () { - if ( sel.rangeCount ) { - lastSelection = sel.getRangeAt( 0 ).cloneRange(); - var startContainer = lastSelection.startContainer, - endContainer = lastSelection.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. + for ( i = 0, l = handlers.length; i < l; i += 1 ) { + obj = handlers[i]; try { - // FF can return the selection as being inside an . WTF? - if ( startContainer && startContainer.isLeaf() ) { - lastSelection.setStartBefore( startContainer ); - } - if ( endContainer && endContainer.isLeaf() ) { - lastSelection.setEndBefore( endContainer ); + if ( obj.handleEvent ) { + obj.handleEvent( event ); + } else { + obj( event ); } } catch ( error ) { - editor.didError({ - name: 'Squire#getSelection error', - message: 'Starts: ' + startContainer.nodeName + - '\nEnds: ' + endContainer.nodeName - }); + editor.didError( error ); } } - return lastSelection; - }; + } +}; - // IE loses selection state of iframe on blur, so make sure we - // cache it just before it loses focus. - if ( losesSelectionOnBlur ) { - win.addEventListener( 'beforedeactivate', getSelection, true ); +var propagateEvent = function ( event ) { + fireEvent( event.type, event ); +}; + +var addEventListener = function ( type, fn ) { + var handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + if ( !customEvents[ type ] ) { + doc.addEventListener( type, propagateEvent, false ); + } + } + handlers.push( fn ); +}; + +var removeEventListener = function ( type, fn ) { + var handlers = events[ type ], + l; + if ( handlers ) { + l = handlers.length; + while ( l-- ) { + if ( handlers[l] === fn ) { + handlers.splice( l, 1 ); + } + } + if ( !handlers.length ) { + delete events[ type ]; + if ( !customEvents[ type ] ) { + doc.removeEventListener( type, propagateEvent, false ); + } + } + } +}; + +// --- Selection and Path --- + +var createRange = function ( range, startOffset, endContainer, endOffset ) { + if ( range instanceof Range ) { + return range.cloneRange(); + } + var domRange = doc.createRange(); + domRange.setStart( range, startOffset ); + if ( endContainer ) { + domRange.setEnd( endContainer, endOffset ); + } else { + domRange.setEnd( range, startOffset ); + } + return domRange; +}; + +var sel = win.getSelection(); +var lastSelection = null; + +var 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 ) { + win.focus(); + } + sel.removeAllRanges(); + sel.addRange( range ); + } +}; + +var getSelection = function () { + if ( sel.rangeCount ) { + lastSelection = sel.getRangeAt( 0 ).cloneRange(); + var startContainer = lastSelection.startContainer, + endContainer = lastSelection.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 ) ) { + lastSelection.setStartBefore( startContainer ); + } + if ( endContainer && isLeaf( endContainer ) ) { + lastSelection.setEndBefore( endContainer ); + } + } catch ( error ) { + editor.didError({ + name: 'Squire#getSelection error', + message: 'Starts: ' + startContainer.nodeName + + '\nEnds: ' + endContainer.nodeName + }); + } + } + return lastSelection; +}; + +// IE loses selection state of iframe on blur, so make sure we +// cache it just before it loses focus. +if ( losesSelectionOnBlur ) { + win.addEventListener( 'beforedeactivate', getSelection, true ); +} + +// --- Workaround for browsers that can't focus empty text nodes --- + +// WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256 + +var placeholderTextNode = null; +var mayRemovePlaceholder = true; +var willEnablePlaceholderRemoval = false; + +var enablePlaceholderRemoval = function () { + mayRemovePlaceholder = true; + willEnablePlaceholderRemoval = false; +}; + +var removePlaceholderTextNode = function () { + if ( !mayRemovePlaceholder ) { return; } + + var node = placeholderTextNode, + index; + + placeholderTextNode = null; + + if ( node.parentNode ) { + while ( ( index = node.data.indexOf( '\u200B' ) ) > -1 ) { + node.deleteData( index, 1 ); + } + if ( !node.data && !node.nextSibling && !node.previousSibling && + isInline( node.parentNode ) ) { + detach( node.parentNode ); + } + } +}; + +var setPlaceholderTextNode = function ( node ) { + if ( placeholderTextNode ) { + mayRemovePlaceholder = true; + removePlaceholderTextNode(); + } + if ( !willEnablePlaceholderRemoval ) { + setTimeout( enablePlaceholderRemoval, 0 ); + willEnablePlaceholderRemoval = true; + } + mayRemovePlaceholder = false; + placeholderTextNode = node; +}; + +// --- Path change events --- + +var lastAnchorNode; +var lastFocusNode; +var path = ''; + +var updatePath = function ( range, force ) { + if ( placeholderTextNode && !force ) { + removePlaceholderTextNode( range ); + } + var anchor = range.startContainer, + focus = range.endContainer, + newPath; + if ( force || anchor !== lastAnchorNode || focus !== lastFocusNode ) { + lastAnchorNode = anchor; + lastFocusNode = focus; + newPath = ( anchor && focus ) ? ( anchor === focus ) ? + getPath( focus ) : '(selection)' : ''; + if ( path !== newPath ) { + path = newPath; + fireEvent( 'pathChange', { path: newPath } ); + } + } + if ( anchor !== focus ) { + fireEvent( 'select' ); + } +}; +var updatePathOnEvent = function () { + updatePath( getSelection() ); +}; +addEventListener( 'keyup', updatePathOnEvent ); +addEventListener( 'mouseup', updatePathOnEvent ); + +// --- Focus --- + +var focus = function () { + // FF seems to need the body to be focussed + // (at least on first load). + if ( isGecko ) { + body.focus(); + } + win.focus(); +}; + +var 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 ) { + body.blur(); + } + top.focus(); +}; + +win.addEventListener( 'focus', propagateEvent, false ); +win.addEventListener( 'blur', propagateEvent, false ); + +// --- Get/Set data --- + +var getHTML = function () { + return body.innerHTML; +}; + +var setHTML = function ( html ) { + var node = body; + node.innerHTML = html; + do { + fixCursor( node ); + } while ( node = getNextBlock( node ) ); +}; + +var insertElement = function ( el, range ) { + if ( !range ) { range = getSelection(); } + range.collapse( true ); + range._insertNode( el ); + range.setStartAfter( el ); + setSelection( range ); + updatePath( range ); +}; + +// --- Bookmarking --- + +var startSelectionId = 'squire-selection-start'; +var endSelectionId = 'squire-selection-end'; + +var saveRangeToBookmark = function ( range ) { + var startNode = createElement( 'INPUT', { + id: startSelectionId, + type: 'hidden' + }), + endNode = createElement( 'INPUT', { + id: endSelectionId, + type: 'hidden' + }), + temp; + + range._insertNode( startNode ); + range.collapse( false ); + range._insertNode( 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; } - // --- Workaround for browsers that can't focus empty text nodes --- + range.setStartAfter( startNode ); + range.setEndBefore( endNode ); +}; - // WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=15256 +var getRangeAndRemoveBookmark = function ( range ) { + var start = doc.getElementById( startSelectionId ), + end = doc.getElementById( endSelectionId ); - var placeholderTextNode = null; - var mayRemovePlaceholder = true; - var willEnablePlaceholderRemoval = false; + if ( start && end ) { + var startContainer = start.parentNode, + endContainer = end.parentNode, + collapsed; - var enablePlaceholderRemoval = function () { - mayRemovePlaceholder = true; - willEnablePlaceholderRemoval = false; - }; - - var setPlaceholderTextNode = function ( node ) { - if ( placeholderTextNode ) { - mayRemovePlaceholder = true; - removePlaceholderTextNode(); - } - if ( !willEnablePlaceholderRemoval ) { - setTimeout( enablePlaceholderRemoval, 0 ); - willEnablePlaceholderRemoval = true; - } - mayRemovePlaceholder = false; - placeholderTextNode = node; - }; - - var removePlaceholderTextNode = function () { - if ( !mayRemovePlaceholder ) { return; } - - var node = placeholderTextNode, - index; - - placeholderTextNode = null; - - if ( node.parentNode ) { - while ( ( index = node.data.indexOf( '\u200B' ) ) > -1 ) { - node.deleteData( index, 1 ); - } - if ( !node.data && !node.nextSibling && !node.previousSibling && - node.parentNode.isInline() ) { - node.parentNode.detach(); - } - } - }; - - // --- Path change events --- - - var lastAnchorNode; - var lastFocusNode; - var path = ''; - - var updatePath = function ( range, force ) { - if ( placeholderTextNode && !force ) { - removePlaceholderTextNode( range ); - } - var anchor = range.startContainer, - focus = range.endContainer, - newPath; - if ( force || anchor !== lastAnchorNode || focus !== lastFocusNode ) { - lastAnchorNode = anchor; - lastFocusNode = focus; - newPath = ( anchor && focus ) ? ( anchor === focus ) ? - focus.getPath() : '(selection)' : ''; - if ( path !== newPath ) { - path = newPath; - fireEvent( 'pathChange', { path: newPath } ); - } - } - if ( anchor !== focus ) { - fireEvent( 'select' ); - } - }; - var updatePathOnEvent = function () { - updatePath( getSelection() ); - }; - addEventListener( 'keyup', updatePathOnEvent ); - addEventListener( 'mouseup', updatePathOnEvent ); - - // --- Focus --- - - var focus = function () { - // FF seems to need the body to be focussed - // (at least on first load). - if ( isGecko ) { - body.focus(); - } - win.focus(); - }; - - var 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 ) { - body.blur(); - } - top.focus(); - }; - - win.addEventListener( 'focus', propagateEvent, false ); - win.addEventListener( 'blur', propagateEvent, false ); - - // --- Get/Set data --- - - var getHTML = function () { - return body.innerHTML; - }; - - var setHTML = function ( html ) { - var node = body; - node.innerHTML = html; - do { - node.fixCursor(); - } while ( node = node.getNextBlock() ); - }; - - var insertElement = function ( el, range ) { - if ( !range ) { range = getSelection(); } - range.collapse( true ); - range._insertNode( el ); - range.setStartAfter( el ); - setSelection( range ); - updatePath( range ); - }; - - // --- Bookmarking --- - - var indexOf = Array.prototype.indexOf; - - var startSelectionId = 'squire-selection-start'; - var endSelectionId = 'squire-selection-end'; - - var saveRangeToBookmark = function ( range ) { - var startNode = createElement( 'INPUT', { - id: startSelectionId, - type: 'hidden' - }), - endNode = createElement( 'INPUT', { - id: endSelectionId, - type: 'hidden' - }), - temp; - - range._insertNode( startNode ); - range.collapse( false ); - range._insertNode( 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 ); - }; - - var getRangeAndRemoveBookmark = function ( range ) { - var 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; - } - - start.detach(); - end.detach(); - - // Merge any text nodes we split - startContainer.mergeInlines( _range ); - if ( startContainer !== endContainer ) { - endContainer.mergeInlines( _range ); - } - - if ( !range ) { - range = doc.createRange(); - } - range.setStart( _range.startContainer, _range.startOffset ); - range.setEnd( _range.endContainer, _range.endOffset ); - collapsed = range.collapsed; - - range.moveBoundariesDownTree(); - if ( collapsed ) { - range.collapse( true ); - } - } - return range || null; - }; - - // --- Undo --- - - // These values are initialised in the editor.setHTML method, - // which is always called on initialisation. - var undoIndex; // = -1, - var undoStack; // = [], - var undoStackLength; // = 0, - var isInUndoState; // = false, - var docWasChanged = function () { - if ( isInUndoState ) { - isInUndoState = false; - fireEvent( 'undoStateChange', { - canUndo: true, - canRedo: false - }); - } - fireEvent( 'input' ); - }; - - addEventListener( 'keyup', 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 ) ) { - docWasChanged(); - } - }); - - // Leaves bookmark - var recordUndoState = function ( range ) { - // Don't record if we're already in an undo state - if ( !isInUndoState ) { - // Advance pointer to new position - undoIndex += 1; - - // Truncate stack if longer (i.e. if has been previously undone) - if ( undoIndex < undoStackLength) { - undoStack.length = undoStackLength = undoIndex; - } - - // Write out data - if ( range ) { - saveRangeToBookmark( range ); - } - undoStack[ undoIndex ] = getHTML(); - undoStackLength += 1; - isInUndoState = true; - } - }; - - var undo = function () { - // Sanity check: must not be at beginning of the history stack - if ( undoIndex !== 0 || !isInUndoState ) { - // Make sure any changes since last checkpoint are saved. - recordUndoState( getSelection() ); - - undoIndex -= 1; - setHTML( undoStack[ undoIndex ] ); - var range = getRangeAndRemoveBookmark(); - if ( range ) { - setSelection( range ); - } - isInUndoState = true; - fireEvent( 'undoStateChange', { - canUndo: undoIndex !== 0, - canRedo: true - }); - fireEvent( 'input' ); - } - }; - - var redo = function () { - // Sanity check: must not be at end of stack and must be in an undo - // state. - if ( undoIndex + 1 < undoStackLength && isInUndoState ) { - undoIndex += 1; - setHTML( undoStack[ undoIndex ] ); - var range = getRangeAndRemoveBookmark(); - if ( range ) { - setSelection( range ); - } - fireEvent( 'undoStateChange', { - canUndo: true, - canRedo: undoIndex + 1 < undoStackLength - }); - fireEvent( 'input' ); - } - }; - - // --- Inline formatting --- - - // Looks for matching tag and attributes, so won't work - // if instead of etc. - var hasFormat = function ( tag, attributes, range ) { - // 1. Normalise the arguments and get selection - tag = tag.toUpperCase(); - if ( !attributes ) { attributes = {}; } - if ( !range && !( range = 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 ( root.nearest( 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 range.containsNode( node, true ) ? - FILTER_ACCEPT : FILTER_SKIP; - }, false ); - - var seenNode = false; - while ( node = walker.nextNode() ) { - if ( !node.nearest( tag, attributes ) ) { - return false; - } - seenNode = true; - } - - return seenNode; - }; - - var addFormat = function ( tag, attributes, range ) { - // If the range is collapsed we simply insert the node by wrapping - // it round the range and focus it. - if ( range.collapsed ) { - var el = createElement( tag, attributes ).fixCursor(); - range._insertNode( 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. - var walker = new TreeWalker( - range.commonAncestorContainer, - SHOW_TEXT, - function ( node ) { - return range.containsNode( 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. - var startContainer, - endContainer, - startOffset = 0, - endOffset = 0, - textnode = walker.currentNode = range.startContainer, - needsFormat; - - if ( textnode.nodeType !== TEXT_NODE ) { - textnode = walker.nextNode(); - } - - do { - needsFormat = !textnode.nearest( 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 ) { - createElement( tag, attributes ).wraps( textnode ); - endOffset = textnode.length; - } - endContainer = textnode; - if ( !startContainer ) { startContainer = endContainer; } - } while ( textnode = walker.nextNode() ); - - // Now set the selection to as it was before - range = createRange( - startContainer, startOffset, endContainer, endOffset ); - } - return range; - }; - - var removeFormat = function ( tag, attributes, range, partial ) { - // Add bookmark - saveRangeToBookmark( range ); - - // We need a node in the selection to break the surrounding - // formatted text. - var fixer; - if ( range.collapsed ) { - if ( cantFocusEmptyTextNodes ) { - fixer = doc.createTextNode( '\u200B' ); - setPlaceholderTextNode( fixer ); - } else { - fixer = doc.createTextNode( '' ); - } - range._insertNode( fixer ); - } - - // Find block-level ancestor of selection - var root = range.commonAncestorContainer; - while ( root.isInline() ) { - 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 ( range.containsNode( 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 ( !range.containsNode( 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 range.containsNode( el, true ) && - el.is( 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 - item[0].cloneNode( false ).wraps( item[1] ); - }); - // and remove old formatting tags. - formatTags.forEach( function ( el ) { - el.replaceWith( el.empty() ); - }); - - // Merge adjacent inlines: - getRangeAndRemoveBookmark( range ); - if ( fixer ) { - range.collapse( false ); - } var _range = { - startContainer: range.startContainer, - startOffset: range.startOffset, - endContainer: range.endContainer, - endOffset: range.endOffset + startContainer: startContainer, + endContainer: endContainer, + startOffset: indexOf.call( startContainer.childNodes, start ), + endOffset: indexOf.call( endContainer.childNodes, end ) }; - root.mergeInlines( _range ); + + 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; - return range; - }; + range.moveBoundariesDownTree(); + if ( collapsed ) { + range.collapse( true ); + } + } + return range || null; +}; - var changeFormat = function ( add, remove, range, partial ) { - // Normalise the arguments and get selection - if ( !range && !( range = getSelection() ) ) { - return; +// --- Undo --- + +// These values are initialised in the editor.setHTML method, +// which is always called on initialisation. +var undoIndex; // = -1, +var undoStack; // = [], +var undoStackLength; // = 0, +var isInUndoState; // = false, +var docWasChanged = function () { + if ( isInUndoState ) { + isInUndoState = false; + fireEvent( 'undoStateChange', { + canUndo: true, + canRedo: false + }); + } + fireEvent( 'input' ); +}; + +addEventListener( 'keyup', 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 ) ) { + docWasChanged(); + } +}); + +// Leaves bookmark +var recordUndoState = function ( range ) { + // Don't record if we're already in an undo state + if ( !isInUndoState ) { + // Advance pointer to new position + undoIndex += 1; + + // Truncate stack if longer (i.e. if has been previously undone) + if ( undoIndex < undoStackLength) { + undoStack.length = undoStackLength = undoIndex; } - // Save undo checkpoint + // Write out data + if ( range ) { + saveRangeToBookmark( range ); + } + undoStack[ undoIndex ] = getHTML(); + undoStackLength += 1; + isInUndoState = true; + } +}; + +var undo = function () { + // Sanity check: must not be at beginning of the history stack + if ( undoIndex !== 0 || !isInUndoState ) { + // Make sure any changes since last checkpoint are saved. + recordUndoState( getSelection() ); + + undoIndex -= 1; + setHTML( undoStack[ undoIndex ] ); + var range = getRangeAndRemoveBookmark(); + if ( range ) { + setSelection( range ); + } + isInUndoState = true; + fireEvent( 'undoStateChange', { + canUndo: undoIndex !== 0, + canRedo: true + }); + fireEvent( 'input' ); + } +}; + +var redo = function () { + // Sanity check: must not be at end of stack and must be in an undo + // state. + if ( undoIndex + 1 < undoStackLength && isInUndoState ) { + undoIndex += 1; + setHTML( undoStack[ undoIndex ] ); + var range = getRangeAndRemoveBookmark(); + if ( range ) { + setSelection( range ); + } + fireEvent( 'undoStateChange', { + canUndo: true, + canRedo: undoIndex + 1 < undoStackLength + }); + fireEvent( 'input' ); + } +}; + +// --- Inline formatting --- + +// Looks for matching tag and attributes, so won't work +// if instead of etc. +var hasFormat = function ( tag, attributes, range ) { + // 1. Normalise the arguments and get selection + tag = tag.toUpperCase(); + if ( !attributes ) { attributes = {}; } + if ( !range && !( range = 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 range.containsNode( 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; +}; + +var 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( createElement( tag, attributes ) ); + range._insertNode( 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 range.containsNode( 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 = 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 = createRange( + startContainer, startOffset, endContainer, endOffset ); + } + return range; +}; + +var removeFormat = function ( tag, attributes, range, partial ) { + // Add bookmark + saveRangeToBookmark( range ); + + // We need a node in the selection to break the surrounding + // formatted text. + var fixer; + if ( range.collapsed ) { + if ( cantFocusEmptyTextNodes ) { + fixer = doc.createTextNode( '\u200B' ); + setPlaceholderTextNode( fixer ); + } else { + fixer = doc.createTextNode( '' ); + } + range._insertNode( 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 ( range.containsNode( 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 ( !range.containsNode( 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 range.containsNode( 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: + 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; +}; + +var changeFormat = function ( add, remove, range, partial ) { + // Normalise the arguments and get selection + if ( !range && !( range = getSelection() ) ) { + return; + } + + // Save undo checkpoint + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + + if ( remove ) { + range = removeFormat( remove.tag.toUpperCase(), + remove.attributes || {}, range, partial ); + } + if ( add ) { + range = addFormat( add.tag.toUpperCase(), + add.attributes || {}, range ); + } + + setSelection( range ); + updatePath( range, true ); + + // We're not still in an undo state + docWasChanged(); +}; + +// --- 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( splitTag ); + block.className = nodeAfterSplit.dir === 'rtl' ? 'dir-rtl' : ''; + block.dir = nodeAfterSplit.dir; + replaceWith( nodeAfterSplit, block ); + block.appendChild( empty( nodeAfterSplit ) ); + nodeAfterSplit = block; + } + return nodeAfterSplit; +}; + +var forEachBlock = function ( fn, mutates, range ) { + if ( !range && !( range = getSelection() ) ) { + return; + } + + // Save undo checkpoint + if ( mutates ) { recordUndoState( range ); getRangeAndRemoveBookmark( range ); + } - if ( remove ) { - range = removeFormat( remove.tag.toUpperCase(), - remove.attributes || {}, range, partial ); - } - if ( add ) { - range = addFormat( add.tag.toUpperCase(), - add.attributes || {}, range ); - } + var start = range.getStartBlock(), + end = range.getEndBlock(); + if ( start && end ) { + do { + if ( fn( start ) || start === end ) { break; } + } while ( start = getNextBlock( start ) ); + } + if ( mutates ) { setSelection( range ); + + // Path may have changed updatePath( range, true ); // We're not still in an undo state docWasChanged(); - }; + } +}; - // --- Block formatting --- +var modifyBlocks = function ( modify, range ) { + if ( !range && !( range = getSelection() ) ) { + return; + } + // 1. Stop firefox adding an extra
to + // if we remove everything. Don't want to do this in Opera + // as it can cause focus problems. + if ( !isOpera ) { + body.setAttribute( 'contenteditable', 'false' ); + } - 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' - }; + // 2. Save undo checkpoint and bookmark selection + if ( isInUndoState ) { + saveRangeToBookmark( range ); + } else { + recordUndoState( range ); + } - var splitBlock = function ( block, node, offset ) { - var splitTag = tagAfterSplit[ block.nodeName ], - nodeAfterSplit = node.split( offset, block.parentNode ); + // 3. Expand range to block boundaries + range.expandToBlockBoundaries(); - // Make sure the new node is the correct type. - if ( nodeAfterSplit.nodeName !== splitTag ) { - block = createElement( splitTag ); - block.className = nodeAfterSplit.dir === 'rtl' ? 'dir-rtl' : ''; - block.dir = nodeAfterSplit.dir; - block.replaces( nodeAfterSplit ) - .appendChild( nodeAfterSplit.empty() ); - nodeAfterSplit = block; - } - return nodeAfterSplit; - }; + // 4. Remove range. + range.moveBoundariesUpTree( body ); + var frag = range._extractContents( body ); - var forEachBlock = function ( fn, mutates, range ) { - if ( !range && !( range = getSelection() ) ) { - return; - } + // 5. Modify tree of fragment and reinsert. + range._insertNode( modify( frag ) ); - // Save undo checkpoint - if ( mutates ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - } + // 6. Merge containers at edges + if ( range.endOffset < range.endContainer.childNodes.length ) { + mergeContainers( range.endContainer.childNodes[ range.endOffset ] ); + } + mergeContainers( range.startContainer.childNodes[ range.startOffset ] ); - var start = range.getStartBlock(), - end = range.getEndBlock(); - if ( start && end ) { - do { - if ( fn( start ) || start === end ) { break; } - } while ( start = start.getNextBlock() ); - } + // 7. Make it editable again + if ( !isOpera ) { + body.setAttribute( 'contenteditable', 'true' ); + } - if ( mutates ) { - setSelection( range ); + // 8. Restore selection + getRangeAndRemoveBookmark( range ); + setSelection( range ); + updatePath( range, true ); - // Path may have changed - updatePath( range, true ); + // 9. We're not still in an undo state + docWasChanged(); +}; - // We're not still in an undo state - docWasChanged(); - } - }; +var increaseBlockQuoteLevel = function ( frag ) { + return createElement( 'BLOCKQUOTE', [ + frag + ]); +}; - var modifyBlocks = function ( modify, range ) { - if ( !range && !( range = getSelection() ) ) { - return; - } - // 1. Stop firefox adding an extra
to - // if we remove everything. Don't want to do this in Opera - // as it can cause focus problems. - if ( !isOpera ) { - body.setAttribute( 'contenteditable', 'false' ); - } +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; +}; - // 2. Save undo checkpoint and bookmark selection - if ( isInUndoState ) { - saveRangeToBookmark( range ); - } else { - recordUndoState( range ); - } +var removeBlockQuote = function ( frag ) { + var blockquotes = frag.querySelectorAll( 'blockquote' ), + l = blockquotes.length, + bq; + while ( l-- ) { + bq = blockquotes[l]; + replaceWith( bq, empty( bq ) ); + } + return frag; +}; - // 3. Expand range to block boundaries - range.expandToBlockBoundaries(); - - // 4. Remove range. - range.moveBoundariesUpTree( body ); - var frag = range._extractContents( body ); - - // 5. Modify tree of fragment and reinsert. - range._insertNode( modify( frag ) ); - - // 6. Merge containers at edges - if ( range.endOffset < range.endContainer.childNodes.length ) { - range.endContainer.childNodes[ range.endOffset ].mergeContainers(); - } - range.startContainer.childNodes[ range.startOffset ].mergeContainers(); - - // 7. Make it editable again - if ( !isOpera ) { - body.setAttribute( 'contenteditable', 'true' ); - } - - // 8. Restore selection - getRangeAndRemoveBookmark( range ); - setSelection( range ); - updatePath( range, true ); - - // 9. We're not still in an undo state - docWasChanged(); - }; - - var increaseBlockQuoteLevel = function ( frag ) { - return createElement( 'BLOCKQUOTE', [ - frag - ]); - }; - - var decreaseBlockQuoteLevel = function ( frag ) { - var blockquotes = frag.querySelectorAll( 'blockquote' ); - Array.prototype.filter.call( blockquotes, function ( el ) { - return !el.parentNode.nearest( 'BLOCKQUOTE' ); - }).forEach( function ( el ) { - el.replaceWith( el.empty() ); - }); - return frag; - }; - - var removeBlockQuote = function ( frag ) { - var blockquotes = frag.querySelectorAll( 'blockquote' ), - l = blockquotes.length, - bq; - while ( l-- ) { - bq = blockquotes[l]; - bq.replaceWith( bq.empty() ); - } - return frag; - }; - - var makeList = function ( nodes, type ) { - var i, l, node, tag, prev, replacement; - for ( i = 0, l = nodes.length; i < l; i += 1 ) { - node = nodes[i]; - tag = node.nodeName; - if ( node.isBlock() ) { - if ( tag !== 'LI' ) { - replacement = createElement( 'LI', { - 'class': node.dir === 'rtl' ? 'dir-rtl' : '', - dir: node.dir - }, [ - node.empty() - ]); - if ( node.parentNode.nodeName === type ) { - node.replaceWith( replacement ); - } - else if ( ( prev = node.previousSibling ) && - prev.nodeName === type ) { - prev.appendChild( replacement ); - node.detach(); - i -= 1; - l -= 1; - } - else { - node.replaceWith( - createElement( type, [ - replacement - ]) - ); - } +var makeList = function ( nodes, type ) { + var i, l, node, tag, prev, replacement; + for ( i = 0, l = nodes.length; i < l; i += 1 ) { + node = nodes[i]; + tag = node.nodeName; + if ( isBlock( node ) ) { + if ( tag !== 'LI' ) { + replacement = createElement( 'LI', { + 'class': node.dir === 'rtl' ? 'dir-rtl' : '', + dir: node.dir + }, [ + empty( node ) + ]); + if ( node.parentNode.nodeName === type ) { + replaceWith( node, replacement ); } - } else if ( node.isContainer() ) { - if ( tag !== type && ( /^[DOU]L$/.test( tag ) ) ) { - node.replaceWith( createElement( type, [ - node.empty() - ]) ); - } else { - makeList( node.childNodes, type ); + else if ( ( prev = node.previousSibling ) && + prev.nodeName === type ) { + prev.appendChild( replacement ); + detach( node ); + i -= 1; + l -= 1; + } + else { + replaceWith( + node, + createElement( type, [ + replacement + ]) + ); } } - } - }; - - var makeUnorderedList = function ( frag ) { - makeList( frag.childNodes, 'UL' ); - return frag; - }; - - var makeOrderedList = function ( frag ) { - makeList( frag.childNodes, 'OL' ); - return frag; - }; - - var decreaseListLevel = function ( frag ) { - var lists = frag.querySelectorAll( 'UL, OL' ); - Array.prototype.filter.call( lists, function ( el ) { - return !el.parentNode.nearest( 'UL' ) && - !el.parentNode.nearest( 'OL' ); - }).forEach( function ( el ) { - var frag = el.empty(), - children = frag.childNodes, - l = children.length, - child; - while ( l-- ) { - child = children[l]; - if ( child.nodeName === 'LI' ) { - frag.replaceChild( createElement( 'DIV', { - 'class': child.dir === 'rtl' ? 'dir-rtl' : '', - dir: child.dir - }, [ - child.empty() - ]), child ); - } - } - el.replaceWith( frag ); - }); - return frag; - }; - - // --- Clean --- - - var linkRegExp = /\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])|(?:[\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,4}))/i; - - var addLinks = function ( frag ) { - var doc = frag.ownerDocument, - walker = new TreeWalker( frag, SHOW_TEXT, - function ( node ) { - return node.nearest( 'A' ) ? FILTER_SKIP : FILTER_ACCEPT; - }, false ), - node, parts, i, l, text, parent, next; - while ( node = walker.nextNode() ) { - parts = node.data.split( linkRegExp ); - l = parts.length; - if ( l > 1 ) { - parent = node.parentNode; - next = node.nextSibling; - for ( i = 0; i < l; i += 1 ) { - text = parts[i]; - if ( i ) { - if ( i % 2 ) { - node = doc.createElement( 'A' ); - node.textContent = text; - node.href = /@/.test( text ) ? 'mailto:' + text : - /^(?:ht|f)tps?:/.test( text ) ? - text : 'http://' + text; - } else { - node = doc.createTextNode( text ); - } - if ( next ) { - parent.insertBefore( node, next ); - } else { - parent.appendChild( node ); - } - } else { - node.data = text; - } - } - walker.currentNode = node; + } else if ( isContainer( node ) ) { + if ( tag !== type && ( /^[DOU]L$/.test( tag ) ) ) { + replaceWith( node, createElement( type, [ empty( node ) ] ) ); + } else { + makeList( node.childNodes, type ); } } - }; + } +}; - var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/; +var makeUnorderedList = function ( frag ) { + makeList( frag.childNodes, 'UL' ); + return frag; +}; - var fontSizes = { - 1: 10, - 2: 13, - 3: 16, - 4: 18, - 5: 24, - 6: 32, - 7: 48 - }; +var makeOrderedList = function ( frag ) { + makeList( frag.childNodes, 'OL' ); + return frag; +}; - var spanToSemantic = { - backgroundColor: { - regexp: notWS, - replace: function ( colour ) { - return createElement( 'SPAN', { - 'class': 'highlight', - style: 'background-color: ' + colour - }); - } - }, - color: { - regexp: notWS, - replace: function ( colour ) { - return createElement( 'SPAN', { - 'class': 'colour', - style: 'color:' + colour - }); - } - }, - fontWeight: { - regexp: /^bold/i, - replace: function () { - return createElement( 'B' ); - } - }, - fontStyle: { - regexp: /^italic/i, - replace: function () { - return createElement( 'I' ); - } - }, - fontFamily: { - regexp: notWS, - replace: function ( family ) { - return createElement( 'SPAN', { - 'class': 'font', - style: 'font-family:' + family - }); - } - }, - fontSize: { - regexp: notWS, - replace: function ( size ) { - return createElement( 'SPAN', { - 'class': 'size', - style: 'font-size:' + size - }); - } - } - }; - - var stylesRewriters = { - SPAN: function ( span, parent ) { - var style = span.style, - attr, converter, css, newTreeBottom, newTreeTop, el; - - for ( attr in spanToSemantic ) { - converter = spanToSemantic[ attr ]; - css = style[ attr ]; - if ( css && converter.regexp.test( css ) ) { - el = converter.replace( css ); - if ( newTreeBottom ) { - newTreeBottom.appendChild( el ); - } - newTreeBottom = el; - if ( !newTreeTop ) { - newTreeTop = el; - } - } - } - - if ( newTreeTop ) { - newTreeBottom.appendChild( span.empty() ); - parent.replaceChild( newTreeTop, span ); - } - - return newTreeBottom || span; - }, - STRONG: function ( node, parent ) { - var el = createElement( 'B' ); - parent.replaceChild( el, node ); - el.appendChild( node.empty() ); - return el; - }, - EM: function ( node, parent ) { - var el = createElement( 'I' ); - parent.replaceChild( el, node ); - el.appendChild( node.empty() ); - return el; - }, - FONT: function ( node, parent ) { - var face = node.face, - size = node.size, - fontSpan, sizeSpan, - newTreeBottom, newTreeTop; - if ( face ) { - fontSpan = createElement( 'SPAN', { - 'class': 'font', - style: 'font-family:' + face - }); - } - if ( size ) { - sizeSpan = createElement( 'SPAN', { - 'class': 'size', - style: 'font-size:' + fontSizes[ size ] + 'px' - }); - if ( fontSpan ) { - fontSpan.appendChild( sizeSpan ); - } - } - newTreeTop = fontSpan || sizeSpan || createElement( 'SPAN' ); - newTreeBottom = sizeSpan || fontSpan || newTreeTop; - parent.replaceChild( newTreeTop, node ); - newTreeBottom.appendChild( node.empty() ); - return newTreeBottom; - }, - TT: function ( node, parent ) { - var el = createElement( 'SPAN', { - 'class': 'font', - style: 'font-family:menlo,consolas,"courier new",monospace' - }); - parent.replaceChild( el, node ); - el.appendChild( node.empty() ); - return el; - } - }; - - var removeEmptyInlines = function ( root ) { - var children = root.childNodes, +var decreaseListLevel = function ( frag ) { + var lists = frag.querySelectorAll( 'UL, OL' ); + Array.prototype.filter.call( lists, function ( el ) { + return !getNearest( el.parentNode, 'UL' ) && + !getNearest( el.parentNode, 'OL' ); + }).forEach( function ( el ) { + var frag = empty( el ), + children = frag.childNodes, l = children.length, child; while ( l-- ) { child = children[l]; - if ( child.nodeType === ELEMENT_NODE ) { - removeEmptyInlines( child ); - if ( child.isInline() && !child.firstChild ) { - root.removeChild( child ); - } + if ( child.nodeName === 'LI' ) { + frag.replaceChild( createElement( 'DIV', { + 'class': child.dir === 'rtl' ? 'dir-rtl' : '', + dir: child.dir + }, [ + empty( child ) + ]), child ); } } - }; + replaceWith( el, frag ); + }); + return frag; +}; - /* - Two purposes: +// --- Clean --- - 1. Remove nodes we don't want, such as weird tags, comment nodes - and whitespace nodes. - 2. Convert inline tags into our preferred format. - */ - var cleanTree = function ( node, allowStyles ) { - var children = node.childNodes, - i, l, child, nodeName, nodeType, rewriter, childLength; - for ( i = 0, l = children.length; i < l; i += 1 ) { - child = children[i]; - nodeName = child.nodeName; - nodeType = child.nodeType; - rewriter = stylesRewriters[ nodeName ]; - if ( nodeType === ELEMENT_NODE ) { - childLength = child.childNodes.length; - if ( rewriter ) { - child = rewriter( child, node ); - } else if ( !allowedBlock.test( nodeName ) && - !child.isInline() ) { - i -= 1; - l += childLength - 1; - node.replaceChild( child.empty(), child ); - continue; - } else if ( !allowStyles && child.style.cssText ) { - child.removeAttribute( 'style' ); - } - if ( childLength ) { - cleanTree( child, allowStyles ); - } - } else if ( nodeType !== TEXT_NODE || ( - !( notWS.test( child.data ) ) && - !( i > 0 && children[ i - 1 ].isInline() ) && - !( i + 1 < l && children[ i + 1 ].isInline() ) - ) ) { - node.removeChild( child ); - i -= 1; - l -= 1; - } - } - return node; - }; +var linkRegExp = /\b((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])|(?:[\w\-.%+]+@(?:[\w\-]+\.)+[A-Z]{2,4}))/i; - var wrapTopLevelInline = function ( root, tag ) { - var children = root.childNodes, - 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 && child.isInline() ) { - if ( !wrapper ) { wrapper = createElement( tag ); } - wrapper.appendChild( child ); - i -= 1; - l -= 1; - } else if ( isBR || wrapper ) { - if ( !wrapper ) { wrapper = createElement( tag ); } - wrapper.fixCursor(); - if ( isBR ) { - root.replaceChild( wrapper, child ); +var addLinks = function ( frag ) { + var doc = frag.ownerDocument, + walker = new TreeWalker( frag, SHOW_TEXT, + function ( node ) { + return getNearest( node, 'A' ) ? FILTER_SKIP : FILTER_ACCEPT; + }, false ), + node, parts, i, l, text, parent, next; + while ( node = walker.nextNode() ) { + parts = node.data.split( linkRegExp ); + l = parts.length; + if ( l > 1 ) { + parent = node.parentNode; + next = node.nextSibling; + for ( i = 0; i < l; i += 1 ) { + text = parts[i]; + if ( i ) { + if ( i % 2 ) { + node = doc.createElement( 'A' ); + node.textContent = text; + node.href = /@/.test( text ) ? 'mailto:' + text : + /^(?:ht|f)tps?:/.test( text ) ? + text : 'http://' + text; + } else { + node = doc.createTextNode( text ); + } + if ( next ) { + parent.insertBefore( node, next ); + } else { + parent.appendChild( node ); + } } else { - root.insertBefore( wrapper, child ); - i += 1; - l += 1; + node.data = text; + } + } + walker.currentNode = node; + } + } +}; + +var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|UL)$/; + +var fontSizes = { + 1: 10, + 2: 13, + 3: 16, + 4: 18, + 5: 24, + 6: 32, + 7: 48 +}; + +var spanToSemantic = { + backgroundColor: { + regexp: notWS, + replace: function ( colour ) { + return createElement( 'SPAN', { + 'class': 'highlight', + style: 'background-color: ' + colour + }); + } + }, + color: { + regexp: notWS, + replace: function ( colour ) { + return createElement( 'SPAN', { + 'class': 'colour', + style: 'color:' + colour + }); + } + }, + fontWeight: { + regexp: /^bold/i, + replace: function () { + return createElement( 'B' ); + } + }, + fontStyle: { + regexp: /^italic/i, + replace: function () { + return createElement( 'I' ); + } + }, + fontFamily: { + regexp: notWS, + replace: function ( family ) { + return createElement( 'SPAN', { + 'class': 'font', + style: 'font-family:' + family + }); + } + }, + fontSize: { + regexp: notWS, + replace: function ( size ) { + return createElement( 'SPAN', { + 'class': 'size', + style: 'font-size:' + size + }); + } + } +}; + +var stylesRewriters = { + SPAN: function ( span, parent ) { + var style = span.style, + attr, converter, css, newTreeBottom, newTreeTop, el; + + for ( attr in spanToSemantic ) { + converter = spanToSemantic[ attr ]; + css = style[ attr ]; + if ( css && converter.regexp.test( css ) ) { + el = converter.replace( css ); + if ( newTreeBottom ) { + newTreeBottom.appendChild( el ); + } + newTreeBottom = el; + if ( !newTreeTop ) { + newTreeTop = el; } - wrapper = null; } } - if ( wrapper ) { - root.appendChild( wrapper.fixCursor() ); - } - return root; - }; - var notWSTextNode = function ( node ) { - return ( node.nodeType === ELEMENT_NODE ? - node.nodeName === 'BR' : - notWS.test( node.data ) ) ? - FILTER_ACCEPT : FILTER_SKIP; - }; - var isLineBreak = function ( br ) { - var block = br.parentNode, - walker; - while ( block.isInline() ) { + if ( newTreeTop ) { + newTreeBottom.appendChild( empty( span ) ); + parent.replaceChild( newTreeTop, span ); + } + + return newTreeBottom || span; + }, + STRONG: function ( node, parent ) { + var el = createElement( 'B' ); + parent.replaceChild( el, node ); + el.appendChild( empty( node ) ); + return el; + }, + EM: function ( node, parent ) { + var el = createElement( 'I' ); + parent.replaceChild( el, node ); + el.appendChild( empty( node ) ); + return el; + }, + FONT: function ( node, parent ) { + var face = node.face, + size = node.size, + fontSpan, sizeSpan, + newTreeBottom, newTreeTop; + if ( face ) { + fontSpan = createElement( 'SPAN', { + 'class': 'font', + style: 'font-family:' + face + }); + } + if ( size ) { + sizeSpan = createElement( 'SPAN', { + 'class': 'size', + style: 'font-size:' + fontSizes[ size ] + 'px' + }); + if ( fontSpan ) { + fontSpan.appendChild( sizeSpan ); + } + } + newTreeTop = fontSpan || sizeSpan || createElement( 'SPAN' ); + newTreeBottom = sizeSpan || fontSpan || newTreeTop; + parent.replaceChild( newTreeTop, node ); + newTreeBottom.appendChild( empty( node ) ); + return newTreeBottom; + }, + TT: function ( node, parent ) { + var el = createElement( 'SPAN', { + 'class': 'font', + style: 'font-family:menlo,consolas,"courier new",monospace' + }); + parent.replaceChild( el, node ); + el.appendChild( empty( node ) ); + return el; + } +}; + +var removeEmptyInlines = function ( root ) { + var children = root.childNodes, + l = children.length, + child; + while ( l-- ) { + child = children[l]; + if ( child.nodeType === ELEMENT_NODE ) { + removeEmptyInlines( child ); + if ( isInline( child ) && !child.firstChild ) { + root.removeChild( child ); + } + } + } +}; + +/* + Two purposes: + + 1. Remove nodes we don't want, such as weird tags, comment nodes + and whitespace nodes. + 2. Convert inline tags into our preferred format. +*/ +var cleanTree = function ( node, allowStyles ) { + var children = node.childNodes, + i, l, child, nodeName, nodeType, rewriter, childLength; + for ( i = 0, l = children.length; i < l; i += 1 ) { + child = children[i]; + nodeName = child.nodeName; + nodeType = child.nodeType; + rewriter = stylesRewriters[ nodeName ]; + if ( nodeType === ELEMENT_NODE ) { + childLength = child.childNodes.length; + if ( rewriter ) { + child = rewriter( child, node ); + } else if ( !allowedBlock.test( nodeName ) && + !isInline( child ) ) { + i -= 1; + l += childLength - 1; + node.replaceChild( empty( child ), child ); + continue; + } else if ( !allowStyles && child.style.cssText ) { + child.removeAttribute( 'style' ); + } + if ( childLength ) { + cleanTree( child, allowStyles ); + } + } else if ( nodeType !== TEXT_NODE || ( + !( notWS.test( child.data ) ) && + !( i > 0 && isInline( children[ i - 1 ] ) ) && + !( i + 1 < l && isInline( children[ i + 1 ] ) ) + ) ) { + node.removeChild( child ); + i -= 1; + l -= 1; + } + } + return node; +}; + +var wrapTopLevelInline = function ( root, tag ) { + var children = root.childNodes, + 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( tag ); } + wrapper.appendChild( child ); + i -= 1; + l -= 1; + } else if ( isBR || wrapper ) { + if ( !wrapper ) { wrapper = createElement( tag ); } + fixCursor( wrapper ); + if ( isBR ) { + root.replaceChild( wrapper, child ); + } else { + root.insertBefore( wrapper, child ); + i += 1; + l += 1; + } + wrapper = null; + } + } + if ( wrapper ) { + root.appendChild( fixCursor( wrapper ) ); + } + return root; +}; + +var notWSTextNode = function ( node ) { + return ( node.nodeType === ELEMENT_NODE ? + node.nodeName === 'BR' : + notWS.test( node.data ) ) ? + FILTER_ACCEPT : FILTER_SKIP; +}; +var isLineBreak = function ( br ) { + var block = br.parentNode, + walker; + while ( isInline( block ) ) { + block = block.parentNode; + } + walker = new TreeWalker( + block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode ); + walker.currentNode = br; + return !!walker.nextNode(); +}; + +//
elements are treated specially, and differently depending on the +// browser, when in rich text editor mode. When adding HTML from external +// sources, we must remove them, replacing the ones that actually affect +// line breaks with a split of the block element containing it (and wrapping +// any not inside a block). Browsers that want
elements at the end of +// each block will then have them added back in a later fixCursor method +// call. +var cleanupBRs = function ( root ) { + var brs = root.querySelectorAll( 'BR' ), + brBreaksLine = [], + l = brs.length, + i, br, block; + + // Must calculate whether the
breaks a line first, because if we + // have two
s next to each other, after the first one is converted + // to a block split, the second will be at the end of a block and + // therefore seem to not be a line break. But in its original context it + // was, so we should also convert it to a block split. + for ( i = 0; i < l; i += 1 ) { + brBreaksLine[i] = isLineBreak( brs[i] ); + } + while ( l-- ) { + br = brs[l]; + // Cleanup may have removed it + block = br.parentNode; + if ( !block ) { continue; } + while ( isInline( block ) ) { block = block.parentNode; } - walker = new TreeWalker( - block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode ); - walker.currentNode = br; - return !!walker.nextNode(); - }; - - //
elements are treated specially, and differently depending on the - // browser, when in rich text editor mode. When adding HTML from external - // sources, we must remove them, replacing the ones that actually affect - // line breaks with a split of the block element containing it (and wrapping - // any not inside a block). Browsers that want
elements at the end of - // each block will then have them added back in a later fixCursor method - // call. - var cleanupBRs = function ( root ) { - var brs = root.querySelectorAll( 'BR' ), - brBreaksLine = [], - l = brs.length, - i, br, block; - - // Must calculate whether the
breaks a line first, because if we - // have two
s next to each other, after the first one is converted - // to a block split, the second will be at the end of a block and - // therefore seem to not be a line break. But in its original context it - // was, so we should also convert it to a block split. - for ( i = 0; i < l; i += 1 ) { - brBreaksLine[i] = isLineBreak( brs[i] ); + // If this is not inside a block, replace it by wrapping + // inlines in DIV. + if ( !isBlock( block ) || !tagAfterSplit[ block.nodeName ] ) { + wrapTopLevelInline( block, 'DIV' ); } - while ( l-- ) { - br = brs[l]; - // Cleanup may have removed it - block = br.parentNode; - if ( !block ) { continue; } - while ( block.isInline() ) { - block = block.parentNode; - } - // If this is not inside a block, replace it by wrapping - // inlines in DIV. - if ( !block.isBlock() || !tagAfterSplit[ block.nodeName ] ) { - wrapTopLevelInline( block, 'DIV' ); - } - // If in a block we can split, split it instead, but only if there - // is actual text content in the block. Otherwise, the
is a - // placeholder to stop the block from collapsing, so we must leave - // it. - else { - if ( brBreaksLine[l] ) { - splitBlock( block, br.parentNode, br ); - } - br.detach(); + // If in a block we can split, split it instead, but only if there + // is actual text content in the block. Otherwise, the
is a + // placeholder to stop the block from collapsing, so we must leave + // it. + else { + if ( brBreaksLine[l] ) { + splitBlock( block, br.parentNode, br ); } + detach( br ); } - }; - - // --- Cut and Paste --- - - var afterCut = function () { - try { - // If all content removed, ensure div at start of body. - body.fixCursor(); - } catch ( error ) { - editor.didError( error ); - } - }; - - addEventListener( isIE ? 'beforecut' : 'cut', function () { - // Save undo checkpoint - var range = getSelection(); - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); - setTimeout( afterCut, 0 ); - }); - - // IE sometimes fires the beforepaste event twice; make sure it is not run - // again before our after paste function is called. - var awaitingPaste = false; - - addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { - if ( awaitingPaste ) { return; } - - // Treat image paste as a drop of an image file. - var clipboardData = event.clipboardData, - items = clipboardData && clipboardData.items, - fireDrop = false, - l; - if ( items ) { - l = items.length; - while ( l-- ) { - if ( /^image\/.*/.test( items[l].type ) ) { - event.preventDefault(); - fireEvent( 'dragover', { - dataTransfer: clipboardData, - /*jshint loopfunc: true */ - preventDefault: function () { - fireDrop = true; - } - /*jshint loopfunc: false */ - }); - if ( fireDrop ) { - fireEvent( 'drop', { - dataTransfer: clipboardData - }); - } - return; - } - } - } - - awaitingPaste = true; - - var range = getSelection(), - startContainer = range.startContainer, - startOffset = range.startOffset, - endContainer = range.endContainer, - endOffset = range.endOffset; - - var pasteArea = createElement( 'DIV', { - style: 'position: absolute; overflow: hidden; top:' + - (body.scrollTop + 30) + 'px; left: 0; width: 1px; height: 1px;' - }); - body.appendChild( pasteArea ); - range.selectNodeContents( pasteArea ); - setSelection( range ); - - // A setTimeout of 0 means this is added to the back of the - // single javascript thread, so it will be executed after the - // paste event. - setTimeout( function () { - try { - // Get the pasted content and clean - var frag = pasteArea.detach().empty(), - first = frag.firstChild, - range = createRange( - startContainer, startOffset, endContainer, endOffset ); - - // Was anything actually pasted? - if ( first ) { - // Safari and IE like putting extra divs around things. - if ( first === frag.lastChild && - first.nodeName === 'DIV' ) { - frag.replaceChild( first.empty(), first ); - } - - frag.normalize(); - addLinks( frag ); - cleanTree( frag, false ); - cleanupBRs( frag ); - removeEmptyInlines( frag ); - - var node = frag, - doPaste = true; - while ( node = node.getNextBlock() ) { - node.fixCursor(); - } - - fireEvent( 'willPaste', { - fragment: frag, - preventDefault: function () { - doPaste = false; - } - }); - - // Insert pasted data - if ( doPaste ) { - range.insertTreeFragment( frag ); - docWasChanged(); - - range.collapse( false ); - } - } - - setSelection( range ); - updatePath( range, true ); - - awaitingPaste = false; - } catch ( error ) { - editor.didError( error ); - } - }, 0 ); - }); - - // --- Keyboard interaction --- - - var keys = { - 8: 'backspace', - 9: 'tab', - 13: 'enter', - 32: 'space', - 46: 'delete' - }; - - var mapKeyTo = function ( fn ) { - return function ( event ) { - event.preventDefault(); - fn(); - }; - }; - - var mapKeyToFormat = function ( tag ) { - return function ( event ) { - event.preventDefault(); - var range = getSelection(); - if ( hasFormat( tag, null, range ) ) { - changeFormat( null, { tag: tag }, range ); - } else { - changeFormat( { tag: tag }, null, range ); - } - }; - }; - - // If you delete the content inside a span with a font styling, Webkit will - // replace it with a tag (!). If you delete all the text inside a - // link in Opera, it won't delete the link. Let's make things consistent. If - // you delete all text inside an inline tag, remove the inline tag. - var afterDelete = function () { - try { - var range = getSelection(), - node = range.startContainer, - parent; - if ( node.nodeType === TEXT_NODE ) { - node = node.parentNode; - } - // If focussed in empty inline element - if ( node.isInline() && !node.textContent ) { - do { - parent = node.parentNode; - } while ( parent.isInline() && - !parent.textContent && ( node = parent ) ); - range.setStart( parent, - indexOf.call( parent.childNodes, node ) ); - range.collapse( true ); - parent.removeChild( node ); - if ( !parent.isBlock() ) { - parent = parent.getPreviousBlock(); - } - parent.fixCursor(); - range.moveBoundariesDownTree(); - setSelection( range ); - updatePath( range ); - } - } catch ( error ) { - editor.didError( error ); - } - }; - - // If you select all in IE8 then type, it makes a P; replace it with - // a DIV. - if ( isIE8 ) { - addEventListener( 'keyup', function () { - var firstChild = body.firstChild; - if ( firstChild.nodeName === 'P' ) { - saveRangeToBookmark( getSelection() ); - firstChild.replaceWith( createElement( 'DIV', [ - firstChild.empty() - ]) ); - setSelection( getRangeAndRemoveBookmark() ); - } - }); } +}; - var keyHandlers = { - enter: function ( event ) { - // We handle this ourselves - event.preventDefault(); +// --- Cut and Paste --- - // Must have some form of selection - var range = getSelection(); - if ( !range ) { return; } +var afterCut = function () { + try { + // If all content removed, ensure div at start of body. + fixCursor( body ); + } catch ( error ) { + editor.didError( error ); + } +}; - // Save undo checkpoint and add any links in the preceding section. - recordUndoState( range ); - addLinks( range.startContainer ); - getRangeAndRemoveBookmark( range ); +addEventListener( isIE ? 'beforecut' : 'cut', function () { + // Save undo checkpoint + var range = getSelection(); + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + setSelection( range ); + setTimeout( afterCut, 0 ); +}); - // Selected text is overwritten, therefore delete the contents - // to collapse selection. - if ( !range.collapsed ) { - range._deleteContents(); - } +// IE sometimes fires the beforepaste event twice; make sure it is not run +// again before our after paste function is called. +var awaitingPaste = false; - var block = range.getStartBlock(), - tag = block ? block.nodeName : 'DIV', - splitTag = tagAfterSplit[ tag ], - nodeAfterSplit; +addEventListener( isIE ? 'beforepaste' : 'paste', function ( event ) { + if ( awaitingPaste ) { return; } - // If this is a malformed bit of document, just play it safe - // and insert a
. - if ( !block ) { - range._insertNode( createElement( 'BR' ) ); - range.collapse( false ); - setSelection( range ); - updatePath( range, true ); - docWasChanged(); + // Treat image paste as a drop of an image file. + var clipboardData = event.clipboardData, + items = clipboardData && clipboardData.items, + fireDrop = false, + l; + if ( items ) { + l = items.length; + while ( l-- ) { + if ( /^image\/.*/.test( items[l].type ) ) { + event.preventDefault(); + fireEvent( 'dragover', { + dataTransfer: clipboardData, + /*jshint loopfunc: true */ + preventDefault: function () { + fireDrop = true; + } + /*jshint loopfunc: false */ + }); + if ( fireDrop ) { + fireEvent( 'drop', { + dataTransfer: clipboardData + }); + } return; } + } + } - // We need to wrap the contents in divs. - var splitNode = range.startContainer, - splitOffset = range.startOffset, - replacement; - if ( !splitTag ) { - // If the selection point is inside the block, we're going to - // rewrite it so our saved referece points won't be valid. - // Pick a node at a deeper point in the tree to avoid this. - if ( splitNode === block ) { - splitNode = splitOffset ? - splitNode.childNodes[ splitOffset - 1 ] : null; - splitOffset = 0; - if ( splitNode ) { - if ( splitNode.nodeName === 'BR' ) { - splitNode = splitNode.nextSibling; - } else { - splitOffset = splitNode.getLength(); - } - if ( !splitNode || splitNode.nodeName === 'BR' ) { - replacement = createElement( 'DIV' ).fixCursor(); - if ( splitNode ) { - block.replaceChild( replacement, splitNode ); - } else { - block.appendChild( replacement ); - } - splitNode = replacement; - } + awaitingPaste = true; + + var range = getSelection(), + startContainer = range.startContainer, + startOffset = range.startOffset, + endContainer = range.endContainer, + endOffset = range.endOffset; + + var pasteArea = createElement( 'DIV', { + style: 'position: absolute; overflow: hidden; top:' + + (body.scrollTop + 30) + 'px; left: 0; width: 1px; height: 1px;' + }); + body.appendChild( pasteArea ); + range.selectNodeContents( pasteArea ); + setSelection( range ); + + // A setTimeout of 0 means this is added to the back of the + // single javascript thread, so it will be executed after the + // paste event. + setTimeout( function () { + try { + // Get the pasted content and clean + var frag = empty( detach( pasteArea ) ), + first = frag.firstChild, + range = createRange( + startContainer, startOffset, endContainer, endOffset ); + + // Was anything actually pasted? + if ( first ) { + // Safari and IE like putting extra divs around things. + if ( first === frag.lastChild && + first.nodeName === 'DIV' ) { + frag.replaceChild( empty( first ), first ); + } + + frag.normalize(); + addLinks( frag ); + cleanTree( frag, false ); + cleanupBRs( frag ); + removeEmptyInlines( frag ); + + var node = frag, + doPaste = true; + while ( node = getNextBlock( node ) ) { + fixCursor( node ); + } + + fireEvent( 'willPaste', { + fragment: frag, + preventDefault: function () { + doPaste = false; } + }); + + // Insert pasted data + if ( doPaste ) { + range.insertTreeFragment( frag ); + docWasChanged(); + + range.collapse( false ); } - wrapTopLevelInline( block, 'DIV' ); - splitTag = 'DIV'; - if ( !splitNode ) { - splitNode = block.firstChild; - } - range.setStart( splitNode, splitOffset ); - range.setEnd( splitNode, splitOffset ); - block = range.getStartBlock(); } - if ( !block.textContent ) { + setSelection( range ); + updatePath( range, true ); + + awaitingPaste = false; + } catch ( error ) { + editor.didError( error ); + } + }, 0 ); +}); + +// --- Keyboard interaction --- + +var keys = { + 8: 'backspace', + 9: 'tab', + 13: 'enter', + 32: 'space', + 46: 'delete' +}; + +var mapKeyTo = function ( fn ) { + return function ( event ) { + event.preventDefault(); + fn(); + }; +}; + +var mapKeyToFormat = function ( tag ) { + return function ( event ) { + event.preventDefault(); + var range = getSelection(); + if ( hasFormat( tag, null, range ) ) { + changeFormat( null, { tag: tag }, range ); + } else { + changeFormat( { tag: tag }, null, range ); + } + }; +}; + +// If you delete the content inside a span with a font styling, Webkit will +// replace it with a tag (!). If you delete all the text inside a +// link in Opera, it won't delete the link. Let's make things consistent. If +// you delete all text inside an inline tag, remove the inline tag. +var afterDelete = function () { + try { + var range = getSelection(), + node = range.startContainer, + parent; + if ( node.nodeType === TEXT_NODE ) { + node = node.parentNode; + } + // If focussed in empty inline element + if ( isInline( node ) && !node.textContent ) { + do { + parent = node.parentNode; + } while ( isInline( parent ) && + !parent.textContent && ( node = parent ) ); + range.setStart( parent, + indexOf.call( parent.childNodes, node ) ); + range.collapse( true ); + parent.removeChild( node ); + if ( !isBlock( parent ) ) { + parent = getPreviousBlock( parent ); + } + fixCursor( parent ); + range.moveBoundariesDownTree(); + setSelection( range ); + updatePath( range ); + } + } catch ( error ) { + editor.didError( error ); + } +}; + +// If you select all in IE8 then type, it makes a P; replace it with +// a DIV. +if ( isIE8 ) { + addEventListener( 'keyup', function () { + var firstChild = body.firstChild; + if ( firstChild.nodeName === 'P' ) { + saveRangeToBookmark( getSelection() ); + replaceWith( firstChild, createElement( 'DIV', [ + empty( firstChild ) + ]) ); + setSelection( getRangeAndRemoveBookmark() ); + } + }); +} + +var keyHandlers = { + enter: function ( event ) { + // We handle this ourselves + event.preventDefault(); + + // Must have some form of selection + var range = getSelection(); + if ( !range ) { return; } + + // Save undo checkpoint and add any links in the preceding section. + recordUndoState( range ); + addLinks( range.startContainer ); + getRangeAndRemoveBookmark( range ); + + // Selected text is overwritten, therefore delete the contents + // to collapse selection. + if ( !range.collapsed ) { + range._deleteContents(); + } + + var block = range.getStartBlock(), + tag = block ? block.nodeName : 'DIV', + splitTag = tagAfterSplit[ tag ], + nodeAfterSplit; + + // If this is a malformed bit of document, just play it safe + // and insert a
. + if ( !block ) { + range._insertNode( createElement( 'BR' ) ); + range.collapse( false ); + setSelection( range ); + updatePath( range, true ); + docWasChanged(); + return; + } + + // We need to wrap the contents in divs. + var splitNode = range.startContainer, + splitOffset = range.startOffset, + replacement; + if ( !splitTag ) { + // If the selection point is inside the block, we're going to + // rewrite it so our saved referece points won't be valid. + // Pick a node at a deeper point in the tree to avoid this. + if ( splitNode === block ) { + splitNode = splitOffset ? + splitNode.childNodes[ splitOffset - 1 ] : null; + splitOffset = 0; + if ( splitNode ) { + if ( splitNode.nodeName === 'BR' ) { + splitNode = splitNode.nextSibling; + } else { + splitOffset = getLength( splitNode ); + } + if ( !splitNode || splitNode.nodeName === 'BR' ) { + replacement = fixCursor( createElement( 'DIV' ) ); + if ( splitNode ) { + block.replaceChild( replacement, splitNode ); + } else { + block.appendChild( replacement ); + } + splitNode = replacement; + } + } + } + wrapTopLevelInline( block, 'DIV' ); + splitTag = 'DIV'; + if ( !splitNode ) { + splitNode = block.firstChild; + } + range.setStart( splitNode, splitOffset ); + range.setEnd( splitNode, splitOffset ); + block = range.getStartBlock(); + } + + if ( !block.textContent ) { + // Break list + if ( getNearest( block, 'UL' ) || getNearest( block, 'OL' ) ) { + return modifyBlocks( decreaseListLevel, range ); + } + // Break blockquote + else if ( getNearest( block, 'BLOCKQUOTE' ) ) { + return modifyBlocks( removeBlockQuote, range ); + } + } + + // Otherwise, split at cursor point. + nodeAfterSplit = splitBlock( block, splitNode, splitOffset ); + + // Focus cursor + // If there's a / etc. at the beginning of the split + // make sure we focus inside it. + while ( nodeAfterSplit.nodeType === ELEMENT_NODE ) { + var child = nodeAfterSplit.firstChild, + next; + + // Don't continue links over a block break; unlikely to be the + // desired outcome. + if ( nodeAfterSplit.nodeName === 'A' ) { + replaceWith( nodeAfterSplit, empty( nodeAfterSplit ) ); + nodeAfterSplit = child; + continue; + } + + while ( child && child.nodeType === TEXT_NODE && !child.data ) { + next = child.nextSibling; + if ( !next || next.nodeName === 'BR' ) { + break; + } + detach( child ); + child = next; + } + + // 'BR's essentially don't count; they're a browser hack. + // If you try to select the contents of a 'BR', FF will not let + // you type anything! + if ( !child || child.nodeName === 'BR' || + ( child.nodeType === TEXT_NODE && !isOpera ) ) { + break; + } + nodeAfterSplit = child; + } + range = createRange( nodeAfterSplit, 0 ); + setSelection( range ); + updatePath( range, true ); + + // Scroll into view + if ( nodeAfterSplit.nodeType === TEXT_NODE ) { + nodeAfterSplit = nodeAfterSplit.parentNode; + } + if ( nodeAfterSplit.offsetTop + nodeAfterSplit.offsetHeight > + ( doc.documentElement.scrollTop || body.scrollTop ) + + body.offsetHeight ) { + nodeAfterSplit.scrollIntoView( false ); + } + + // We're not still in an undo state + docWasChanged(); + }, + backspace: function ( event ) { + var range = getSelection(); + // If not collapsed, delete contents + if ( !range.collapsed ) { + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + event.preventDefault(); + range._deleteContents(); + setSelection( range ); + updatePath( range, true ); + } + // If at beginning of block, merge with previous + else if ( range.startsAtBlockBoundary() ) { + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + event.preventDefault(); + var current = range.getStartBlock(), + previous = current && getPreviousBlock( current ); + // Must not be at the very beginning of the text area. + if ( previous ) { + // If not editable, just delete whole block. + if ( !previous.isContentEditable ) { + detach( previous ); + return; + } + // Otherwise merge. + mergeWithBlock( previous, current, range ); + // If deleted line between containers, merge newly adjacent + // containers. + current = previous.parentNode; + while ( current && !current.nextSibling ) { + current = current.parentNode; + } + if ( current && ( current = current.nextSibling ) ) { + mergeContainers( current ); + } + setSelection( range ); + } + // If at very beginning of text area, allow backspace + // to break lists/blockquote. + else { // Break list - if ( block.nearest( 'UL' ) || block.nearest( 'OL' ) ) { + if ( getNearest( current, 'UL' ) || + getNearest( current, 'OL' ) ) { return modifyBlocks( decreaseListLevel, range ); } // Break blockquote - else if ( block.nearest( 'BLOCKQUOTE' ) ) { - return modifyBlocks( removeBlockQuote, range ); + else if ( getNearest( current, 'BLOCKQUOTE' ) ) { + return modifyBlocks( decreaseBlockQuoteLevel, range ); } - } - - // Otherwise, split at cursor point. - nodeAfterSplit = splitBlock( block, splitNode, splitOffset ); - - // Focus cursor - // If there's a / etc. at the beginning of the split - // make sure we focus inside it. - while ( nodeAfterSplit.nodeType === ELEMENT_NODE ) { - var child = nodeAfterSplit.firstChild, - next; - - // Don't continue links over a block break; unlikely to be the - // desired outcome. - if ( nodeAfterSplit.nodeName === 'A' ) { - nodeAfterSplit.replaceWith( nodeAfterSplit.empty() ); - nodeAfterSplit = child; - continue; - } - - while ( child && child.nodeType === TEXT_NODE && !child.data ) { - next = child.nextSibling; - if ( !next || next.nodeName === 'BR' ) { - break; - } - child.detach(); - child = next; - } - - // 'BR's essentially don't count; they're a browser hack. - // If you try to select the contents of a 'BR', FF will not let - // you type anything! - if ( !child || child.nodeName === 'BR' || - ( child.nodeType === TEXT_NODE && !isOpera ) ) { - break; - } - nodeAfterSplit = child; - } - range = createRange( nodeAfterSplit, 0 ); - setSelection( range ); - updatePath( range, true ); - - // Scroll into view - if ( nodeAfterSplit.nodeType === TEXT_NODE ) { - nodeAfterSplit = nodeAfterSplit.parentNode; - } - if ( nodeAfterSplit.offsetTop + nodeAfterSplit.offsetHeight > - ( doc.documentElement.scrollTop || body.scrollTop ) + - body.offsetHeight ) { - nodeAfterSplit.scrollIntoView( false ); - } - - // We're not still in an undo state - docWasChanged(); - }, - backspace: function ( event ) { - var range = getSelection(); - // If not collapsed, delete contents - if ( !range.collapsed ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - event.preventDefault(); - range._deleteContents(); setSelection( range ); updatePath( range, true ); } - // If at beginning of block, merge with previous - else if ( range.startsAtBlockBoundary() ) { + } + // Otherwise, leave to browser but check afterwards whether it has + // left behind an empty inline tag. + else { + var text = range.startContainer.data || ''; + if ( !notWS.test( text.charAt( range.startOffset - 1 ) ) ) { recordUndoState( range ); getRangeAndRemoveBookmark( range ); - event.preventDefault(); - var current = range.getStartBlock(), - previous = current && current.getPreviousBlock(); - // Must not be at the very beginning of the text area. - if ( previous ) { - // If not editable, just delete whole block. - if ( !previous.isContentEditable ) { - previous.detach(); - return; - } - // Otherwise merge. - previous.mergeWithBlock( current, range ); - // If deleted line between containers, merge newly adjacent - // containers. - current = previous.parentNode; - while ( current && !current.nextSibling ) { - current = current.parentNode; - } - if ( current && ( current = current.nextSibling ) ) { - current.mergeContainers(); - } - setSelection( range ); - } - // If at very beginning of text area, allow backspace - // to break lists/blockquote. - else { - // Break list - if ( current.nearest( 'UL' ) || current.nearest( 'OL' ) ) { - return modifyBlocks( decreaseListLevel, range ); - } - // Break blockquote - else if ( current.nearest( 'BLOCKQUOTE' ) ) { - return modifyBlocks( decreaseBlockQuoteLevel, range ); - } - setSelection( range ); - updatePath( range, true ); - } + setSelection( range ); } - // Otherwise, leave to browser but check afterwards whether it has - // left behind an empty inline tag. - else { - var text = range.startContainer.data || ''; - if ( !notWS.test( text.charAt( range.startOffset - 1 ) ) ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); + setTimeout( afterDelete, 0 ); + } + }, + 'delete': function ( event ) { + var range = getSelection(); + // If not collapsed, delete contents + if ( !range.collapsed ) { + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + event.preventDefault(); + range._deleteContents(); + setSelection( range ); + updatePath( range, true ); + } + // If at end of block, merge next into this block + else if ( range.endsAtBlockBoundary() ) { + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + event.preventDefault(); + var current = range.getStartBlock(), + next = current && getNextBlock( current ); + // Must not be at the very end of the text area. + if ( next ) { + // If not editable, just delete whole block. + if ( !next.isContentEditable ) { + detach( next ); + return; + } + // Otherwise merge. + mergeWithBlock( current, next, range ); + // If deleted line between containers, merge newly adjacent + // containers. + next = current.parentNode; + while ( next && !next.nextSibling ) { + next = next.parentNode; + } + if ( next && ( next = next.nextSibling ) ) { + mergeContainers( next ); } - setTimeout( afterDelete, 0 ); - } - }, - 'delete': function ( event ) { - var range = getSelection(); - // If not collapsed, delete contents - if ( !range.collapsed ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - event.preventDefault(); - range._deleteContents(); setSelection( range ); updatePath( range, true ); } - // If at end of block, merge next into this block - else if ( range.endsAtBlockBoundary() ) { + } + // Otherwise, leave to browser but check afterwards whether it has + // left behind an empty inline tag. + else { + // Record undo point if deleting whitespace + var text = range.startContainer.data || ''; + if ( !notWS.test( text.charAt( range.startOffset ) ) ) { recordUndoState( range ); getRangeAndRemoveBookmark( range ); - event.preventDefault(); - var current = range.getStartBlock(), - next = current && current.getNextBlock(); - // Must not be at the very end of the text area. - if ( next ) { - // If not editable, just delete whole block. - if ( !next.isContentEditable ) { - next.detach(); - return; - } - // Otherwise merge. - current.mergeWithBlock( next, range ); - // If deleted line between containers, merge newly adjacent - // containers. - next = current.parentNode; - while ( next && !next.nextSibling ) { - next = next.parentNode; - } - if ( next && ( next = next.nextSibling ) ) { - next.mergeContainers(); - } - setSelection( range ); - updatePath( range, true ); - } - } - // Otherwise, leave to browser but check afterwards whether it has - // left behind an empty inline tag. - else { - // Record undo point if deleting whitespace - var text = range.startContainer.data || ''; - if ( !notWS.test( text.charAt( range.startOffset ) ) ) { - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); - } - setTimeout( afterDelete, 0 ); - } - }, - space: function () { - var range = getSelection(); - recordUndoState( range ); - addLinks( range.startContainer ); - getRangeAndRemoveBookmark( range ); - setSelection( range ); - }, - 'ctrl-b': mapKeyToFormat( 'B' ), - 'ctrl-i': mapKeyToFormat( 'I' ), - 'ctrl-u': mapKeyToFormat( 'U' ), - 'ctrl-y': mapKeyTo( redo ), - 'ctrl-z': mapKeyTo( undo ), - 'ctrl-shift-z': mapKeyTo( redo ) - }; - - // Ref: http://unixpapa.com/js/key.html - // Opera does not fire keydown repeatedly. - addEventListener( isOpera ? 'keypress' : 'keydown', - function ( event ) { - var code = event.keyCode, - key = keys[ code ] || String.fromCharCode( code ).toLowerCase(), - modifiers = ''; - - // On keypress, delete and '.' both have event.keyCode 46 - // Must check event.which to differentiate. - if ( isOpera && event.which === 46 ) { - key = '.'; - } - - // Function keys - if ( 111 < code && code < 124 ) { - key = 'f' + ( code - 111 ); - } - - if ( event.altKey ) { modifiers += 'alt-'; } - if ( event.ctrlKey || event.metaKey ) { modifiers += 'ctrl-'; } - if ( event.shiftKey ) { modifiers += 'shift-'; } - - key = modifiers + key; - - if ( keyHandlers[ key ] ) { - keyHandlers[ key ]( event ); - } - }); - - // --- Export --- - - var chain = function ( fn ) { - return function () { - fn.apply( null, arguments ); - return this; - }; - }; - - var command = function ( fn, arg, arg2 ) { - return function () { - fn( arg, arg2 ); - focus(); - return this; - }; - }; - - win.editor = editor = { - - didError: function ( error ) { - console.log( error ); - }, - - _setPlaceholderTextNode: setPlaceholderTextNode, - - addEventListener: chain( addEventListener ), - removeEventListener: chain( removeEventListener ), - - focus: chain( focus ), - blur: chain( blur ), - - getDocument: function () { - return doc; - }, - - addStyles: function ( styles ) { - if ( styles ) { - var head = doc.documentElement.firstChild, - style = createElement( 'STYLE', { - type: 'text/css' - }); - if ( style.styleSheet ) { - // IE8: must append to document BEFORE adding styles - // or you get the IE7 CSS parser! - head.appendChild( style ); - style.styleSheet.cssText = styles; - } else { - // Everyone else - style.appendChild( doc.createTextNode( styles ) ); - head.appendChild( style ); - } - } - return this; - }, - - getHTML: function ( withBookMark ) { - var brs = [], - node, fixer, html, l, range; - if ( withBookMark && ( range = getSelection() ) ) { - saveRangeToBookmark( range ); - } - if ( useTextFixer ) { - node = body; - while ( node = node.getNextBlock() ) { - if ( !node.textContent && !node.querySelector( 'BR' ) ) { - fixer = createElement( 'BR' ); - node.appendChild( fixer ); - brs.push( fixer ); - } - } - } - html = getHTML(); - if ( useTextFixer ) { - l = brs.length; - while ( l-- ) { - brs[l].detach(); - } - } - if ( range ) { - getRangeAndRemoveBookmark( range ); - } - return html; - }, - setHTML: function ( html ) { - var frag = doc.createDocumentFragment(), - div = createElement( 'DIV' ), - child; - - // Parse HTML into DOM tree - div.innerHTML = html; - frag.appendChild( div.empty() ); - - cleanTree( frag, true ); - cleanupBRs( frag ); - - wrapTopLevelInline( frag, 'DIV' ); - - // Fix cursor - var node = frag; - while ( node = node.getNextBlock() ) { - node.fixCursor(); - } - - // Remove existing body children - while ( child = body.lastChild ) { - body.removeChild( child ); - } - - // And insert new content - body.appendChild( frag ); - body.fixCursor(); - - // Reset the undo stack - undoIndex = -1; - undoStack = []; - undoStackLength = 0; - isInUndoState = false; - - // Record undo state - var range = getRangeAndRemoveBookmark() || - createRange( body.firstChild, 0 ); - recordUndoState( range ); - getRangeAndRemoveBookmark( range ); - // IE will also set focus when selecting text so don't use - // setSelection. Instead, just store it in lastSelection, so if - // anything calls getSelection before first focus, we have a range - // to return. - if ( losesSelectionOnBlur ) { - lastSelection = range; - } else { setSelection( range ); } - updatePath( range, true ); + setTimeout( afterDelete, 0 ); + } + }, + space: function () { + var range = getSelection(); + recordUndoState( range ); + addLinks( range.startContainer ); + getRangeAndRemoveBookmark( range ); + setSelection( range ); + }, + 'ctrl-b': mapKeyToFormat( 'B' ), + 'ctrl-i': mapKeyToFormat( 'I' ), + 'ctrl-u': mapKeyToFormat( 'U' ), + 'ctrl-y': mapKeyTo( redo ), + 'ctrl-z': mapKeyTo( undo ), + 'ctrl-shift-z': mapKeyTo( redo ) +}; - return this; - }, +// Ref: http://unixpapa.com/js/key.html +// Opera does not fire keydown repeatedly. +addEventListener( isOpera ? 'keypress' : 'keydown', + function ( event ) { + var code = event.keyCode, + key = keys[ code ] || String.fromCharCode( code ).toLowerCase(), + modifiers = ''; - getSelectedText: function () { - return getSelection().getTextContent(); - }, - - insertElement: chain( insertElement ), - insertImage: function ( src ) { - var img = createElement( 'IMG', { - src: src - }); - insertElement( img ); - return img; - }, - - getPath: function () { - return path; - }, - getSelection: getSelection, - setSelection: chain( setSelection ), - - undo: chain( undo ), - redo: chain( redo ), - - hasFormat: hasFormat, - changeFormat: chain( changeFormat ), - - bold: command( changeFormat, { tag: 'B' } ), - italic: command( changeFormat, { tag: 'I' } ), - underline: command( changeFormat, { tag: 'U' } ), - - removeBold: command( changeFormat, null, { tag: 'B' } ), - removeItalic: command( changeFormat, null, { tag: 'I' } ), - removeUnderline: command( changeFormat, null, { tag: 'U' } ), - - makeLink: function ( url ) { - url = encodeURI( url ); - var range = getSelection(); - if ( range.collapsed ) { - var protocolEnd = url.indexOf( ':' ) + 1; - if ( protocolEnd ) { - while ( url[ protocolEnd ] === '/' ) { protocolEnd += 1; } - } - range._insertNode( - doc.createTextNode( url.slice( protocolEnd ) ) - ); - } - changeFormat({ - tag: 'A', - attributes: { - href: url - } - }, { - tag: 'A' - }, range ); - focus(); - return this; - }, - - removeLink: function () { - changeFormat( null, { - tag: 'A' - }, getSelection(), true ); - focus(); - return this; - }, - - setFontFace: function ( name ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'font', - style: 'font-family: ' + name + ', sans-serif;' - } - }, { - tag: 'SPAN', - attributes: { 'class': 'font' } - }); - focus(); - return this; - }, - setFontSize: function ( size ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'size', - style: 'font-size: ' + - ( typeof size === 'number' ? size + 'px' : size ) - } - }, { - tag: 'SPAN', - attributes: { 'class': 'size' } - }); - focus(); - return this; - }, - - setTextColour: function ( colour ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'colour', - style: 'color: ' + colour - } - }, { - tag: 'SPAN', - attributes: { 'class': 'colour' } - }); - focus(); - return this; - }, - - setHighlightColour: function ( colour ) { - changeFormat({ - tag: 'SPAN', - attributes: { - 'class': 'highlight', - style: 'background-color: ' + colour - } - }, { - tag: 'SPAN', - attributes: { 'class': 'highlight' } - }); - focus(); - return this; - }, - - setTextAlignment: function ( alignment ) { - forEachBlock( function ( block ) { - block.className = ( block.className - .split( /\s+/ ) - .filter( function ( klass ) { - return !( /align/.test( klass ) ); - }) - .join( ' ' ) + - ' align-' + alignment ).trim(); - block.style.textAlign = alignment; - }, true ); - focus(); - return this; - }, - - setTextDirection: function ( direction ) { - forEachBlock( function ( block ) { - block.className = ( block.className - .split( /\s+/ ) - .filter( function ( klass ) { - return !( /dir/.test( klass ) ); - }) - .join( ' ' ) + - ' dir-' + direction ).trim(); - block.dir = direction; - }, true ); - focus(); - return this; - }, - - forEachBlock: chain( forEachBlock ), - modifyBlocks: chain( modifyBlocks ), - - increaseQuoteLevel: command( modifyBlocks, increaseBlockQuoteLevel ), - decreaseQuoteLevel: command( modifyBlocks, decreaseBlockQuoteLevel ), - - makeUnorderedList: command( modifyBlocks, makeUnorderedList ), - makeOrderedList: command( modifyBlocks, makeOrderedList ), - removeList: command( modifyBlocks, decreaseListLevel ) - }; - - // --- Initialise --- - - body.setAttribute( 'contenteditable', 'true' ); - editor.setHTML( '' ); - - if ( win.onEditorLoad ) { - win.onEditorLoad( win.editor ); - win.onEditorLoad = null; + // On keypress, delete and '.' both have event.keyCode 46 + // Must check event.which to differentiate. + if ( isOpera && event.which === 46 ) { + key = '.'; } -}( document, UA, DOMTreeWalker ) ); + // Function keys + if ( 111 < code && code < 124 ) { + key = 'f' + ( code - 111 ); + } + + if ( event.altKey ) { modifiers += 'alt-'; } + if ( event.ctrlKey || event.metaKey ) { modifiers += 'ctrl-'; } + if ( event.shiftKey ) { modifiers += 'shift-'; } + + key = modifiers + key; + + if ( keyHandlers[ key ] ) { + keyHandlers[ key ]( event ); + } +}); + +// --- Export --- + +var chain = function ( fn ) { + return function () { + fn.apply( null, arguments ); + return this; + }; +}; + +var command = function ( fn, arg, arg2 ) { + return function () { + fn( arg, arg2 ); + focus(); + return this; + }; +}; + +editor = win.editor = { + + didError: function ( error ) { + console.log( error ); + }, + + addEventListener: chain( addEventListener ), + removeEventListener: chain( removeEventListener ), + + focus: chain( focus ), + blur: chain( blur ), + + getDocument: function () { + return doc; + }, + + addStyles: function ( styles ) { + if ( styles ) { + var head = doc.documentElement.firstChild, + style = createElement( 'STYLE', { + type: 'text/css' + }); + if ( style.styleSheet ) { + // IE8: must append to document BEFORE adding styles + // or you get the IE7 CSS parser! + head.appendChild( style ); + style.styleSheet.cssText = styles; + } else { + // Everyone else + style.appendChild( doc.createTextNode( styles ) ); + head.appendChild( style ); + } + } + return this; + }, + + getHTML: function ( withBookMark ) { + var brs = [], + node, fixer, html, l, range; + if ( withBookMark && ( range = getSelection() ) ) { + saveRangeToBookmark( range ); + } + if ( useTextFixer ) { + node = body; + while ( node = getNextBlock( node ) ) { + if ( !node.textContent && !node.querySelector( 'BR' ) ) { + fixer = createElement( 'BR' ); + node.appendChild( fixer ); + brs.push( fixer ); + } + } + } + html = getHTML(); + if ( useTextFixer ) { + l = brs.length; + while ( l-- ) { + detach( brs[l] ); + } + } + if ( range ) { + getRangeAndRemoveBookmark( range ); + } + return html; + }, + setHTML: function ( html ) { + var frag = doc.createDocumentFragment(), + div = createElement( 'DIV' ), + child; + + // Parse HTML into DOM tree + div.innerHTML = html; + frag.appendChild( empty( div ) ); + + cleanTree( frag, true ); + cleanupBRs( frag ); + + wrapTopLevelInline( frag, 'DIV' ); + + // Fix cursor + var node = frag; + while ( node = getNextBlock( node ) ) { + fixCursor( node ); + } + + // Remove existing body children + while ( child = body.lastChild ) { + body.removeChild( child ); + } + + // And insert new content + body.appendChild( frag ); + fixCursor( body ); + + // Reset the undo stack + undoIndex = -1; + undoStack = []; + undoStackLength = 0; + isInUndoState = false; + + // Record undo state + var range = getRangeAndRemoveBookmark() || + createRange( body.firstChild, 0 ); + recordUndoState( range ); + getRangeAndRemoveBookmark( range ); + // IE will also set focus when selecting text so don't use + // setSelection. Instead, just store it in lastSelection, so if + // anything calls getSelection before first focus, we have a range + // to return. + if ( losesSelectionOnBlur ) { + lastSelection = range; + } else { + setSelection( range ); + } + updatePath( range, true ); + + return this; + }, + + getSelectedText: function () { + return getSelection().getTextContent(); + }, + + insertElement: chain( insertElement ), + insertImage: function ( src ) { + var img = createElement( 'IMG', { + src: src + }); + insertElement( img ); + return img; + }, + + getPath: function () { + return path; + }, + getSelection: getSelection, + setSelection: chain( setSelection ), + + undo: chain( undo ), + redo: chain( redo ), + + hasFormat: hasFormat, + changeFormat: chain( changeFormat ), + + bold: command( changeFormat, { tag: 'B' } ), + italic: command( changeFormat, { tag: 'I' } ), + underline: command( changeFormat, { tag: 'U' } ), + + removeBold: command( changeFormat, null, { tag: 'B' } ), + removeItalic: command( changeFormat, null, { tag: 'I' } ), + removeUnderline: command( changeFormat, null, { tag: 'U' } ), + + makeLink: function ( url ) { + url = encodeURI( url ); + var range = getSelection(); + if ( range.collapsed ) { + var protocolEnd = url.indexOf( ':' ) + 1; + if ( protocolEnd ) { + while ( url[ protocolEnd ] === '/' ) { protocolEnd += 1; } + } + range._insertNode( + doc.createTextNode( url.slice( protocolEnd ) ) + ); + } + changeFormat({ + tag: 'A', + attributes: { + href: url + } + }, { + tag: 'A' + }, range ); + focus(); + return this; + }, + + removeLink: function () { + changeFormat( null, { + tag: 'A' + }, getSelection(), true ); + focus(); + return this; + }, + + setFontFace: function ( name ) { + changeFormat({ + tag: 'SPAN', + attributes: { + 'class': 'font', + style: 'font-family: ' + name + ', sans-serif;' + } + }, { + tag: 'SPAN', + attributes: { 'class': 'font' } + }); + focus(); + return this; + }, + setFontSize: function ( size ) { + changeFormat({ + tag: 'SPAN', + attributes: { + 'class': 'size', + style: 'font-size: ' + + ( typeof size === 'number' ? size + 'px' : size ) + } + }, { + tag: 'SPAN', + attributes: { 'class': 'size' } + }); + focus(); + return this; + }, + + setTextColour: function ( colour ) { + changeFormat({ + tag: 'SPAN', + attributes: { + 'class': 'colour', + style: 'color: ' + colour + } + }, { + tag: 'SPAN', + attributes: { 'class': 'colour' } + }); + focus(); + return this; + }, + + setHighlightColour: function ( colour ) { + changeFormat({ + tag: 'SPAN', + attributes: { + 'class': 'highlight', + style: 'background-color: ' + colour + } + }, { + tag: 'SPAN', + attributes: { 'class': 'highlight' } + }); + focus(); + return this; + }, + + setTextAlignment: function ( alignment ) { + forEachBlock( function ( block ) { + block.className = ( block.className + .split( /\s+/ ) + .filter( function ( klass ) { + return !( /align/.test( klass ) ); + }) + .join( ' ' ) + + ' align-' + alignment ).trim(); + block.style.textAlign = alignment; + }, true ); + focus(); + return this; + }, + + setTextDirection: function ( direction ) { + forEachBlock( function ( block ) { + block.className = ( block.className + .split( /\s+/ ) + .filter( function ( klass ) { + return !( /dir/.test( klass ) ); + }) + .join( ' ' ) + + ' dir-' + direction ).trim(); + block.dir = direction; + }, true ); + focus(); + return this; + }, + + forEachBlock: chain( forEachBlock ), + modifyBlocks: chain( modifyBlocks ), + + increaseQuoteLevel: command( modifyBlocks, increaseBlockQuoteLevel ), + decreaseQuoteLevel: command( modifyBlocks, decreaseBlockQuoteLevel ), + + makeUnorderedList: command( modifyBlocks, makeUnorderedList ), + makeOrderedList: command( modifyBlocks, makeOrderedList ), + removeList: command( modifyBlocks, decreaseListLevel ) +}; + +// --- Initialise --- + +body.setAttribute( 'contenteditable', 'true' ); +editor.setHTML( '' ); + +if ( win.onEditorLoad ) { + win.onEditorLoad( win.editor ); + win.onEditorLoad = null; +} diff --git a/source/Node.js b/source/Node.js index 3f4f590..511871d 100644 --- a/source/Node.js +++ b/source/Node.js @@ -1,37 +1,23 @@ -/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license. */ +/*global + ELEMENT_NODE, + TEXT_NODE, + SHOW_ELEMENT, + FILTER_ACCEPT, + FILTER_SKIP, + doc, + isOpera, + useTextFixer, + cantFocusEmptyTextNodes, -/*global Node, Text, Element, HTMLDocument, window, document, - editor, UA, DOMTreeWalker */ + TreeWalker, -( function ( UA, TreeWalker ) { + Text, -"use strict"; + setPlaceholderTextNode +*/ +/*jshint strict:false */ -var implement = function ( constructors, props ) { - var l = constructors.length, - proto, prop; - while ( l-- ) { - proto = constructors[l].prototype; - for ( prop in props ) { - proto[ prop ] = props[ prop ]; - } - } -}; - -var every = function ( nodeList, fn ) { - var l = nodeList.length; - while ( l-- ) { - if ( !fn( nodeList[l] ) ) { - return false; - } - } - return true; -}; - -var $False = function () { return false; }; -var $True = function () { return true; }; - -var inlineNodeNames = /^(?:A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TRONG|AMP)|U)$/; +var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:FN|EL)|EM|FONT|HR|I(?:NPUT|MG|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:U[BP]|PAN|TRONG|AMP)|U)$/; var leafNodeNames = { BR: 1, @@ -39,305 +25,231 @@ var leafNodeNames = { INPUT: 1 }; -var swap = function ( node, node2 ) { - var parent = node2.parentNode; - if ( parent ) { - parent.replaceChild( node, node2 ); +function every ( nodeList, fn ) { + var l = nodeList.length; + while ( l-- ) { + if ( !fn( nodeList[l] ) ) { + return false; + } } - return node; -}; + return true; +} -var ELEMENT_NODE = 1, // Node.ELEMENT_NODE, - TEXT_NODE = 3, // Node.TEXT_NODE, - SHOW_ELEMENT = 1, // NodeFilter.SHOW_ELEMENT, - FILTER_ACCEPT = 1, // NodeFilter.FILTER_ACCEPT, - FILTER_SKIP = 3; // NodeFilter.FILTER_SKIP; +// --- -var isBlock = function ( el ) { - return el.isBlock() ? FILTER_ACCEPT : FILTER_SKIP; -}; - -implement( window.Node ? [ Node ] : [ Text, Element, HTMLDocument ], { - isLeaf: $False, - isInline: $False, - isBlock: $False, - isContainer: $False, - getPath: function () { - var parent = this.parentNode; - return parent ? parent.getPath() : ''; - }, - detach: function () { - var parent = this.parentNode; - if ( parent ) { - parent.removeChild( this ); - } - return this; - }, - replaceWith: function ( node ) { - swap( node, this ); - return this; - }, - replaces: function ( node ) { - swap( this, node ); - return this; - }, - nearest: function ( tag, attributes ) { - var parent = this.parentNode; - return parent ? parent.nearest( tag, attributes ) : null; - }, - getPreviousBlock: function () { - var doc = this.ownerDocument, - walker = new TreeWalker( - doc.body, SHOW_ELEMENT, isBlock, false ); - walker.currentNode = this; - return walker.previousNode(); - }, - getNextBlock: function () { - var doc = this.ownerDocument, - walker = new TreeWalker( - doc.body, SHOW_ELEMENT, isBlock, false ); - walker.currentNode = this; - return walker.nextNode(); - }, - split: function ( node, stopNode ) { - return node; - }, - mergeContainers: function () {} -}); - -implement([ Text ], { - isInline: $True, - getLength: function () { - return this.length; - }, - isLike: function ( node ) { - return node.nodeType === TEXT_NODE; - }, - split: function ( offset, stopNode ) { - var node = this; - if ( node === stopNode ) { - return offset; - } - return node.parentNode.split( node.splitText( offset ), stopNode ); +function hasTagAttributes ( node, tag, attributes ) { + if ( node.nodeName !== tag ) { + return false; } -}); - -implement([ Element ], { - isLeaf: function () { - return !!leafNodeNames[ this.nodeName ]; - }, - isInline: function () { - return inlineNodeNames.test( this.nodeName ); - }, - isBlock: function () { - return !this.isInline() && every( this.childNodes, function ( child ) { - return child.isInline(); - }); - }, - isContainer: function () { - return !this.isInline() && !this.isBlock(); - }, - getLength: function () { - return this.childNodes.length; - }, - getPath: function () { - var parent = this.parentNode, - path, id, className, classNames; - if ( !parent ) { - return ''; + for ( var attr in attributes ) { + if ( node.getAttribute( attr ) !== attributes[ attr ] ) { + return false; } - path = parent.getPath(); - path += ( path ? '>' : '' ) + this.nodeName; - if ( id = this.id ) { + } + 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 acceptIfBlock ( el ) { + return isBlock( el ) ? FILTER_ACCEPT : FILTER_SKIP; +} +function getBlockWalker ( node ) { + var doc = node.ownerDocument, + walker = new TreeWalker( + doc.body, SHOW_ELEMENT, acceptIfBlock, 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 = this.className.trim() ) { + if ( className = node.className.trim() ) { classNames = className.split( /\s\s*/ ); classNames.sort(); path += '.'; path += classNames.join( '.' ); } - return path; - }, - wraps: function ( node ) { - swap( this, node ).appendChild( node ); - return this; - }, - empty: function () { - var frag = this.ownerDocument.createDocumentFragment(), - l = this.childNodes.length; - while ( l-- ) { - frag.appendChild( this.firstChild ); + } + 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 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, + fixer, child; + + if ( node.nodeName === 'BODY' ) { + if ( !( child = node.firstChild ) || child.nodeName === 'BR' ) { + fixer = doc.createElement( 'DIV' ); + if ( child ) { + node.replaceChild( fixer, child ); + } + else { + node.appendChild( fixer ); + } + node = fixer; + fixer = null; } - return frag; - }, - is: function ( tag, attributes ) { - if ( this.nodeName !== tag ) { return false; } - var attr; - for ( attr in attributes ) { - if ( this.getAttribute( attr ) !== attributes[ attr ] ) { - return false; + } + + if ( isInline( node ) ) { + if ( !node.firstChild ) { + if ( cantFocusEmptyTextNodes ) { + fixer = doc.createTextNode( '\u200B' ); + setPlaceholderTextNode( fixer ); + } else { + fixer = doc.createTextNode( '' ); } } - return true; - }, - nearest: function ( tag, attributes ) { - var el = this; - do { - if ( el.is( tag, attributes ) ) { - return el; + } else { + if ( useTextFixer ) { + while ( node.nodeType !== TEXT_NODE && !isLeaf( node ) ) { + child = node.firstChild; + if ( !child ) { + fixer = doc.createTextNode( '' ); + break; + } + node = child; } - } while ( ( el = el.parentNode ) && - ( el.nodeType === ELEMENT_NODE ) ); - return null; - }, - isLike: function ( node ) { - return ( - node.nodeType === ELEMENT_NODE && - node.nodeName === this.nodeName && - node.className === this.className && - node.style.cssText === this.style.cssText - ); - }, - mergeInlines: function ( range ) { - var children = this.childNodes, - l = children.length, - frags = [], - child, prev, len; - while ( l-- ) { - child = children[l]; - prev = l && children[ l - 1 ]; - if ( l && child.isInline() && child.isLike( prev ) && - !leafNodeNames[ child.nodeName ] ) { - if ( range.startContainer === child ) { - range.startContainer = prev; - range.startOffset += prev.getLength(); + 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 = ''; } - if ( range.endContainer === child ) { - range.endContainer = prev; - range.endOffset += prev.getLength(); - } - if ( range.startContainer === this ) { - if ( range.startOffset > l ) { - range.startOffset -= 1; - } - else if ( range.startOffset === l ) { - range.startContainer = prev; - range.startOffset = prev.getLength(); - } - } - if ( range.endContainer === this ) { - if ( range.endOffset > l ) { - range.endOffset -= 1; - } - else if ( range.endOffset === l ) { - range.endContainer = prev; - range.endOffset = prev.getLength(); - } - } - child.detach(); - if ( child.nodeType === TEXT_NODE ) { - prev.appendData( child.data.replace( /\u200B/g, '' ) ); - } - else { - frags.push( child.empty() ); - } - } - else if ( child.nodeType === ELEMENT_NODE ) { - len = frags.length; - while ( len-- ) { - child.appendChild( frags.pop() ); - } - child.mergeInlines( range ); + } else if ( isLeaf( node ) ) { + node.parentNode.insertBefore( doc.createTextNode( '' ), node ); } } - }, - mergeWithBlock: function ( next, range ) { - var block = this, - container = next, - last, offset, _range; - while ( container.parentNode.childNodes.length === 1 ) { - container = container.parentNode; - } - container.detach(); - - 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( next.empty() ); - block.mergeInlines( _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 ( window.opera && ( last = block.lastChild ) && - last.nodeName === 'BR' ) { - block.removeChild( last ); - } - }, - mergeContainers: function () { - var prev = this.previousSibling, - first = this.firstChild; - if ( prev && prev.isLike( this ) && prev.isContainer() ) { - prev.appendChild( this.detach().empty() ); - if ( first ) { - first.mergeContainers(); + else if ( !node.querySelector( 'BR' ) ) { + fixer = doc.createElement( 'BR' ); + while ( ( child = node.lastElementChild ) && !isInline( child ) ) { + node = child; } } - }, - split: function ( childNodeToSplitBefore, stopNode ) { - var node = this; + } + if ( fixer ) { + node.appendChild( fixer ); + } - if ( typeof( childNodeToSplitBefore ) === 'number' ) { - childNodeToSplitBefore = - childNodeToSplitBefore < node.childNodes.length ? - node.childNodes[ childNodeToSplitBefore ] : null; - } + return node; +} +function split ( node, offset, stopNode ) { + var nodeType = node.nodeType, + parent, clone, next; + if ( nodeType === TEXT_NODE ) { if ( node === stopNode ) { - return childNodeToSplitBefore; + return offset; + } + 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 - var parent = node.parentNode, - clone = node.cloneNode( false ), - next; + parent = node.parentNode, + clone = node.cloneNode( false ); // Add right-hand siblings to the clone - while ( childNodeToSplitBefore ) { - next = childNodeToSplitBefore.nextSibling; - clone.appendChild( childNodeToSplitBefore ); - childNodeToSplitBefore = next; + while ( offset ) { + next = offset.nextSibling; + clone.appendChild( offset ); + offset = next; } // 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. - node.fixCursor(); - clone.fixCursor(); + fixCursor( node ); + fixCursor( clone ); // Inject clone after original node if ( next = node.nextSibling ) { @@ -347,74 +259,142 @@ implement([ Element ], { } // Keep on splitting up the tree - return parent.split( clone, stopNode ); - }, - fixCursor: function () { - // 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 el = this, - doc = el.ownerDocument, - fixer, child; - - if ( el.nodeName === 'BODY' ) { - if ( !( child = el.firstChild ) || child.nodeName === 'BR' ) { - fixer = doc.createElement( 'DIV' ); - if ( child ) { - el.replaceChild( fixer, child ); - } - else { - el.appendChild( fixer ); - } - el = fixer; - fixer = null; - } - } - - if ( el.isInline() ) { - if ( !el.firstChild ) { - if ( UA.cantFocusEmptyTextNodes ) { - fixer = doc.createTextNode( '\u200B' ); - editor._setPlaceholderTextNode( fixer ); - } else { - fixer = doc.createTextNode( '' ); - } - } - } else { - if ( UA.useTextFixer ) { - while ( el.nodeType !== TEXT_NODE && !el.isLeaf() ) { - child = el.firstChild; - if ( !child ) { - fixer = doc.createTextNode( '' ); - break; - } - el = child; - } - if ( el.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( el.data ) ) { - el.data = ''; - } - } else if ( el.isLeaf() ) { - el.parentNode.insertBefore( doc.createTextNode( '' ), el ); - } - } - else if ( !el.querySelector( 'BR' ) ) { - fixer = doc.createElement( 'BR' ); - while ( ( child = el.lastElementChild ) && !child.isInline() ) { - el = child; - } - } - } - if ( fixer ) { - el.appendChild( fixer ); - } - - return this; + return split( parent, clone, stopNode ); } -}); + return node; +} + +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.replace( /\u200B/g, '' ) ); + } + 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 ( isOpera && ( last = block.lastChild ) && last.nodeName === 'BR' ) { + block.removeChild( last ); + } +} + +function mergeContainers ( node ) { + var prev = node.previousSibling, + first = node.firstChild; + if ( prev && areAlike( prev, node ) && isContainer( prev ) ) { + detach( node ); + prev.appendChild( empty( node ) ); + if ( first ) { + mergeContainers( first ); + } + } +} + +function createElement ( tag, props, children ) { + var el = doc.createElement( tag ), + attr, i, l; + if ( props instanceof Array ) { + children = props; + props = null; + } + if ( props ) { + for ( attr in props ) { + el.setAttribute( attr, props[ attr ] ); + } + } + if ( children ) { + for ( i = 0, l = children.length; i < l; i += 1 ) { + el.appendChild( children[i] ); + } + } + return el; +} // 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 @@ -423,8 +403,8 @@ implement([ Element ], { // the document and replaced by another, rather than just having its data // shortened. if ( function () { - var div = document.createElement( 'div' ), - text = document.createTextNode( '12' ); + var div = doc.createElement( 'div' ), + text = doc.createTextNode( '12' ); div.appendChild( text ); text.splitText( 2 ); return div.childNodes.length !== 2; @@ -446,5 +426,3 @@ if ( function () { return afterSplit; }; } - -}( UA, DOMTreeWalker ) ); diff --git a/source/Range.js b/source/Range.js index 5669aa5..1997a1d 100644 --- a/source/Range.js +++ b/source/Range.js @@ -1,21 +1,30 @@ -/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license. */ +/*global + ELEMENT_NODE, + TEXT_NODE, + SHOW_TEXT, + FILTER_ACCEPT, + START_TO_START, + START_TO_END, + END_TO_END, + END_TO_START, + indexOf, -/*global Range, DOMTreeWalker */ + TreeWalker, -( function ( TreeWalker ) { + isLeaf, + isInline, + isBlock, + getPreviousBlock, + getNextBlock, + getLength, + fixCursor, + split, + mergeWithBlock, + mergeContainers, -"use strict"; - -var indexOf = Array.prototype.indexOf; - -var ELEMENT_NODE = 1, // Node.ELEMENT_NODE - TEXT_NODE = 3, // Node.TEXT_NODE - SHOW_TEXT = 4, // NodeFilter.SHOW_TEXT, - FILTER_ACCEPT = 1, // NodeFilter.FILTER_ACCEPT, - START_TO_START = 0, // Range.START_TO_START - START_TO_END = 1, // Range.START_TO_END - END_TO_END = 2, // Range.END_TO_END - END_TO_START = 3; // Range.END_TO_START + Range +*/ +/*jshint strict:false */ var getNodeBefore = function ( node, offset ) { var children = node.childNodes; @@ -42,482 +51,474 @@ var getNodeAfter = function ( node, offset ) { return node; }; -var RangePrototypeExtensions = { +var RangePrototype = Range.prototype; - forEachTextNode: function ( fn ) { - var range = this.cloneRange(); - range.moveBoundariesDownTree(); +RangePrototype.forEachTextNode = function ( fn ) { + var range = this.cloneRange(); + range.moveBoundariesDownTree(); - var startContainer = range.startContainer, - endContainer = range.endContainer, - root = range.commonAncestorContainer, - walker = new TreeWalker( - root, SHOW_TEXT, function ( node ) { - return FILTER_ACCEPT; - }, false ), - textnode = walker.currentNode = startContainer; + var startContainer = range.startContainer, + endContainer = range.endContainer, + root = range.commonAncestorContainer, + walker = new TreeWalker( + root, SHOW_TEXT, function ( node ) { + return FILTER_ACCEPT; + }, false ), + textnode = walker.currentNode = startContainer; - while ( !fn( textnode, range ) && - textnode !== endContainer && - ( textnode = walker.nextNode() ) ) {} - }, + while ( !fn( textnode, range ) && + textnode !== endContainer && + ( textnode = walker.nextNode() ) ) {} +}; - getTextContent: function () { - var textContent = ''; - this.forEachTextNode( 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; +RangePrototype.getTextContent = function () { + var textContent = ''; + this.forEachTextNode( function ( textnode, range ) { + var value = textnode.data; + if ( value && ( /\S/.test( value ) ) ) { + if ( textnode === range.endContainer ) { + value = value.slice( 0, range.endOffset ); } - }); - return textContent; - }, - - // --- - - _insertNode: function ( node ) { - // Insert at start. - var startContainer = this.startContainer, - startOffset = this.startOffset, - endContainer = this.endContainer, - endOffset = this.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 ( this.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 ); + if ( textnode === range.startContainer ) { + value = value.slice( range.startOffset ); } - startContainer = parent; - } else { - children = startContainer.childNodes; + textContent += value; } + }); + return textContent; +}; - childCount = children.length; +// --- - if ( startOffset === childCount) { - startContainer.appendChild( node ); - } else { - startContainer.insertBefore( node, children[ startOffset ] ); - } - if ( startContainer === endContainer ) { - endOffset += children.length - childCount; - } +RangePrototype._insertNode = function ( node ) { + // Insert at start. + var startContainer = this.startContainer, + startOffset = this.startOffset, + endContainer = this.endContainer, + endOffset = this.endOffset, + parent, children, childCount, afterSplit; - this.setStart( startContainer, startOffset ); - this.setEnd( endContainer, endOffset ); - - return this; - }, - - _extractContents: function ( common ) { - var startContainer = this.startContainer, - startOffset = this.startOffset, - endContainer = this.endContainer, - endOffset = this.endOffset; - - if ( !common ) { - common = this.commonAncestorContainer; - } - - if ( common.nodeType === TEXT_NODE ) { - common = common.parentNode; - } - - var endNode = endContainer.split( endOffset, common ), - startNode = startContainer.split( startOffset, common ), - frag = common.ownerDocument.createDocumentFragment(), - next; - - // End node will be null if at end of child nodes list. - while ( startNode !== endNode ) { - next = startNode.nextSibling; - frag.appendChild( startNode ); - startNode = next; - } - - this.setStart( common, endNode ? - indexOf.call( common.childNodes, endNode ) : - common.childNodes.length ); - this.collapse( true ); - - common.fixCursor(); - - return frag; - }, - - _deleteContents: function () { - // Move boundaries up as much as possible to reduce need to split. - this.moveBoundariesUpTree(); - - // Remove selected range - this._extractContents(); - - // If we split into two different blocks, merge the blocks. - var startBlock = this.getStartBlock(), - endBlock = this.getEndBlock(); - if ( startBlock && endBlock && startBlock !== endBlock ) { - startBlock.mergeWithBlock( endBlock, this ); - } - - // Ensure block has necessary children - if ( startBlock ) { - startBlock.fixCursor(); - } - - // Ensure body has a block-level element in it. - var body = this.endContainer.ownerDocument.body, - child = body.firstChild; - if ( !child || child.nodeName === 'BR' ) { - body.fixCursor(); - this.selectNodeContents( body.firstChild ); - } - - // Ensure valid range (must have only block or inline containers) - var isCollapsed = this.collapsed; - this.moveBoundariesDownTree(); - if ( isCollapsed ) { - // Collapse - this.collapse( true ); - } - - return this; - }, - - // --- - - insertTreeFragment: function ( frag ) { - // Check if it's all inline content - var isInline = true, - children = frag.childNodes, - l = children.length; - while ( l-- ) { - if ( !children[l].isInline() ) { - isInline = false; - break; - } - } - - // Delete any selected content - if ( !this.collapsed ) { - this._deleteContents(); - } - - // Move range down into text ndoes - this.moveBoundariesDownTree(); - - // If inline, just insert at the current position. - if ( isInline ) { - this._insertNode( frag ); - this.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 = this.startContainer.split( this.startOffset, - this.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 ) && child.isInline() ) { - startContainer.appendChild( child ); - } - while ( ( child = frag.lastChild ) && child.isInline() ) { - endContainer.insertBefore( child, endContainer.firstChild ); - endOffset += 1; - } - - // Fix cursor then insert block(s) - node = frag; - while ( node = node.getNextBlock() ) { - node.fixCursor(); - } - 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 { - nodeAfterSplit.mergeContainers(); - } - if ( !nodeAfterSplit.parentNode ) { - endContainer = node; - endOffset = endContainer.getLength(); - } - - if ( !nodeBeforeSplit.textContent) { - startContainer = nodeBeforeSplit.nextSibling; - startOffset = 0; - parent.removeChild( nodeBeforeSplit ); - } else { - nodeBeforeSplit.mergeContainers(); - } - - this.setStart( startContainer, startOffset ); - this.setEnd( endContainer, endOffset ); - this.moveBoundariesDownTree(); - } - }, - - // --- - - containsNode: function ( node, partial ) { - var range = this, - 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 ); - } - }, - - moveBoundariesDownTree: function () { - var startContainer = this.startContainer, - startOffset = this.startOffset, - endContainer = this.endContainer, - endOffset = this.endOffset, - child; - - while ( startContainer.nodeType !== TEXT_NODE ) { - child = startContainer.childNodes[ startOffset ]; - if ( !child || child.isLeaf() ) { - break; - } - startContainer = child; - startOffset = 0; - } - if ( endOffset ) { - while ( endContainer.nodeType !== TEXT_NODE ) { - child = endContainer.childNodes[ endOffset - 1 ]; - if ( !child || child.isLeaf() ) { - break; - } - endContainer = child; - endOffset = endContainer.getLength(); + // 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 ( this.collapsed ) { + endContainer = parent; + endOffset = startOffset; } } else { - while ( endContainer.nodeType !== TEXT_NODE ) { - child = endContainer.firstChild; - if ( !child || child.isLeaf() ) { - 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 ( this.collapsed ) { - this.setStart( endContainer, endOffset ); - this.setEnd( startContainer, startOffset ); - } else { - this.setStart( startContainer, startOffset ); - this.setEnd( endContainer, endOffset ); - } - - return this; - }, - - moveBoundariesUpTree: function ( common ) { - var startContainer = this.startContainer, - startOffset = this.startOffset, - endContainer = this.endContainer, - endOffset = this.endOffset, - parent; - - if ( !common ) { - common = this.commonAncestorContainer; - } - - while ( startContainer !== common && !startOffset ) { - parent = startContainer.parentNode; - startOffset = indexOf.call( parent.childNodes, startContainer ); - startContainer = parent; - } - - while ( endContainer !== common && - endOffset === endContainer.getLength() ) { - parent = endContainer.parentNode; - endOffset = indexOf.call( parent.childNodes, endContainer ) + 1; - endContainer = parent; - } - - this.setStart( startContainer, startOffset ); - this.setEnd( endContainer, endOffset ); - - return this; - }, - - // Returns the first block at least partially contained by the range, - // or null if no block is contained by the range. - getStartBlock: function () { - var container = this.startContainer, - block; - - // If inline, get the containing block. - if ( container.isInline() ) { - block = container.getPreviousBlock(); - } else if ( container.isBlock() ) { - block = container; - } else { - block = getNodeBefore( container, this.startOffset ); - block = block.getNextBlock(); - } - // Check the block actually intersects the range - return block && this.containsNode( 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. - getEndBlock: function () { - var container = this.endContainer, - block, child; - - // If inline, get the containing block. - if ( container.isInline() ) { - block = container.getPreviousBlock(); - } else if ( container.isBlock() ) { - block = container; - } else { - block = getNodeAfter( container, this.endOffset ); - if ( !block ) { - block = container.ownerDocument.body; - while ( child = block.lastChild ) { - block = child; - } - } - block = block.getPreviousBlock(); - - } - // Check the block actually intersects the range - return block && this.containsNode( block, true ) ? block : null; - }, - - startsAtBlockBoundary: function () { - var startContainer = this.startContainer, - startOffset = this.startOffset, - parent, child; - - while ( startContainer.isInline() ) { if ( startOffset ) { - return false; + afterSplit = startContainer.splitText( startOffset ); + if ( endContainer === startContainer ) { + endOffset -= startOffset; + endContainer = afterSplit; + } + else if ( endContainer === parent ) { + endOffset += 1; + } + startContainer = afterSplit; } - parent = startContainer.parentNode; - startOffset = indexOf.call( parent.childNodes, startContainer ); - startContainer = parent; + startOffset = indexOf.call( children, startContainer ); } - // Skip empty text nodes and
s. - while ( startOffset && - ( child = startContainer.childNodes[ startOffset - 1 ] ) && - ( child.data === '' || child.nodeName === 'BR' ) ) { - startOffset -= 1; - } - return !startOffset; - }, + startContainer = parent; + } else { + children = startContainer.childNodes; + } - endsAtBlockBoundary: function () { - var endContainer = this.endContainer, - endOffset = this.endOffset, - length = endContainer.getLength(), - parent, child; + childCount = children.length; - while ( endContainer.isInline() ) { - if ( endOffset !== length ) { - return false; - } - parent = endContainer.parentNode; - endOffset = indexOf.call( parent.childNodes, endContainer ) + 1; - endContainer = parent; - length = endContainer.childNodes.length; + if ( startOffset === childCount) { + startContainer.appendChild( node ); + } else { + startContainer.insertBefore( node, children[ startOffset ] ); + } + if ( startContainer === endContainer ) { + endOffset += children.length - childCount; + } + + this.setStart( startContainer, startOffset ); + this.setEnd( endContainer, endOffset ); + + return this; +}; + +RangePrototype._extractContents = function ( common ) { + var startContainer = this.startContainer, + startOffset = this.startOffset, + endContainer = this.endContainer, + endOffset = this.endOffset; + + if ( !common ) { + common = this.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; + + // End node will be null if at end of child nodes list. + while ( startNode !== endNode ) { + next = startNode.nextSibling; + frag.appendChild( startNode ); + startNode = next; + } + + this.setStart( common, endNode ? + indexOf.call( common.childNodes, endNode ) : + common.childNodes.length ); + this.collapse( true ); + + fixCursor( common ); + + return frag; +}; + +RangePrototype._deleteContents = function () { + // Move boundaries up as much as possible to reduce need to split. + this.moveBoundariesUpTree(); + + // Remove selected range + this._extractContents(); + + // If we split into two different blocks, merge the blocks. + var startBlock = this.getStartBlock(), + endBlock = this.getEndBlock(); + if ( startBlock && endBlock && startBlock !== endBlock ) { + mergeWithBlock( startBlock, endBlock, this ); + } + + // Ensure block has necessary children + if ( startBlock ) { + fixCursor( startBlock ); + } + + // Ensure body has a block-level element in it. + var body = this.endContainer.ownerDocument.body, + child = body.firstChild; + if ( !child || child.nodeName === 'BR' ) { + fixCursor( body ); + this.selectNodeContents( body.firstChild ); + } + + // Ensure valid range (must have only block or inline containers) + var isCollapsed = this.collapsed; + this.moveBoundariesDownTree(); + if ( isCollapsed ) { + // Collapse + this.collapse( true ); + } + + return this; +}; + +// --- + +RangePrototype.insertTreeFragment = function ( 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; } - // Skip empty text nodes and
s. - while ( endOffset < length && - ( child = endContainer.childNodes[ endOffset ] ) && - ( child.data === '' || child.nodeName === 'BR' ) ) { + } + + // Delete any selected content + if ( !this.collapsed ) { + this._deleteContents(); + } + + // Move range down into text ndoes + this.moveBoundariesDownTree(); + + // If inline, just insert at the current position. + if ( allInline ) { + this._insertNode( frag ); + this.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( this.startContainer, this.startOffset, + this.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; } - return endOffset === length; - }, - expandToBlockBoundaries: function () { - var start = this.getStartBlock(), - end = this.getEndBlock(), - parent; + // Fix cursor then insert block(s) + node = frag; + while ( node = getNextBlock( node ) ) { + fixCursor( node ); + } + parent.insertBefore( frag, nodeAfterSplit ); - if ( start && end ) { - parent = start.parentNode; - this.setStart( parent, indexOf.call( parent.childNodes, start ) ); - parent = end.parentNode; - this.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 ); + // 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 ); } - return this; + if ( !nodeBeforeSplit.textContent) { + startContainer = nodeBeforeSplit.nextSibling; + startOffset = 0; + parent.removeChild( nodeBeforeSplit ); + } else { + mergeContainers( nodeBeforeSplit ); + } + + this.setStart( startContainer, startOffset ); + this.setEnd( endContainer, endOffset ); + this.moveBoundariesDownTree(); } }; -var prop; -for ( prop in RangePrototypeExtensions ) { - Range.prototype[ prop ] = RangePrototypeExtensions[ prop ]; -} +// --- -}( DOMTreeWalker ) ); +RangePrototype.containsNode = function ( node, partial ) { + var range = this, + 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 ); + } +}; + +RangePrototype.moveBoundariesDownTree = function () { + var startContainer = this.startContainer, + startOffset = this.startOffset, + endContainer = this.endContainer, + endOffset = this.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 ( this.collapsed ) { + this.setStart( endContainer, endOffset ); + this.setEnd( startContainer, startOffset ); + } else { + this.setStart( startContainer, startOffset ); + this.setEnd( endContainer, endOffset ); + } + + return this; +}; + +RangePrototype.moveBoundariesUpTree = function ( common ) { + var startContainer = this.startContainer, + startOffset = this.startOffset, + endContainer = this.endContainer, + endOffset = this.endOffset, + parent; + + if ( !common ) { + common = this.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; + } + + this.setStart( startContainer, startOffset ); + this.setEnd( endContainer, endOffset ); + + return this; +}; + +// Returns the first block at least partially contained by the range, +// or null if no block is contained by the range. +RangePrototype.getStartBlock = function () { + var container = this.startContainer, + block; + + // If inline, get the containing block. + if ( isInline( container ) ) { + block = getPreviousBlock( container ); + } else if ( isBlock( container ) ) { + block = container; + } else { + block = getNodeBefore( container, this.startOffset ); + block = getNextBlock( block ); + } + // Check the block actually intersects the range + return block && this.containsNode( 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. +RangePrototype.getEndBlock = function () { + var container = this.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, this.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 && this.containsNode( block, true ) ? block : null; +}; + +RangePrototype.startsAtBlockBoundary = function () { + var startContainer = this.startContainer, + startOffset = this.startOffset, + parent, child; + + while ( isInline( startContainer ) ) { + if ( startOffset ) { + return false; + } + parent = startContainer.parentNode; + startOffset = indexOf.call( parent.childNodes, startContainer ); + startContainer = parent; + } + // Skip empty text nodes and
s. + while ( startOffset && + ( child = startContainer.childNodes[ startOffset - 1 ] ) && + ( child.data === '' || child.nodeName === 'BR' ) ) { + startOffset -= 1; + } + return !startOffset; +}; + +RangePrototype.endsAtBlockBoundary = function () { + var endContainer = this.endContainer, + endOffset = this.endOffset, + length = getLength( endContainer ), + parent, child; + + while ( isInline( endContainer ) ) { + if ( endOffset !== length ) { + return false; + } + parent = endContainer.parentNode; + endOffset = indexOf.call( parent.childNodes, endContainer ) + 1; + endContainer = parent; + length = endContainer.childNodes.length; + } + // Skip empty text nodes and
s. + while ( endOffset < length && + ( child = endContainer.childNodes[ endOffset ] ) && + ( child.data === '' || child.nodeName === 'BR' ) ) { + endOffset += 1; + } + return endOffset === length; +}; + +RangePrototype.expandToBlockBoundaries = function () { + var start = this.getStartBlock(), + end = this.getEndBlock(), + parent; + + if ( start && end ) { + parent = start.parentNode; + this.setStart( parent, indexOf.call( parent.childNodes, start ) ); + parent = end.parentNode; + this.setEnd( parent, indexOf.call( parent.childNodes, end ) + 1 ); + } + + return this; +}; diff --git a/source/TreeWalker.js b/source/TreeWalker.js index b6d423b..8bf7ffc 100644 --- a/source/TreeWalker.js +++ b/source/TreeWalker.js @@ -1,6 +1,5 @@ -/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license. */ - -/*global document, window */ +/*global FILTER_ACCEPT */ +/*jshint strict:false */ /* Native TreeWalker is buggy in IE and Opera: @@ -13,90 +12,80 @@ (subset) of the spec in all browsers. */ -var DOMTreeWalker = (function () { +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 +}; - "use strict"; +function TreeWalker ( root, nodeType, filter ) { + this.root = this.currentNode = root; + this.nodeType = nodeType; + this.filter = filter; +} - 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 - }; - - var FILTER_ACCEPT = 1; - - var TreeWalker = function ( 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 ) === FILTER_ACCEPT ) { - 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 ) { +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 ) { - return null; + break; } - 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 ) === FILTER_ACCEPT ) { - this.currentNode = node; - return node; - } - current = node; + node = current.nextSibling; + if ( !node ) { current = current.parentNode; } } - }; + if ( !node ) { + return null; + } + if ( ( typeToBitArray[ node.nodeType ] & nodeType ) && + filter( node ) === FILTER_ACCEPT ) { + this.currentNode = node; + return node; + } + current = node; + } +}; - return TreeWalker; - -})(); +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 ) === FILTER_ACCEPT ) { + this.currentNode = node; + return node; + } + current = node; + } +}; diff --git a/source/UA.js b/source/UA.js deleted file mode 100644 index 0f0493d..0000000 --- a/source/UA.js +++ /dev/null @@ -1,29 +0,0 @@ -/* Copyright © 2011-2012 by Neil Jenkins. Licensed under the MIT license. */ - -/*global navigator, window */ - -var UA = (function ( win ) { - - "use strict"; - - var ua = navigator.userAgent; - var isOpera = !!win.opera; - var isIE = /Trident\//.test( ua ); - var isWebKit = /WebKit\//.test( ua ); - - return { - // Browser sniffing. Unfortunately necessary. - isOpera: isOpera, - isIE8: ( win.ie === 8 ), - isIE: isIE, - isGecko: /Gecko\//.test( ua ), - isWebKit: isWebKit, - isIOS: /iP(?:ad|hone|od)/.test( ua ), - - // Browser quirks - useTextFixer: isIE || isOpera, - cantFocusEmptyTextNodes: isIE || isWebKit, - losesSelectionOnBlur: isIE - }; - -})( window ); diff --git a/source/intro.js b/source/intro.js new file mode 100644 index 0000000..2470de2 --- /dev/null +++ b/source/intro.js @@ -0,0 +1,5 @@ +/* Copyright © 2011-2013 by Neil Jenkins. MIT Licensed. */ + +( function ( doc ) { + +"use strict"; diff --git a/source/outro.js b/source/outro.js new file mode 100644 index 0000000..31fe0ba --- /dev/null +++ b/source/outro.js @@ -0,0 +1 @@ +}( document ) );