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 ) );