diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 00000000..a339d89d --- /dev/null +++ b/Changelog.md @@ -0,0 +1,4 @@ + +# v0.1.0 (2014-01-24) + +- Setup Versioning diff --git a/Gruntfile.js b/Gruntfile.js index 0eb24d96..e99493d0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -64,7 +64,6 @@ module.exports = function(grunt) { } }, clean: { - dist: ['.tmp', 'dist'], server: '.tmp' }, jshint: { @@ -94,7 +93,7 @@ module.exports = function(grunt) { concat: { dist: { files: { - 'dist/editable.js': [ + 'editable.js': [ 'vendor/rangy-1.2.3/rangy-core.js', 'vendor/rangy-1.2.3/rangy-selectionsaverestore.js', 'vendor/bowser.js', @@ -125,8 +124,8 @@ module.exports = function(grunt) { uglify: { dist: { files: { - 'dist/editable.min.js': [ - 'dist/editable.js' + 'editable.min.js': [ + 'editable.js' ], } } @@ -135,12 +134,18 @@ module.exports = function(grunt) { lukas: { files: [{ expand: true, - cwd: 'dist/', - src: ['*'], + src: ['editable.js'], dest: '../livingdocs-engine/vendor/editableJS/' }] } + }, + bump: { + options: { + files: ['package.json', 'bower.json'], + commitFiles: ['package.json', 'bower.json', 'Changelog.md'], // '-a' for all files + pushTo: 'origin' + } } }); @@ -175,7 +180,6 @@ module.exports = function(grunt) { grunt.registerTask('build', [ 'jshint', - 'clean:dist', 'clean:server', 'concat:editable', // 'karma:build', @@ -189,4 +193,18 @@ module.exports = function(grunt) { ]); grunt.registerTask('default', ['server']); + + + // Release a new version + // Only do this on the `master` branch. + // + // options: + // release:patch + // release:minor + // release:major + grunt.registerTask('release', function (type) { + type = type ? type : 'patch'; + grunt.task.run('bump:' + type); + }); + }; diff --git a/bower.json b/bower.json new file mode 100644 index 00000000..bc4e927d --- /dev/null +++ b/bower.json @@ -0,0 +1,16 @@ +{ + "name": "EditableJS", + "version": "0.1.0", + "homepage": "https://github.com/upfrontIO/Editable.JS", + "authors": [ + "Matteo Agosti", + "Lukas Peyer" + ], + "description": "Friendly contenteditable API", + "license": "MIT", + "ignore": [ + "**/**", + "!editable.js", + "!editable.min.js" + ] +} diff --git a/editable.js b/editable.js new file mode 100644 index 00000000..d2a26740 --- /dev/null +++ b/editable.js @@ -0,0 +1,6240 @@ +/** + * @license Rangy, a cross-browser JavaScript range and selection library + * http://code.google.com/p/rangy/ + * + * Copyright 2012, Tim Down + * Licensed under the MIT license. + * Version: 1.2.3 + * Build date: 26 February 2012 + */ +window['rangy'] = (function() { + + + var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined"; + + var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer", "START_TO_START", "START_TO_END", "END_TO_START", "END_TO_END"]; + + var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", + "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents", + "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; + + var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; + + // Subset of TextRange's full set of methods that we're interested in + var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "getBookmark", "moveToBookmark", + "moveToElementText", "parentElement", "pasteHTML", "select", "setEndPoint", "getBoundingClientRect"]; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Trio of functions taken from Peter Michaux's article: + // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting + function isHostMethod(o, p) { + var t = typeof o[p]; + return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown"; + } + + function isHostObject(o, p) { + return !!(typeof o[p] == OBJECT && o[p]); + } + + function isHostProperty(o, p) { + return typeof o[p] != UNDEFINED; + } + + // Creates a convenience function to save verbose repeated calls to tests functions + function createMultiplePropertyTest(testFunc) { + return function(o, props) { + var i = props.length; + while (i--) { + if (!testFunc(o, props[i])) { + return false; + } + } + return true; + }; + } + + // Next trio of functions are a convenience to save verbose repeated calls to previous two functions + var areHostMethods = createMultiplePropertyTest(isHostMethod); + var areHostObjects = createMultiplePropertyTest(isHostObject); + var areHostProperties = createMultiplePropertyTest(isHostProperty); + + function isTextRange(range) { + return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); + } + + var api = { + version: "1.2.3", + initialized: false, + supported: true, + + util: { + isHostMethod: isHostMethod, + isHostObject: isHostObject, + isHostProperty: isHostProperty, + areHostMethods: areHostMethods, + areHostObjects: areHostObjects, + areHostProperties: areHostProperties, + isTextRange: isTextRange + }, + + features: {}, + + modules: {}, + config: { + alertOnWarn: false, + preferTextRange: false + } + }; + + function fail(reason) { + window.alert("Rangy not supported in your browser. Reason: " + reason); + api.initialized = true; + api.supported = false; + } + + api.fail = fail; + + function warn(msg) { + var warningMessage = "Rangy warning: " + msg; + if (api.config.alertOnWarn) { + window.alert(warningMessage); + } else if (typeof window.console != UNDEFINED && typeof window.console.log != UNDEFINED) { + window.console.log(warningMessage); + } + } + + api.warn = warn; + + if ({}.hasOwnProperty) { + api.util.extend = function(o, props) { + for (var i in props) { + if (props.hasOwnProperty(i)) { + o[i] = props[i]; + } + } + }; + } else { + fail("hasOwnProperty not supported"); + } + + var initListeners = []; + var moduleInitializers = []; + + // Initialization + function init() { + if (api.initialized) { + return; + } + var testRange; + var implementsDomRange = false, implementsTextRange = false; + + // First, perform basic feature tests + + if (isHostMethod(document, "createRange")) { + testRange = document.createRange(); + if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { + implementsDomRange = true; + } + testRange.detach(); + } + + var body = isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0]; + + if (body && isHostMethod(body, "createTextRange")) { + testRange = body.createTextRange(); + if (isTextRange(testRange)) { + implementsTextRange = true; + } + } + + if (!implementsDomRange && !implementsTextRange) { + fail("Neither Range nor TextRange are implemented"); + } + + api.initialized = true; + api.features = { + implementsDomRange: implementsDomRange, + implementsTextRange: implementsTextRange + }; + + // Initialize modules and call init listeners + var allListeners = moduleInitializers.concat(initListeners); + for (var i = 0, len = allListeners.length; i < len; ++i) { + try { + allListeners[i](api); + } catch (ex) { + if (isHostObject(window, "console") && isHostMethod(window.console, "log")) { + window.console.log("Init listener threw an exception. Continuing.", ex); + } + + } + } + } + + // Allow external scripts to initialize this library in case it's loaded after the document has loaded + api.init = init; + + // Execute listener immediately if already initialized + api.addInitListener = function(listener) { + if (api.initialized) { + listener(api); + } else { + initListeners.push(listener); + } + }; + + var createMissingNativeApiListeners = []; + + api.addCreateMissingNativeApiListener = function(listener) { + createMissingNativeApiListeners.push(listener); + }; + + function createMissingNativeApi(win) { + win = win || window; + init(); + + // Notify listeners + for (var i = 0, len = createMissingNativeApiListeners.length; i < len; ++i) { + createMissingNativeApiListeners[i](win); + } + } + + api.createMissingNativeApi = createMissingNativeApi; + + /** + * @constructor + */ + function Module(name) { + this.name = name; + this.initialized = false; + this.supported = false; + } + + Module.prototype.fail = function(reason) { + this.initialized = true; + this.supported = false; + + throw new Error("Module '" + this.name + "' failed to load: " + reason); + }; + + Module.prototype.warn = function(msg) { + api.warn("Module " + this.name + ": " + msg); + }; + + Module.prototype.createError = function(msg) { + return new Error("Error in Rangy " + this.name + " module: " + msg); + }; + + api.createModule = function(name, initFunc) { + var module = new Module(name); + api.modules[name] = module; + + moduleInitializers.push(function(api) { + initFunc(api, module); + module.initialized = true; + module.supported = true; + }); + }; + + api.requireModules = function(modules) { + for (var i = 0, len = modules.length, module, moduleName; i < len; ++i) { + moduleName = modules[i]; + module = api.modules[moduleName]; + if (!module || !(module instanceof Module)) { + throw new Error("Module '" + moduleName + "' not found"); + } + if (!module.supported) { + throw new Error("Module '" + moduleName + "' not supported"); + } + } + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Wait for document to load before running tests + + var docReady = false; + + var loadHandler = function(e) { + + if (!docReady) { + docReady = true; + if (!api.initialized) { + init(); + } + } + }; + + // Test whether we have window and document objects that we will need + if (typeof window == UNDEFINED) { + fail("No window found"); + return; + } + if (typeof document == UNDEFINED) { + fail("No document found"); + return; + } + + if (isHostMethod(document, "addEventListener")) { + document.addEventListener("DOMContentLoaded", loadHandler, false); + } + + // Add a fallback in case the DOMContentLoaded event isn't supported + if (isHostMethod(window, "addEventListener")) { + window.addEventListener("load", loadHandler, false); + } else if (isHostMethod(window, "attachEvent")) { + window.attachEvent("onload", loadHandler); + } else { + fail("Window does not have required addEventListener or attachEvent method"); + } + + return api; +})(); +rangy.createModule("DomUtil", function(api, module) { + + var UNDEF = "undefined"; + var util = api.util; + + // Perform feature tests + if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { + module.fail("document missing a Node creation method"); + } + + if (!util.isHostMethod(document, "getElementsByTagName")) { + module.fail("document missing getElementsByTagName method"); + } + + var el = document.createElement("div"); + if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { + module.fail("Incomplete Element implementation"); + } + + // innerHTML is required for Range's createContextualFragment method + if (!util.isHostProperty(el, "innerHTML")) { + module.fail("Element is missing innerHTML property"); + } + + var textNode = document.createTextNode("test"); + if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || + !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || + !util.areHostProperties(textNode, ["data"]))) { + module.fail("Incomplete Text Node implementation"); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been + // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that + // contains just the document as a single element and the value searched for is the document. + var arrayContains = /*Array.prototype.indexOf ? + function(arr, val) { + return arr.indexOf(val) > -1; + }:*/ + + function(arr, val) { + var i = arr.length; + while (i--) { + if (arr[i] === val) { + return true; + } + } + return false; + }; + + // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI + function isHtmlNamespace(node) { + var ns; + return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"); + } + + function parentElement(node) { + var parent = node.parentNode; + return (parent.nodeType == 1) ? parent : null; + } + + function getNodeIndex(node) { + var i = 0; + while( (node = node.previousSibling) ) { + i++; + } + return i; + } + + function getNodeLength(node) { + var childNodes; + return isCharacterDataNode(node) ? node.length : ((childNodes = node.childNodes) ? childNodes.length : 0); + } + + function getCommonAncestor(node1, node2) { + var ancestors = [], n; + for (n = node1; n; n = n.parentNode) { + ancestors.push(n); + } + + for (n = node2; n; n = n.parentNode) { + if (arrayContains(ancestors, n)) { + return n; + } + } + + return null; + } + + function isAncestorOf(ancestor, descendant, selfIsAncestor) { + var n = selfIsAncestor ? descendant : descendant.parentNode; + while (n) { + if (n === ancestor) { + return true; + } else { + n = n.parentNode; + } + } + return false; + } + + function getClosestAncestorIn(node, ancestor, selfIsAncestor) { + var p, n = selfIsAncestor ? node : node.parentNode; + while (n) { + p = n.parentNode; + if (p === ancestor) { + return n; + } + n = p; + } + return null; + } + + function isCharacterDataNode(node) { + var t = node.nodeType; + return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment + } + + function insertAfter(node, precedingNode) { + var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode; + if (nextNode) { + parent.insertBefore(node, nextNode); + } else { + parent.appendChild(node); + } + return node; + } + + // Note that we cannot use splitText() because it is bugridden in IE 9. + function splitDataNode(node, index) { + var newNode = node.cloneNode(false); + newNode.deleteData(0, index); + node.deleteData(index, node.length - index); + insertAfter(newNode, node); + return newNode; + } + + function getDocument(node) { + if (node.nodeType == 9) { + return node; + } else if (typeof node.ownerDocument != UNDEF) { + return node.ownerDocument; + } else if (typeof node.document != UNDEF) { + return node.document; + } else if (node.parentNode) { + return getDocument(node.parentNode); + } else { + throw new Error("getDocument: no document found for node"); + } + } + + function getWindow(node) { + var doc = getDocument(node); + if (typeof doc.defaultView != UNDEF) { + return doc.defaultView; + } else if (typeof doc.parentWindow != UNDEF) { + return doc.parentWindow; + } else { + throw new Error("Cannot get a window object for node"); + } + } + + function getIframeDocument(iframeEl) { + if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument; + } else if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow.document; + } else { + throw new Error("getIframeWindow: No Document object found for iframe element"); + } + } + + function getIframeWindow(iframeEl) { + if (typeof iframeEl.contentWindow != UNDEF) { + return iframeEl.contentWindow; + } else if (typeof iframeEl.contentDocument != UNDEF) { + return iframeEl.contentDocument.defaultView; + } else { + throw new Error("getIframeWindow: No Window object found for iframe element"); + } + } + + function getBody(doc) { + return util.isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; + } + + function getRootContainer(node) { + var parent; + while ( (parent = node.parentNode) ) { + node = parent; + } + return node; + } + + function comparePoints(nodeA, offsetA, nodeB, offsetB) { + // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing + var nodeC, root, childA, childB, n; + if (nodeA == nodeB) { + + // Case 1: nodes are the same + return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) { + + // Case 2: node C (container B or an ancestor) is a child node of A + return offsetA <= getNodeIndex(nodeC) ? -1 : 1; + } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) { + + // Case 3: node C (container A or an ancestor) is a child node of B + return getNodeIndex(nodeC) < offsetB ? -1 : 1; + } else { + + // Case 4: containers are siblings or descendants of siblings + root = getCommonAncestor(nodeA, nodeB); + childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true); + childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true); + + if (childA === childB) { + // This shouldn't be possible + + throw new Error("comparePoints got to case 4 and childA and childB are the same!"); + } else { + n = root.firstChild; + while (n) { + if (n === childA) { + return -1; + } else if (n === childB) { + return 1; + } + n = n.nextSibling; + } + throw new Error("Should not be here!"); + } + } + } + + function fragmentFromNodeChildren(node) { + var fragment = getDocument(node).createDocumentFragment(), child; + while ( (child = node.firstChild) ) { + fragment.appendChild(child); + } + return fragment; + } + + function inspectNode(node) { + if (!node) { + return "[No node]"; + } + if (isCharacterDataNode(node)) { + return '"' + node.data + '"'; + } else if (node.nodeType == 1) { + var idAttr = node.id ? ' id="' + node.id + '"' : ""; + return "<" + node.nodeName + idAttr + ">[" + node.childNodes.length + "]"; + } else { + return node.nodeName; + } + } + + /** + * @constructor + */ + function NodeIterator(root) { + this.root = root; + this._next = root; + } + + NodeIterator.prototype = { + _current: null, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + var n = this._current = this._next; + var child, next; + if (this._current) { + child = n.firstChild; + if (child) { + this._next = child; + } else { + next = null; + while ((n !== this.root) && !(next = n.nextSibling)) { + n = n.parentNode; + } + this._next = next; + } + } + return this._current; + }, + + detach: function() { + this._current = this._next = this.root = null; + } + }; + + function createIterator(root) { + return new NodeIterator(root); + } + + /** + * @constructor + */ + function DomPosition(node, offset) { + this.node = node; + this.offset = offset; + } + + DomPosition.prototype = { + equals: function(pos) { + return this.node === pos.node & this.offset == pos.offset; + }, + + inspect: function() { + return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; + } + }; + + /** + * @constructor + */ + function DOMException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "DOMException: " + this.codeName; + } + + DOMException.prototype = { + INDEX_SIZE_ERR: 1, + HIERARCHY_REQUEST_ERR: 3, + WRONG_DOCUMENT_ERR: 4, + NO_MODIFICATION_ALLOWED_ERR: 7, + NOT_FOUND_ERR: 8, + NOT_SUPPORTED_ERR: 9, + INVALID_STATE_ERR: 11 + }; + + DOMException.prototype.toString = function() { + return this.message; + }; + + api.dom = { + arrayContains: arrayContains, + isHtmlNamespace: isHtmlNamespace, + parentElement: parentElement, + getNodeIndex: getNodeIndex, + getNodeLength: getNodeLength, + getCommonAncestor: getCommonAncestor, + isAncestorOf: isAncestorOf, + getClosestAncestorIn: getClosestAncestorIn, + isCharacterDataNode: isCharacterDataNode, + insertAfter: insertAfter, + splitDataNode: splitDataNode, + getDocument: getDocument, + getWindow: getWindow, + getIframeWindow: getIframeWindow, + getIframeDocument: getIframeDocument, + getBody: getBody, + getRootContainer: getRootContainer, + comparePoints: comparePoints, + inspectNode: inspectNode, + fragmentFromNodeChildren: fragmentFromNodeChildren, + createIterator: createIterator, + DomPosition: DomPosition + }; + + api.DOMException = DOMException; +});rangy.createModule("DomRange", function(api, module) { + api.requireModules( ["DomUtil"] ); + + + var dom = api.dom; + var DomPosition = dom.DomPosition; + var DOMException = api.DOMException; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Utility functions + + function isNonTextPartiallySelected(node, range) { + return (node.nodeType != 3) && + (dom.isAncestorOf(node, range.startContainer, true) || dom.isAncestorOf(node, range.endContainer, true)); + } + + function getRangeDocument(range) { + return dom.getDocument(range.startContainer); + } + + function dispatchEvent(range, type, args) { + var listeners = range._listeners[type]; + if (listeners) { + for (var i = 0, len = listeners.length; i < len; ++i) { + listeners[i].call(range, {target: range, args: args}); + } + } + } + + function getBoundaryBeforeNode(node) { + return new DomPosition(node.parentNode, dom.getNodeIndex(node)); + } + + function getBoundaryAfterNode(node) { + return new DomPosition(node.parentNode, dom.getNodeIndex(node) + 1); + } + + function insertNodeAtPosition(node, n, o) { + var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; + if (dom.isCharacterDataNode(n)) { + if (o == n.length) { + dom.insertAfter(node, n); + } else { + n.parentNode.insertBefore(node, o == 0 ? n : dom.splitDataNode(n, o)); + } + } else if (o >= n.childNodes.length) { + n.appendChild(node); + } else { + n.insertBefore(node, n.childNodes[o]); + } + return firstNodeInserted; + } + + function cloneSubtree(iterator) { + var partiallySelected; + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + partiallySelected = iterator.isPartiallySelectedSubtree(); + + node = node.cloneNode(!partiallySelected); + if (partiallySelected) { + subIterator = iterator.getSubtreeIterator(); + node.appendChild(cloneSubtree(subIterator)); + subIterator.detach(true); + } + + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function iterateSubtree(rangeIterator, func, iteratorState) { + var it, n; + iteratorState = iteratorState || { stop: false }; + for (var node, subRangeIterator; node = rangeIterator.next(); ) { + //log.debug("iterateSubtree, partially selected: " + rangeIterator.isPartiallySelectedSubtree(), nodeToString(node)); + if (rangeIterator.isPartiallySelectedSubtree()) { + // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of the + // node selected by the Range. + if (func(node) === false) { + iteratorState.stop = true; + return; + } else { + subRangeIterator = rangeIterator.getSubtreeIterator(); + iterateSubtree(subRangeIterator, func, iteratorState); + subRangeIterator.detach(true); + if (iteratorState.stop) { + return; + } + } + } else { + // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its + // descendant + it = dom.createIterator(node); + while ( (n = it.next()) ) { + if (func(n) === false) { + iteratorState.stop = true; + return; + } + } + } + } + } + + function deleteSubtree(iterator) { + var subIterator; + while (iterator.next()) { + if (iterator.isPartiallySelectedSubtree()) { + subIterator = iterator.getSubtreeIterator(); + deleteSubtree(subIterator); + subIterator.detach(true); + } else { + iterator.remove(); + } + } + } + + function extractSubtree(iterator) { + + for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) { + + + if (iterator.isPartiallySelectedSubtree()) { + node = node.cloneNode(false); + subIterator = iterator.getSubtreeIterator(); + node.appendChild(extractSubtree(subIterator)); + subIterator.detach(true); + } else { + iterator.remove(); + } + if (node.nodeType == 10) { // DocumentType + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + frag.appendChild(node); + } + return frag; + } + + function getNodesInRange(range, nodeTypes, filter) { + //log.info("getNodesInRange, " + nodeTypes.join(",")); + var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex; + var filterExists = !!filter; + if (filterNodeTypes) { + regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); + } + + var nodes = []; + iterateSubtree(new RangeIterator(range, false), function(node) { + if ((!filterNodeTypes || regex.test(node.nodeType)) && (!filterExists || filter(node))) { + nodes.push(node); + } + }); + return nodes; + } + + function inspect(range) { + var name = (typeof range.getName == "undefined") ? "Range" : range.getName(); + return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + + dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) + + /** + * @constructor + */ + function RangeIterator(range, clonePartiallySelectedTextNodes) { + this.range = range; + this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; + + + + if (!range.collapsed) { + this.sc = range.startContainer; + this.so = range.startOffset; + this.ec = range.endContainer; + this.eo = range.endOffset; + var root = range.commonAncestorContainer; + + if (this.sc === this.ec && dom.isCharacterDataNode(this.sc)) { + this.isSingleCharacterDataNode = true; + this._first = this._last = this._next = this.sc; + } else { + this._first = this._next = (this.sc === root && !dom.isCharacterDataNode(this.sc)) ? + this.sc.childNodes[this.so] : dom.getClosestAncestorIn(this.sc, root, true); + this._last = (this.ec === root && !dom.isCharacterDataNode(this.ec)) ? + this.ec.childNodes[this.eo - 1] : dom.getClosestAncestorIn(this.ec, root, true); + } + + } + } + + RangeIterator.prototype = { + _current: null, + _next: null, + _first: null, + _last: null, + isSingleCharacterDataNode: false, + + reset: function() { + this._current = null; + this._next = this._first; + }, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + // Move to next node + var current = this._current = this._next; + if (current) { + this._next = (current !== this._last) ? current.nextSibling : null; + + // Check for partially selected text nodes + if (dom.isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { + if (current === this.ec) { + + (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); + } + if (this._current === this.sc) { + + (current = current.cloneNode(true)).deleteData(0, this.so); + } + } + } + + return current; + }, + + remove: function() { + var current = this._current, start, end; + + if (dom.isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { + start = (current === this.sc) ? this.so : 0; + end = (current === this.ec) ? this.eo : current.length; + if (start != end) { + current.deleteData(start, end - start); + } + } else { + if (current.parentNode) { + current.parentNode.removeChild(current); + } else { + + } + } + }, + + // Checks if the current node is partially selected + isPartiallySelectedSubtree: function() { + var current = this._current; + return isNonTextPartiallySelected(current, this.range); + }, + + getSubtreeIterator: function() { + var subRange; + if (this.isSingleCharacterDataNode) { + subRange = this.range.cloneRange(); + subRange.collapse(); + } else { + subRange = new Range(getRangeDocument(this.range)); + var current = this._current; + var startContainer = current, startOffset = 0, endContainer = current, endOffset = dom.getNodeLength(current); + + if (dom.isAncestorOf(current, this.sc, true)) { + startContainer = this.sc; + startOffset = this.so; + } + if (dom.isAncestorOf(current, this.ec, true)) { + endContainer = this.ec; + endOffset = this.eo; + } + + updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); + } + return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); + }, + + detach: function(detachRange) { + if (detachRange) { + this.range.detach(); + } + this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; + } + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Exceptions + + /** + * @constructor + */ + function RangeException(codeName) { + this.code = this[codeName]; + this.codeName = codeName; + this.message = "RangeException: " + this.codeName; + } + + RangeException.prototype = { + BAD_BOUNDARYPOINTS_ERR: 1, + INVALID_NODE_TYPE_ERR: 2 + }; + + RangeException.prototype.toString = function() { + return this.message; + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + /** + * Currently iterates through all nodes in the range on creation until I think of a decent way to do it + * TODO: Look into making this a proper iterator, not requiring preloading everything first + * @constructor + */ + function RangeNodeIterator(range, nodeTypes, filter) { + this.nodes = getNodesInRange(range, nodeTypes, filter); + this._next = this.nodes[0]; + this._position = 0; + } + + RangeNodeIterator.prototype = { + _current: null, + + hasNext: function() { + return !!this._next; + }, + + next: function() { + this._current = this._next; + this._next = this.nodes[ ++this._position ]; + return this._current; + }, + + detach: function() { + this._current = this._next = this.nodes = null; + } + }; + + var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; + var rootContainerNodeTypes = [2, 9, 11]; + var readonlyNodeTypes = [5, 6, 10, 12]; + var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; + var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; + + function createAncestorFinder(nodeTypes) { + return function(node, selfIsAncestor) { + var t, n = selfIsAncestor ? node : node.parentNode; + while (n) { + t = n.nodeType; + if (dom.arrayContains(nodeTypes, t)) { + return n; + } + n = n.parentNode; + } + return null; + }; + } + + var getRootContainer = dom.getRootContainer; + var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] ); + var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); + var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] ); + + function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { + if (getDocTypeNotationEntityAncestor(node, allowSelf)) { + throw new RangeException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertNotDetached(range) { + if (!range.startContainer) { + throw new DOMException("INVALID_STATE_ERR"); + } + } + + function assertValidNodeType(node, invalidTypes) { + if (!dom.arrayContains(invalidTypes, node.nodeType)) { + throw new RangeException("INVALID_NODE_TYPE_ERR"); + } + } + + function assertValidOffset(node, offset) { + if (offset < 0 || offset > (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length)) { + throw new DOMException("INDEX_SIZE_ERR"); + } + } + + function assertSameDocumentOrFragment(node1, node2) { + if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + } + + function assertNodeNotReadOnly(node) { + if (getReadonlyAncestor(node, true)) { + throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); + } + } + + function assertNode(node, codeName) { + if (!node) { + throw new DOMException(codeName); + } + } + + function isOrphan(node) { + return !dom.arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true); + } + + function isValidOffset(node, offset) { + return offset <= (dom.isCharacterDataNode(node) ? node.length : node.childNodes.length); + } + + function isRangeValid(range) { + return (!!range.startContainer && !!range.endContainer + && !isOrphan(range.startContainer) + && !isOrphan(range.endContainer) + && isValidOffset(range.startContainer, range.startOffset) + && isValidOffset(range.endContainer, range.endOffset)); + } + + function assertRangeValid(range) { + assertNotDetached(range); + if (!isRangeValid(range)) { + throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")"); + } + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Test the browser's innerHTML support to decide how to implement createContextualFragment + var styleEl = document.createElement("style"); + var htmlParsingConforms = false; + try { + styleEl.innerHTML = "x"; + htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node + } catch (e) { + // IE 6 and 7 throw + } + + api.features.htmlParsingConforms = htmlParsingConforms; + + var createContextualFragment = htmlParsingConforms ? + + // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See + // discussion and base code for this implementation at issue 67. + // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface + // Thanks to Aleks Williams. + function(fragmentStr) { + // "Let node the context object's start's node." + var node = this.startContainer; + var doc = dom.getDocument(node); + + // "If the context object's start's node is null, raise an INVALID_STATE_ERR + // exception and abort these steps." + if (!node) { + throw new DOMException("INVALID_STATE_ERR"); + } + + // "Let element be as follows, depending on node's interface:" + // Document, Document Fragment: null + var el = null; + + // "Element: node" + if (node.nodeType == 1) { + el = node; + + // "Text, Comment: node's parentElement" + } else if (dom.isCharacterDataNode(node)) { + el = dom.parentElement(node); + } + + // "If either element is null or element's ownerDocument is an HTML document + // and element's local name is "html" and element's namespace is the HTML + // namespace" + if (el === null || ( + el.nodeName == "HTML" + && dom.isHtmlNamespace(dom.getDocument(el).documentElement) + && dom.isHtmlNamespace(el) + )) { + + // "let element be a new Element with "body" as its local name and the HTML + // namespace as its namespace."" + el = doc.createElement("body"); + } else { + el = el.cloneNode(false); + } + + // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." + // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." + // "In either case, the algorithm must be invoked with fragment as the input + // and element as the context element." + el.innerHTML = fragmentStr; + + // "If this raises an exception, then abort these steps. Otherwise, let new + // children be the nodes returned." + + // "Let fragment be a new DocumentFragment." + // "Append all new children to fragment." + // "Return fragment." + return dom.fragmentFromNodeChildren(el); + } : + + // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that + // previous versions of Rangy used (with the exception of using a body element rather than a div) + function(fragmentStr) { + assertNotDetached(this); + var doc = getRangeDocument(this); + var el = doc.createElement("body"); + el.innerHTML = fragmentStr; + + return dom.fragmentFromNodeChildren(el); + }; + + /*----------------------------------------------------------------------------------------------------------------*/ + + var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", + "commonAncestorContainer"]; + + var s2s = 0, s2e = 1, e2e = 2, e2s = 3; + var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3; + + function RangePrototype() {} + + RangePrototype.prototype = { + attachListener: function(type, listener) { + this._listeners[type].push(listener); + }, + + compareBoundaryPoints: function(how, range) { + assertRangeValid(this); + assertSameDocumentOrFragment(this.startContainer, range.startContainer); + + var nodeA, offsetA, nodeB, offsetB; + var prefixA = (how == e2s || how == s2s) ? "start" : "end"; + var prefixB = (how == s2e || how == s2s) ? "start" : "end"; + nodeA = this[prefixA + "Container"]; + offsetA = this[prefixA + "Offset"]; + nodeB = range[prefixB + "Container"]; + offsetB = range[prefixB + "Offset"]; + return dom.comparePoints(nodeA, offsetA, nodeB, offsetB); + }, + + insertNode: function(node) { + assertRangeValid(this); + assertValidNodeType(node, insertableNodeTypes); + assertNodeNotReadOnly(this.startContainer); + + if (dom.isAncestorOf(node, this.startContainer, true)) { + throw new DOMException("HIERARCHY_REQUEST_ERR"); + } + + // No check for whether the container of the start of the Range is of a type that does not allow + // children of the type of node: the browser's DOM implementation should do this for us when we attempt + // to add the node + + var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); + this.setStartBefore(firstNodeInserted); + }, + + cloneContents: function() { + assertRangeValid(this); + + var clone, frag; + if (this.collapsed) { + return getRangeDocument(this).createDocumentFragment(); + } else { + if (this.startContainer === this.endContainer && dom.isCharacterDataNode(this.startContainer)) { + clone = this.startContainer.cloneNode(true); + clone.data = clone.data.slice(this.startOffset, this.endOffset); + frag = getRangeDocument(this).createDocumentFragment(); + frag.appendChild(clone); + return frag; + } else { + var iterator = new RangeIterator(this, true); + clone = cloneSubtree(iterator); + iterator.detach(); + } + return clone; + } + }, + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + surroundContents: function(node) { + assertValidNodeType(node, surroundNodeTypes); + + if (!this.canSurroundContents()) { + throw new RangeException("BAD_BOUNDARYPOINTS_ERR"); + } + + // Extract the contents + var content = this.extractContents(); + + // Clear the children of the node + if (node.hasChildNodes()) { + while (node.lastChild) { + node.removeChild(node.lastChild); + } + } + + // Insert the new node and add the extracted contents + insertNodeAtPosition(node, this.startContainer, this.startOffset); + node.appendChild(content); + + this.selectNode(node); + }, + + cloneRange: function() { + assertRangeValid(this); + var range = new Range(getRangeDocument(this)); + var i = rangeProperties.length, prop; + while (i--) { + prop = rangeProperties[i]; + range[prop] = this[prop]; + } + return range; + }, + + toString: function() { + assertRangeValid(this); + var sc = this.startContainer; + if (sc === this.endContainer && dom.isCharacterDataNode(sc)) { + return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : ""; + } else { + var textBits = [], iterator = new RangeIterator(this, true); + + iterateSubtree(iterator, function(node) { + // Accept only text or CDATA nodes, not comments + + if (node.nodeType == 3 || node.nodeType == 4) { + textBits.push(node.data); + } + }); + iterator.detach(); + return textBits.join(""); + } + }, + + // The methods below are all non-standard. The following batch were introduced by Mozilla but have since + // been removed from Mozilla. + + compareNode: function(node) { + assertRangeValid(this); + + var parent = node.parentNode; + var nodeIndex = dom.getNodeIndex(node); + + if (!parent) { + throw new DOMException("NOT_FOUND_ERR"); + } + + var startComparison = this.comparePoint(parent, nodeIndex), + endComparison = this.comparePoint(parent, nodeIndex + 1); + + if (startComparison < 0) { // Node starts before + return (endComparison > 0) ? n_b_a : n_b; + } else { + return (endComparison > 0) ? n_a : n_i; + } + }, + + comparePoint: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + if (dom.comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { + return -1; + } else if (dom.comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { + return 1; + } + return 0; + }, + + createContextualFragment: createContextualFragment, + + toHtml: function() { + assertRangeValid(this); + var container = getRangeDocument(this).createElement("div"); + container.appendChild(this.cloneContents()); + return container.innerHTML; + }, + + // touchingIsIntersecting determines whether this method considers a node that borders a range intersects + // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) + intersectsNode: function(node, touchingIsIntersecting) { + assertRangeValid(this); + assertNode(node, "NOT_FOUND_ERR"); + if (dom.getDocument(node) !== getRangeDocument(this)) { + return false; + } + + var parent = node.parentNode, offset = dom.getNodeIndex(node); + assertNode(parent, "NOT_FOUND_ERR"); + + var startComparison = dom.comparePoints(parent, offset, this.endContainer, this.endOffset), + endComparison = dom.comparePoints(parent, offset + 1, this.startContainer, this.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, + + + isPointInRange: function(node, offset) { + assertRangeValid(this); + assertNode(node, "HIERARCHY_REQUEST_ERR"); + assertSameDocumentOrFragment(node, this.startContainer); + + return (dom.comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) && + (dom.comparePoints(node, offset, this.endContainer, this.endOffset) <= 0); + }, + + // The methods below are non-standard and invented by me. + + // Sharing a boundary start-to-end or end-to-start does not count as intersection. + intersectsRange: function(range, touchingIsIntersecting) { + assertRangeValid(this); + + if (getRangeDocument(range) != getRangeDocument(this)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + + var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.endContainer, range.endOffset), + endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.startContainer, range.startOffset); + + return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; + }, + + intersection: function(range) { + if (this.intersectsRange(range)) { + var startComparison = dom.comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), + endComparison = dom.comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); + + var intersectionRange = this.cloneRange(); + + if (startComparison == -1) { + intersectionRange.setStart(range.startContainer, range.startOffset); + } + if (endComparison == 1) { + intersectionRange.setEnd(range.endContainer, range.endOffset); + } + return intersectionRange; + } + return null; + }, + + union: function(range) { + if (this.intersectsRange(range, true)) { + var unionRange = this.cloneRange(); + if (dom.comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { + unionRange.setStart(range.startContainer, range.startOffset); + } + if (dom.comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { + unionRange.setEnd(range.endContainer, range.endOffset); + } + return unionRange; + } else { + throw new RangeException("Ranges do not intersect"); + } + }, + + containsNode: function(node, allowPartial) { + if (allowPartial) { + return this.intersectsNode(node, false); + } else { + return this.compareNode(node) == n_i; + } + }, + + containsNodeContents: function(node) { + return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, dom.getNodeLength(node)) <= 0; + }, + + containsRange: function(range) { + return this.intersection(range).equals(range); + }, + + containsNodeText: function(node) { + var nodeRange = this.cloneRange(); + nodeRange.selectNode(node); + var textNodes = nodeRange.getNodes([3]); + if (textNodes.length > 0) { + nodeRange.setStart(textNodes[0], 0); + var lastTextNode = textNodes.pop(); + nodeRange.setEnd(lastTextNode, lastTextNode.length); + var contains = this.containsRange(nodeRange); + nodeRange.detach(); + return contains; + } else { + return this.containsNodeContents(node); + } + }, + + createNodeIterator: function(nodeTypes, filter) { + assertRangeValid(this); + return new RangeNodeIterator(this, nodeTypes, filter); + }, + + getNodes: function(nodeTypes, filter) { + assertRangeValid(this); + return getNodesInRange(this, nodeTypes, filter); + }, + + getDocument: function() { + return getRangeDocument(this); + }, + + collapseBefore: function(node) { + assertNotDetached(this); + + this.setEndBefore(node); + this.collapse(false); + }, + + collapseAfter: function(node) { + assertNotDetached(this); + + this.setStartAfter(node); + this.collapse(true); + }, + + getName: function() { + return "DomRange"; + }, + + equals: function(range) { + return Range.rangesEqual(this, range); + }, + + isValid: function() { + return isRangeValid(this); + }, + + inspect: function() { + return inspect(this); + } + }; + + function copyComparisonConstantsToObject(obj) { + obj.START_TO_START = s2s; + obj.START_TO_END = s2e; + obj.END_TO_END = e2e; + obj.END_TO_START = e2s; + + obj.NODE_BEFORE = n_b; + obj.NODE_AFTER = n_a; + obj.NODE_BEFORE_AND_AFTER = n_b_a; + obj.NODE_INSIDE = n_i; + } + + function copyComparisonConstants(constructor) { + copyComparisonConstantsToObject(constructor); + copyComparisonConstantsToObject(constructor.prototype); + } + + function createRangeContentRemover(remover, boundaryUpdater) { + return function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer; + + var iterator = new RangeIterator(this, true); + + // Work out where to position the range after content removal + var node, boundary; + if (sc !== root) { + node = dom.getClosestAncestorIn(sc, root, true); + boundary = getBoundaryAfterNode(node); + sc = boundary.node; + so = boundary.offset; + } + + // Check none of the range is read-only + iterateSubtree(iterator, assertNodeNotReadOnly); + + iterator.reset(); + + // Remove the content + var returnValue = remover(iterator); + iterator.detach(); + + // Move to the new position + boundaryUpdater(this, sc, so, sc, so); + + return returnValue; + }; + } + + function createPrototypeRange(constructor, boundaryUpdater, detacher) { + function createBeforeAfterNodeSetter(isBefore, isStart) { + return function(node) { + assertNotDetached(this); + assertValidNodeType(node, beforeAfterNodeTypes); + assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); + + var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); + (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); + }; + } + + function setRangeStart(range, node, offset) { + var ec = range.endContainer, eo = range.endOffset; + if (node !== range.startContainer || offset !== range.startOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(ec) || dom.comparePoints(node, offset, ec, eo) == 1) { + ec = node; + eo = offset; + } + boundaryUpdater(range, node, offset, ec, eo); + } + } + + function setRangeEnd(range, node, offset) { + var sc = range.startContainer, so = range.startOffset; + if (node !== range.endContainer || offset !== range.endOffset) { + // Check the root containers of the range and the new boundary, and also check whether the new boundary + // is after the current end. In either case, collapse the range to the new position + if (getRootContainer(node) != getRootContainer(sc) || dom.comparePoints(node, offset, sc, so) == -1) { + sc = node; + so = offset; + } + boundaryUpdater(range, sc, so, node, offset); + } + } + + function setRangeStartAndEnd(range, node, offset) { + if (node !== range.startContainer || offset !== range.startOffset || node !== range.endContainer || offset !== range.endOffset) { + boundaryUpdater(range, node, offset, node, offset); + } + } + + constructor.prototype = new RangePrototype(); + + api.util.extend(constructor.prototype, { + setStart: function(node, offset) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeStart(this, node, offset); + }, + + setEnd: function(node, offset) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeEnd(this, node, offset); + }, + + setStartBefore: createBeforeAfterNodeSetter(true, true), + setStartAfter: createBeforeAfterNodeSetter(false, true), + setEndBefore: createBeforeAfterNodeSetter(true, false), + setEndAfter: createBeforeAfterNodeSetter(false, false), + + collapse: function(isStart) { + assertRangeValid(this); + if (isStart) { + boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); + } else { + boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); + } + }, + + selectNodeContents: function(node) { + // This doesn't seem well specified: the spec talks only about selecting the node's contents, which + // could be taken to mean only its children. However, browsers implement this the same as selectNode for + // text nodes, so I shall do likewise + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, true); + + boundaryUpdater(this, node, 0, node, dom.getNodeLength(node)); + }, + + selectNode: function(node) { + assertNotDetached(this); + assertNoDocTypeNotationEntityAncestor(node, false); + assertValidNodeType(node, beforeAfterNodeTypes); + + var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node); + boundaryUpdater(this, start.node, start.offset, end.node, end.offset); + }, + + extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), + + deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), + + canSurroundContents: function() { + assertRangeValid(this); + assertNodeNotReadOnly(this.startContainer); + assertNodeNotReadOnly(this.endContainer); + + // Check if the contents can be surrounded. Specifically, this means whether the range partially selects + // no non-text nodes. + var iterator = new RangeIterator(this, true); + var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) || + (iterator._last && isNonTextPartiallySelected(iterator._last, this))); + iterator.detach(); + return !boundariesInvalid; + }, + + detach: function() { + detacher(this); + }, + + splitBoundaries: function() { + assertRangeValid(this); + + + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; + var startEndSame = (sc === ec); + + if (dom.isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { + dom.splitDataNode(ec, eo); + + } + + if (dom.isCharacterDataNode(sc) && so > 0 && so < sc.length) { + + sc = dom.splitDataNode(sc, so); + if (startEndSame) { + eo -= so; + ec = sc; + } else if (ec == sc.parentNode && eo >= dom.getNodeIndex(sc)) { + eo++; + } + so = 0; + + } + boundaryUpdater(this, sc, so, ec, eo); + }, + + normalizeBoundaries: function() { + assertRangeValid(this); + + var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset; + + var mergeForward = function(node) { + var sibling = node.nextSibling; + if (sibling && sibling.nodeType == node.nodeType) { + ec = node; + eo = node.length; + node.appendData(sibling.data); + sibling.parentNode.removeChild(sibling); + } + }; + + var mergeBackward = function(node) { + var sibling = node.previousSibling; + if (sibling && sibling.nodeType == node.nodeType) { + sc = node; + var nodeLength = node.length; + so = sibling.length; + node.insertData(0, sibling.data); + sibling.parentNode.removeChild(sibling); + if (sc == ec) { + eo += so; + ec = sc; + } else if (ec == node.parentNode) { + var nodeIndex = dom.getNodeIndex(node); + if (eo == nodeIndex) { + ec = node; + eo = nodeLength; + } else if (eo > nodeIndex) { + eo--; + } + } + } + }; + + var normalizeStart = true; + + if (dom.isCharacterDataNode(ec)) { + if (ec.length == eo) { + mergeForward(ec); + } + } else { + if (eo > 0) { + var endNode = ec.childNodes[eo - 1]; + if (endNode && dom.isCharacterDataNode(endNode)) { + mergeForward(endNode); + } + } + normalizeStart = !this.collapsed; + } + + if (normalizeStart) { + if (dom.isCharacterDataNode(sc)) { + if (so == 0) { + mergeBackward(sc); + } + } else { + if (so < sc.childNodes.length) { + var startNode = sc.childNodes[so]; + if (startNode && dom.isCharacterDataNode(startNode)) { + mergeBackward(startNode); + } + } + } + } else { + sc = ec; + so = eo; + } + + boundaryUpdater(this, sc, so, ec, eo); + }, + + collapseToPoint: function(node, offset) { + assertNotDetached(this); + + assertNoDocTypeNotationEntityAncestor(node, true); + assertValidOffset(node, offset); + + setRangeStartAndEnd(this, node, offset); + } + }); + + copyComparisonConstants(constructor); + } + + /*----------------------------------------------------------------------------------------------------------------*/ + + // Updates commonAncestorContainer and collapsed after boundary change + function updateCollapsedAndCommonAncestor(range) { + range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset); + range.commonAncestorContainer = range.collapsed ? + range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); + } + + function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { + var startMoved = (range.startContainer !== startContainer || range.startOffset !== startOffset); + var endMoved = (range.endContainer !== endContainer || range.endOffset !== endOffset); + + range.startContainer = startContainer; + range.startOffset = startOffset; + range.endContainer = endContainer; + range.endOffset = endOffset; + + updateCollapsedAndCommonAncestor(range); + dispatchEvent(range, "boundarychange", {startMoved: startMoved, endMoved: endMoved}); + } + + function detach(range) { + assertNotDetached(range); + range.startContainer = range.startOffset = range.endContainer = range.endOffset = null; + range.collapsed = range.commonAncestorContainer = null; + dispatchEvent(range, "detach", null); + range._listeners = null; + } + + /** + * @constructor + */ + function Range(doc) { + this.startContainer = doc; + this.startOffset = 0; + this.endContainer = doc; + this.endOffset = 0; + this._listeners = { + boundarychange: [], + detach: [] + }; + updateCollapsedAndCommonAncestor(this); + } + + createPrototypeRange(Range, updateBoundaries, detach); + + api.rangePrototype = RangePrototype.prototype; + + Range.rangeProperties = rangeProperties; + Range.RangeIterator = RangeIterator; + Range.copyComparisonConstants = copyComparisonConstants; + Range.createPrototypeRange = createPrototypeRange; + Range.inspect = inspect; + Range.getRangeDocument = getRangeDocument; + Range.rangesEqual = function(r1, r2) { + return r1.startContainer === r2.startContainer && + r1.startOffset === r2.startOffset && + r1.endContainer === r2.endContainer && + r1.endOffset === r2.endOffset; + }; + + api.DomRange = Range; + api.RangeException = RangeException; +});rangy.createModule("WrappedRange", function(api, module) { + api.requireModules( ["DomUtil", "DomRange"] ); + + /** + * @constructor + */ + var WrappedRange; + var dom = api.dom; + var DomPosition = dom.DomPosition; + var DomRange = api.DomRange; + + + + /*----------------------------------------------------------------------------------------------------------------*/ + + /* + This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() + method. For example, in the following (where pipes denote the selection boundaries): + + + + var range = document.selection.createRange(); + alert(range.parentElement().id); // Should alert "ul" but alerts "b" + + This method returns the common ancestor node of the following: + - the parentElement() of the textRange + - the parentElement() of the textRange after calling collapse(true) + - the parentElement() of the textRange after calling collapse(false) + */ + function getTextRangeContainerElement(textRange) { + var parentEl = textRange.parentElement(); + + var range = textRange.duplicate(); + range.collapse(true); + var startEl = range.parentElement(); + range = textRange.duplicate(); + range.collapse(false); + var endEl = range.parentElement(); + var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl); + + return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); + } + + function textRangeIsCollapsed(textRange) { + return textRange.compareEndPoints("StartToEnd", textRange) == 0; + } + + // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started out as + // an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/) but has + // grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange bugs, handling + // for inputs and images, plus optimizations. + function getTextRangeBoundaryPosition(textRange, wholeRangeContainerElement, isStart, isCollapsed) { + var workingRange = textRange.duplicate(); + + workingRange.collapse(isStart); + var containerElement = workingRange.parentElement(); + + // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so + // check for that + // TODO: Find out when. Workaround for wholeRangeContainerElement may break this + if (!dom.isAncestorOf(wholeRangeContainerElement, containerElement, true)) { + containerElement = wholeRangeContainerElement; + + } + + + + // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and + // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx + if (!containerElement.canHaveHTML) { + return new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); + } + + var workingNode = dom.getDocument(containerElement).createElement("span"); + var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; + var previousNode, nextNode, boundaryPosition, boundaryNode; + + // Move the working range through the container's children, starting at the end and working backwards, until the + // working range reaches or goes past the boundary we're interested in + do { + containerElement.insertBefore(workingNode, workingNode.previousSibling); + workingRange.moveToElementText(workingNode); + } while ( (comparison = workingRange.compareEndPoints(workingComparisonType, textRange)) > 0 && + workingNode.previousSibling); + + // We've now reached or gone past the boundary of the text range we're interested in + // so have identified the node we want + boundaryNode = workingNode.nextSibling; + + if (comparison == -1 && boundaryNode && dom.isCharacterDataNode(boundaryNode)) { + // This is a character data node (text, comment, cdata). The working range is collapsed at the start of the + // node containing the text range's boundary, so we move the end of the working range to the boundary point + // and measure the length of its text to get the boundary's offset within the node. + workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); + + + var offset; + + if (/[\r\n]/.test(boundaryNode.data)) { + /* + For the particular case of a boundary within a text node containing line breaks (within a
 element,
+                for example), we need a slightly complicated approach to get the boundary's offset in IE. The facts:
+
+                - Each line break is represented as \r in the text node's data/nodeValue properties
+                - Each line break is represented as \r\n in the TextRange's 'text' property
+                - The 'text' property of the TextRange does not contain trailing line breaks
+
+                To get round the problem presented by the final fact above, we can use the fact that TextRange's
+                moveStart() and moveEnd() methods return the actual number of characters moved, which is not necessarily
+                the same as the number of characters it was instructed to move. The simplest approach is to use this to
+                store the characters moved when moving both the start and end of the range to the start of the document
+                body and subtracting the start offset from the end offset (the "move-negative-gazillion" method).
+                However, this is extremely slow when the document is large and the range is near the end of it. Clearly
+                doing the mirror image (i.e. moving the range boundaries to the end of the document) has the same
+                problem.
+
+                Another approach that works is to use moveStart() to move the start boundary of the range up to the end
+                boundary one character at a time and incrementing a counter with the value returned by the moveStart()
+                call. However, the check for whether the start boundary has reached the end boundary is expensive, so
+                this method is slow (although unlike "move-negative-gazillion" is largely unaffected by the location of
+                the range within the document).
+
+                The method below is a hybrid of the two methods above. It uses the fact that a string containing the
+                TextRange's 'text' property with each \r\n converted to a single \r character cannot be longer than the
+                text of the TextRange, so the start of the range is moved that length initially and then a character at
+                a time to make up for any trailing line breaks not contained in the 'text' property. This has good
+                performance in most situations compared to the previous two methods.
+                */
+                var tempRange = workingRange.duplicate();
+                var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
+
+                offset = tempRange.moveStart("character", rangeLength);
+                while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
+                    offset++;
+                    tempRange.moveStart("character", 1);
+                }
+            } else {
+                offset = workingRange.text.length;
+            }
+            boundaryPosition = new DomPosition(boundaryNode, offset);
+        } else {
+
+
+            // If the boundary immediately follows a character data node and this is the end boundary, we should favour
+            // a position within that, and likewise for a start boundary preceding a character data node
+            previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
+            nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
+
+
+
+            if (nextNode && dom.isCharacterDataNode(nextNode)) {
+                boundaryPosition = new DomPosition(nextNode, 0);
+            } else if (previousNode && dom.isCharacterDataNode(previousNode)) {
+                boundaryPosition = new DomPosition(previousNode, previousNode.length);
+            } else {
+                boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
+            }
+        }
+
+        // Clean up
+        workingNode.parentNode.removeChild(workingNode);
+
+        return boundaryPosition;
+    }
+
+    // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that node.
+    // This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
+    // (http://code.google.com/p/ierange/)
+    function createBoundaryTextRange(boundaryPosition, isStart) {
+        var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
+        var doc = dom.getDocument(boundaryPosition.node);
+        var workingNode, childNodes, workingRange = doc.body.createTextRange();
+        var nodeIsDataNode = dom.isCharacterDataNode(boundaryPosition.node);
+
+        if (nodeIsDataNode) {
+            boundaryNode = boundaryPosition.node;
+            boundaryParent = boundaryNode.parentNode;
+        } else {
+            childNodes = boundaryPosition.node.childNodes;
+            boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
+            boundaryParent = boundaryPosition.node;
+        }
+
+        // Position the range immediately before the node containing the boundary
+        workingNode = doc.createElement("span");
+
+        // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within the
+        // element rather than immediately before or after it, which is what we want
+        workingNode.innerHTML = "&#feff;";
+
+        // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
+        // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
+        if (boundaryNode) {
+            boundaryParent.insertBefore(workingNode, boundaryNode);
+        } else {
+            boundaryParent.appendChild(workingNode);
+        }
+
+        workingRange.moveToElementText(workingNode);
+        workingRange.collapse(!isStart);
+
+        // Clean up
+        boundaryParent.removeChild(workingNode);
+
+        // Move the working range to the text offset, if required
+        if (nodeIsDataNode) {
+            workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
+        }
+
+        return workingRange;
+    }
+
+    /*----------------------------------------------------------------------------------------------------------------*/
+
+    if (api.features.implementsDomRange && (!api.features.implementsTextRange || !api.config.preferTextRange)) {
+        // This is a wrapper around the browser's native DOM Range. It has two aims:
+        // - Provide workarounds for specific browser bugs
+        // - provide convenient extensions, which are inherited from Rangy's DomRange
+
+        (function() {
+            var rangeProto;
+            var rangeProperties = DomRange.rangeProperties;
+            var canSetRangeStartAfterEnd;
+
+            function updateRangeProperties(range) {
+                var i = rangeProperties.length, prop;
+                while (i--) {
+                    prop = rangeProperties[i];
+                    range[prop] = range.nativeRange[prop];
+                }
+            }
+
+            function updateNativeRange(range, startContainer, startOffset, endContainer,endOffset) {
+                var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
+                var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
+
+                // Always set both boundaries for the benefit of IE9 (see issue 35)
+                if (startMoved || endMoved) {
+                    range.setEnd(endContainer, endOffset);
+                    range.setStart(startContainer, startOffset);
+                }
+            }
+
+            function detach(range) {
+                range.nativeRange.detach();
+                range.detached = true;
+                var i = rangeProperties.length, prop;
+                while (i--) {
+                    prop = rangeProperties[i];
+                    range[prop] = null;
+                }
+            }
+
+            var createBeforeAfterNodeSetter;
+
+            WrappedRange = function(range) {
+                if (!range) {
+                    throw new Error("Range must be specified");
+                }
+                this.nativeRange = range;
+                updateRangeProperties(this);
+            };
+
+            DomRange.createPrototypeRange(WrappedRange, updateNativeRange, detach);
+
+            rangeProto = WrappedRange.prototype;
+
+            rangeProto.selectNode = function(node) {
+                this.nativeRange.selectNode(node);
+                updateRangeProperties(this);
+            };
+
+            rangeProto.deleteContents = function() {
+                this.nativeRange.deleteContents();
+                updateRangeProperties(this);
+            };
+
+            rangeProto.extractContents = function() {
+                var frag = this.nativeRange.extractContents();
+                updateRangeProperties(this);
+                return frag;
+            };
+
+            rangeProto.cloneContents = function() {
+                return this.nativeRange.cloneContents();
+            };
+
+            // TODO: Until I can find a way to programmatically trigger the Firefox bug (apparently long-standing, still
+            // present in 3.6.8) that throws "Index or size is negative or greater than the allowed amount" for
+            // insertNode in some circumstances, all browsers will have to use the Rangy's own implementation of
+            // insertNode, which works but is almost certainly slower than the native implementation.
+/*
+            rangeProto.insertNode = function(node) {
+                this.nativeRange.insertNode(node);
+                updateRangeProperties(this);
+            };
+*/
+
+            rangeProto.surroundContents = function(node) {
+                this.nativeRange.surroundContents(node);
+                updateRangeProperties(this);
+            };
+
+            rangeProto.collapse = function(isStart) {
+                this.nativeRange.collapse(isStart);
+                updateRangeProperties(this);
+            };
+
+            rangeProto.cloneRange = function() {
+                return new WrappedRange(this.nativeRange.cloneRange());
+            };
+
+            rangeProto.refresh = function() {
+                updateRangeProperties(this);
+            };
+
+            rangeProto.toString = function() {
+                return this.nativeRange.toString();
+            };
+
+            // Create test range and node for feature detection
+
+            var testTextNode = document.createTextNode("test");
+            dom.getBody(document).appendChild(testTextNode);
+            var range = document.createRange();
+
+            /*--------------------------------------------------------------------------------------------------------*/
+
+            // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
+            // correct for it
+
+            range.setStart(testTextNode, 0);
+            range.setEnd(testTextNode, 0);
+
+            try {
+                range.setStart(testTextNode, 1);
+                canSetRangeStartAfterEnd = true;
+
+                rangeProto.setStart = function(node, offset) {
+                    this.nativeRange.setStart(node, offset);
+                    updateRangeProperties(this);
+                };
+
+                rangeProto.setEnd = function(node, offset) {
+                    this.nativeRange.setEnd(node, offset);
+                    updateRangeProperties(this);
+                };
+
+                createBeforeAfterNodeSetter = function(name) {
+                    return function(node) {
+                        this.nativeRange[name](node);
+                        updateRangeProperties(this);
+                    };
+                };
+
+            } catch(ex) {
+
+
+                canSetRangeStartAfterEnd = false;
+
+                rangeProto.setStart = function(node, offset) {
+                    try {
+                        this.nativeRange.setStart(node, offset);
+                    } catch (ex) {
+                        this.nativeRange.setEnd(node, offset);
+                        this.nativeRange.setStart(node, offset);
+                    }
+                    updateRangeProperties(this);
+                };
+
+                rangeProto.setEnd = function(node, offset) {
+                    try {
+                        this.nativeRange.setEnd(node, offset);
+                    } catch (ex) {
+                        this.nativeRange.setStart(node, offset);
+                        this.nativeRange.setEnd(node, offset);
+                    }
+                    updateRangeProperties(this);
+                };
+
+                createBeforeAfterNodeSetter = function(name, oppositeName) {
+                    return function(node) {
+                        try {
+                            this.nativeRange[name](node);
+                        } catch (ex) {
+                            this.nativeRange[oppositeName](node);
+                            this.nativeRange[name](node);
+                        }
+                        updateRangeProperties(this);
+                    };
+                };
+            }
+
+            rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
+            rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
+            rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
+            rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
+
+            /*--------------------------------------------------------------------------------------------------------*/
+
+            // Test for and correct Firefox 2 behaviour with selectNodeContents on text nodes: it collapses the range to
+            // the 0th character of the text node
+            range.selectNodeContents(testTextNode);
+            if (range.startContainer == testTextNode && range.endContainer == testTextNode &&
+                    range.startOffset == 0 && range.endOffset == testTextNode.length) {
+                rangeProto.selectNodeContents = function(node) {
+                    this.nativeRange.selectNodeContents(node);
+                    updateRangeProperties(this);
+                };
+            } else {
+                rangeProto.selectNodeContents = function(node) {
+                    this.setStart(node, 0);
+                    this.setEnd(node, DomRange.getEndOffset(node));
+                };
+            }
+
+            /*--------------------------------------------------------------------------------------------------------*/
+
+            // Test for WebKit bug that has the beahviour of compareBoundaryPoints round the wrong way for constants
+            // START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
+
+            range.selectNodeContents(testTextNode);
+            range.setEnd(testTextNode, 3);
+
+            var range2 = document.createRange();
+            range2.selectNodeContents(testTextNode);
+            range2.setEnd(testTextNode, 4);
+            range2.setStart(testTextNode, 2);
+
+            if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &
+                    range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
+                // This is the wrong way round, so correct for it
+
+
+                rangeProto.compareBoundaryPoints = function(type, range) {
+                    range = range.nativeRange || range;
+                    if (type == range.START_TO_END) {
+                        type = range.END_TO_START;
+                    } else if (type == range.END_TO_START) {
+                        type = range.START_TO_END;
+                    }
+                    return this.nativeRange.compareBoundaryPoints(type, range);
+                };
+            } else {
+                rangeProto.compareBoundaryPoints = function(type, range) {
+                    return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
+                };
+            }
+
+            /*--------------------------------------------------------------------------------------------------------*/
+
+            // Test for existence of createContextualFragment and delegate to it if it exists
+            if (api.util.isHostMethod(range, "createContextualFragment")) {
+                rangeProto.createContextualFragment = function(fragmentStr) {
+                    return this.nativeRange.createContextualFragment(fragmentStr);
+                };
+            }
+
+            /*--------------------------------------------------------------------------------------------------------*/
+
+            // Clean up
+            dom.getBody(document).removeChild(testTextNode);
+            range.detach();
+            range2.detach();
+        })();
+
+        api.createNativeRange = function(doc) {
+            doc = doc || document;
+            return doc.createRange();
+        };
+    } else if (api.features.implementsTextRange) {
+        // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
+        // prototype
+
+        WrappedRange = function(textRange) {
+            this.textRange = textRange;
+            this.refresh();
+        };
+
+        WrappedRange.prototype = new DomRange(document);
+
+        WrappedRange.prototype.refresh = function() {
+            var start, end;
+
+            // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
+            var rangeContainerElement = getTextRangeContainerElement(this.textRange);
+
+            if (textRangeIsCollapsed(this.textRange)) {
+                end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, true);
+            } else {
+
+                start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
+                end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false);
+            }
+
+            this.setStart(start.node, start.offset);
+            this.setEnd(end.node, end.offset);
+        };
+
+        DomRange.copyComparisonConstants(WrappedRange);
+
+        // Add WrappedRange as the Range property of the global object to allow expression like Range.END_TO_END to work
+        var globalObj = (function() { return this; })();
+        if (typeof globalObj.Range == "undefined") {
+            globalObj.Range = WrappedRange;
+        }
+
+        api.createNativeRange = function(doc) {
+            doc = doc || document;
+            return doc.body.createTextRange();
+        };
+    }
+
+    if (api.features.implementsTextRange) {
+        WrappedRange.rangeToTextRange = function(range) {
+            if (range.collapsed) {
+                var tr = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+
+
+
+                return tr;
+
+                //return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+            } else {
+                var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
+                var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
+                var textRange = dom.getDocument(range.startContainer).body.createTextRange();
+                textRange.setEndPoint("StartToStart", startRange);
+                textRange.setEndPoint("EndToEnd", endRange);
+                return textRange;
+            }
+        };
+    }
+
+    WrappedRange.prototype.getName = function() {
+        return "WrappedRange";
+    };
+
+    api.WrappedRange = WrappedRange;
+
+    api.createRange = function(doc) {
+        doc = doc || document;
+        return new WrappedRange(api.createNativeRange(doc));
+    };
+
+    api.createRangyRange = function(doc) {
+        doc = doc || document;
+        return new DomRange(doc);
+    };
+
+    api.createIframeRange = function(iframeEl) {
+        return api.createRange(dom.getIframeDocument(iframeEl));
+    };
+
+    api.createIframeRangyRange = function(iframeEl) {
+        return api.createRangyRange(dom.getIframeDocument(iframeEl));
+    };
+
+    api.addCreateMissingNativeApiListener(function(win) {
+        var doc = win.document;
+        if (typeof doc.createRange == "undefined") {
+            doc.createRange = function() {
+                return api.createRange(this);
+            };
+        }
+        doc = win = null;
+    });
+});rangy.createModule("WrappedSelection", function(api, module) {
+    // This will create a selection object wrapper that follows the Selection object found in the WHATWG draft DOM Range
+    // spec (http://html5.org/specs/dom-range.html)
+
+    api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
+
+    api.config.checkSelectionRanges = true;
+
+    var BOOLEAN = "boolean",
+        windowPropertyName = "_rangySelection",
+        dom = api.dom,
+        util = api.util,
+        DomRange = api.DomRange,
+        WrappedRange = api.WrappedRange,
+        DOMException = api.DOMException,
+        DomPosition = dom.DomPosition,
+        getSelection,
+        selectionIsCollapsed,
+        CONTROL = "Control";
+
+
+
+    function getWinSelection(winParam) {
+        return (winParam || window).getSelection();
+    }
+
+    function getDocSelection(winParam) {
+        return (winParam || window).document.selection;
+    }
+
+    // Test for the Range/TextRange and Selection features required
+    // Test for ability to retrieve selection
+    var implementsWinGetSelection = api.util.isHostMethod(window, "getSelection"),
+        implementsDocSelection = api.util.isHostObject(document, "selection");
+
+    var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
+
+    if (useDocumentSelection) {
+        getSelection = getDocSelection;
+        api.isSelectionValid = function(winParam) {
+            var doc = (winParam || window).document, nativeSel = doc.selection;
+
+            // Check whether the selection TextRange is actually contained within the correct document
+            return (nativeSel.type != "None" || dom.getDocument(nativeSel.createRange().parentElement()) == doc);
+        };
+    } else if (implementsWinGetSelection) {
+        getSelection = getWinSelection;
+        api.isSelectionValid = function() {
+            return true;
+        };
+    } else {
+        module.fail("Neither document.selection or window.getSelection() detected.");
+    }
+
+    api.getNativeSelection = getSelection;
+
+    var testSelection = getSelection();
+    var testRange = api.createNativeRange(document);
+    var body = dom.getBody(document);
+
+    // Obtaining a range from a selection
+    var selectionHasAnchorAndFocus = util.areHostObjects(testSelection, ["anchorNode", "focusNode"] &&
+                                     util.areHostProperties(testSelection, ["anchorOffset", "focusOffset"]));
+    api.features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
+
+    // Test for existence of native selection extend() method
+    var selectionHasExtend = util.isHostMethod(testSelection, "extend");
+    api.features.selectionHasExtend = selectionHasExtend;
+
+    // Test if rangeCount exists
+    var selectionHasRangeCount = (typeof testSelection.rangeCount == "number");
+    api.features.selectionHasRangeCount = selectionHasRangeCount;
+
+    var selectionSupportsMultipleRanges = false;
+    var collapsedNonEditableSelectionsSupported = true;
+
+    if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
+            typeof testSelection.rangeCount == "number" && api.features.implementsDomRange) {
+
+        (function() {
+            var iframe = document.createElement("iframe");
+            iframe.frameBorder = 0;
+            iframe.style.position = "absolute";
+            iframe.style.left = "-10000px";
+            body.appendChild(iframe);
+
+            var iframeDoc = dom.getIframeDocument(iframe);
+            iframeDoc.open();
+            iframeDoc.write("12");
+            iframeDoc.close();
+
+            var sel = dom.getIframeWindow(iframe).getSelection();
+            var docEl = iframeDoc.documentElement;
+            var iframeBody = docEl.lastChild, textNode = iframeBody.firstChild;
+
+            // Test whether the native selection will allow a collapsed selection within a non-editable element
+            var r1 = iframeDoc.createRange();
+            r1.setStart(textNode, 1);
+            r1.collapse(true);
+            sel.addRange(r1);
+            collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
+            sel.removeAllRanges();
+
+            // Test whether the native selection is capable of supporting multiple ranges
+            var r2 = r1.cloneRange();
+            r1.setStart(textNode, 0);
+            r2.setEnd(textNode, 2);
+            sel.addRange(r1);
+            sel.addRange(r2);
+
+            selectionSupportsMultipleRanges = (sel.rangeCount == 2);
+
+            // Clean up
+            r1.detach();
+            r2.detach();
+
+            body.removeChild(iframe);
+        })();
+    }
+
+    api.features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
+    api.features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
+
+    // ControlRanges
+    var implementsControlRange = false, testControlRange;
+
+    if (body && util.isHostMethod(body, "createControlRange")) {
+        testControlRange = body.createControlRange();
+        if (util.areHostProperties(testControlRange, ["item", "add"])) {
+            implementsControlRange = true;
+        }
+    }
+    api.features.implementsControlRange = implementsControlRange;
+
+    // Selection collapsedness
+    if (selectionHasAnchorAndFocus) {
+        selectionIsCollapsed = function(sel) {
+            return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
+        };
+    } else {
+        selectionIsCollapsed = function(sel) {
+            return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
+        };
+    }
+
+    function updateAnchorAndFocusFromRange(sel, range, backwards) {
+        var anchorPrefix = backwards ? "end" : "start", focusPrefix = backwards ? "start" : "end";
+        sel.anchorNode = range[anchorPrefix + "Container"];
+        sel.anchorOffset = range[anchorPrefix + "Offset"];
+        sel.focusNode = range[focusPrefix + "Container"];
+        sel.focusOffset = range[focusPrefix + "Offset"];
+    }
+
+    function updateAnchorAndFocusFromNativeSelection(sel) {
+        var nativeSel = sel.nativeSelection;
+        sel.anchorNode = nativeSel.anchorNode;
+        sel.anchorOffset = nativeSel.anchorOffset;
+        sel.focusNode = nativeSel.focusNode;
+        sel.focusOffset = nativeSel.focusOffset;
+    }
+
+    function updateEmptySelection(sel) {
+        sel.anchorNode = sel.focusNode = null;
+        sel.anchorOffset = sel.focusOffset = 0;
+        sel.rangeCount = 0;
+        sel.isCollapsed = true;
+        sel._ranges.length = 0;
+    }
+
+    function getNativeRange(range) {
+        var nativeRange;
+        if (range instanceof DomRange) {
+            nativeRange = range._selectionNativeRange;
+            if (!nativeRange) {
+                nativeRange = api.createNativeRange(dom.getDocument(range.startContainer));
+                nativeRange.setEnd(range.endContainer, range.endOffset);
+                nativeRange.setStart(range.startContainer, range.startOffset);
+                range._selectionNativeRange = nativeRange;
+                range.attachListener("detach", function() {
+
+                    this._selectionNativeRange = null;
+                });
+            }
+        } else if (range instanceof WrappedRange) {
+            nativeRange = range.nativeRange;
+        } else if (api.features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
+            nativeRange = range;
+        }
+        return nativeRange;
+    }
+
+    function rangeContainsSingleElement(rangeNodes) {
+        if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
+            return false;
+        }
+        for (var i = 1, len = rangeNodes.length; i < len; ++i) {
+            if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    function getSingleElementFromRange(range) {
+        var nodes = range.getNodes();
+        if (!rangeContainsSingleElement(nodes)) {
+            throw new Error("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
+        }
+        return nodes[0];
+    }
+
+    function isTextRange(range) {
+        return !!range && typeof range.text != "undefined";
+    }
+
+    function updateFromTextRange(sel, range) {
+        // Create a Range from the selected TextRange
+        var wrappedRange = new WrappedRange(range);
+        sel._ranges = [wrappedRange];
+
+        updateAnchorAndFocusFromRange(sel, wrappedRange, false);
+        sel.rangeCount = 1;
+        sel.isCollapsed = wrappedRange.collapsed;
+    }
+
+    function updateControlSelection(sel) {
+        // Update the wrapped selection based on what's now in the native selection
+        sel._ranges.length = 0;
+        if (sel.docSelection.type == "None") {
+            updateEmptySelection(sel);
+        } else {
+            var controlRange = sel.docSelection.createRange();
+            if (isTextRange(controlRange)) {
+                // This case (where the selection type is "Control" and calling createRange() on the selection returns
+                // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
+                // ControlRange have been removed from the ControlRange and removed from the document.
+                updateFromTextRange(sel, controlRange);
+            } else {
+                sel.rangeCount = controlRange.length;
+                var range, doc = dom.getDocument(controlRange.item(0));
+                for (var i = 0; i < sel.rangeCount; ++i) {
+                    range = api.createRange(doc);
+                    range.selectNode(controlRange.item(i));
+                    sel._ranges.push(range);
+                }
+                sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
+                updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
+            }
+        }
+    }
+
+    function addRangeToControlSelection(sel, range) {
+        var controlRange = sel.docSelection.createRange();
+        var rangeElement = getSingleElementFromRange(range);
+
+        // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
+        // contained by the supplied range
+        var doc = dom.getDocument(controlRange.item(0));
+        var newControlRange = dom.getBody(doc).createControlRange();
+        for (var i = 0, len = controlRange.length; i < len; ++i) {
+            newControlRange.add(controlRange.item(i));
+        }
+        try {
+            newControlRange.add(rangeElement);
+        } catch (ex) {
+            throw new Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
+        }
+        newControlRange.select();
+
+        // Update the wrapped selection based on what's now in the native selection
+        updateControlSelection(sel);
+    }
+
+    var getSelectionRangeAt;
+
+    if (util.isHostMethod(testSelection,  "getRangeAt")) {
+        getSelectionRangeAt = function(sel, index) {
+            try {
+                return sel.getRangeAt(index);
+            } catch(ex) {
+                return null;
+            }
+        };
+    } else if (selectionHasAnchorAndFocus) {
+        getSelectionRangeAt = function(sel) {
+            var doc = dom.getDocument(sel.anchorNode);
+            var range = api.createRange(doc);
+            range.setStart(sel.anchorNode, sel.anchorOffset);
+            range.setEnd(sel.focusNode, sel.focusOffset);
+
+            // Handle the case when the selection was selected backwards (from the end to the start in the
+            // document)
+            if (range.collapsed !== this.isCollapsed) {
+                range.setStart(sel.focusNode, sel.focusOffset);
+                range.setEnd(sel.anchorNode, sel.anchorOffset);
+            }
+
+            return range;
+        };
+    }
+
+    /**
+     * @constructor
+     */
+    function WrappedSelection(selection, docSelection, win) {
+        this.nativeSelection = selection;
+        this.docSelection = docSelection;
+        this._ranges = [];
+        this.win = win;
+        this.refresh();
+    }
+
+    api.getSelection = function(win) {
+        win = win || window;
+        var sel = win[windowPropertyName];
+        var nativeSel = getSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
+        if (sel) {
+            sel.nativeSelection = nativeSel;
+            sel.docSelection = docSel;
+            sel.refresh(win);
+        } else {
+            sel = new WrappedSelection(nativeSel, docSel, win);
+            win[windowPropertyName] = sel;
+        }
+        return sel;
+    };
+
+    api.getIframeSelection = function(iframeEl) {
+        return api.getSelection(dom.getIframeWindow(iframeEl));
+    };
+
+    var selProto = WrappedSelection.prototype;
+
+    function createControlSelection(sel, ranges) {
+        // Ensure that the selection becomes of type "Control"
+        var doc = dom.getDocument(ranges[0].startContainer);
+        var controlRange = dom.getBody(doc).createControlRange();
+        for (var i = 0, el; i < rangeCount; ++i) {
+            el = getSingleElementFromRange(ranges[i]);
+            try {
+                controlRange.add(el);
+            } catch (ex) {
+                throw new Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)");
+            }
+        }
+        controlRange.select();
+
+        // Update the wrapped selection based on what's now in the native selection
+        updateControlSelection(sel);
+    }
+
+    // Selecting a range
+    if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
+        selProto.removeAllRanges = function() {
+            this.nativeSelection.removeAllRanges();
+            updateEmptySelection(this);
+        };
+
+        var addRangeBackwards = function(sel, range) {
+            var doc = DomRange.getRangeDocument(range);
+            var endRange = api.createRange(doc);
+            endRange.collapseToPoint(range.endContainer, range.endOffset);
+            sel.nativeSelection.addRange(getNativeRange(endRange));
+            sel.nativeSelection.extend(range.startContainer, range.startOffset);
+            sel.refresh();
+        };
+
+        if (selectionHasRangeCount) {
+            selProto.addRange = function(range, backwards) {
+                if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+                    addRangeToControlSelection(this, range);
+                } else {
+                    if (backwards && selectionHasExtend) {
+                        addRangeBackwards(this, range);
+                    } else {
+                        var previousRangeCount;
+                        if (selectionSupportsMultipleRanges) {
+                            previousRangeCount = this.rangeCount;
+                        } else {
+                            this.removeAllRanges();
+                            previousRangeCount = 0;
+                        }
+                        this.nativeSelection.addRange(getNativeRange(range));
+
+                        // Check whether adding the range was successful
+                        this.rangeCount = this.nativeSelection.rangeCount;
+
+                        if (this.rangeCount == previousRangeCount + 1) {
+                            // The range was added successfully
+
+                            // Check whether the range that we added to the selection is reflected in the last range extracted from
+                            // the selection
+                            if (api.config.checkSelectionRanges) {
+                                var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
+                                if (nativeRange && !DomRange.rangesEqual(nativeRange, range)) {
+                                    // Happens in WebKit with, for example, a selection placed at the start of a text node
+                                    range = new WrappedRange(nativeRange);
+                                }
+                            }
+                            this._ranges[this.rangeCount - 1] = range;
+                            updateAnchorAndFocusFromRange(this, range, selectionIsBackwards(this.nativeSelection));
+                            this.isCollapsed = selectionIsCollapsed(this);
+                        } else {
+                            // The range was not added successfully. The simplest thing is to refresh
+                            this.refresh();
+                        }
+                    }
+                }
+            };
+        } else {
+            selProto.addRange = function(range, backwards) {
+                if (backwards && selectionHasExtend) {
+                    addRangeBackwards(this, range);
+                } else {
+                    this.nativeSelection.addRange(getNativeRange(range));
+                    this.refresh();
+                }
+            };
+        }
+
+        selProto.setRanges = function(ranges) {
+            if (implementsControlRange && ranges.length > 1) {
+                createControlSelection(this, ranges);
+            } else {
+                this.removeAllRanges();
+                for (var i = 0, len = ranges.length; i < len; ++i) {
+                    this.addRange(ranges[i]);
+                }
+            }
+        };
+    } else if (util.isHostMethod(testSelection, "empty") && util.isHostMethod(testRange, "select") &&
+               implementsControlRange && useDocumentSelection) {
+
+        selProto.removeAllRanges = function() {
+            // Added try/catch as fix for issue #21
+            try {
+                this.docSelection.empty();
+
+                // Check for empty() not working (issue #24)
+                if (this.docSelection.type != "None") {
+                    // Work around failure to empty a control selection by instead selecting a TextRange and then
+                    // calling empty()
+                    var doc;
+                    if (this.anchorNode) {
+                        doc = dom.getDocument(this.anchorNode);
+                    } else if (this.docSelection.type == CONTROL) {
+                        var controlRange = this.docSelection.createRange();
+                        if (controlRange.length) {
+                            doc = dom.getDocument(controlRange.item(0)).body.createTextRange();
+                        }
+                    }
+                    if (doc) {
+                        var textRange = doc.body.createTextRange();
+                        textRange.select();
+                        this.docSelection.empty();
+                    }
+                }
+            } catch(ex) {}
+            updateEmptySelection(this);
+        };
+
+        selProto.addRange = function(range) {
+            if (this.docSelection.type == CONTROL) {
+                addRangeToControlSelection(this, range);
+            } else {
+                WrappedRange.rangeToTextRange(range).select();
+                this._ranges[0] = range;
+                this.rangeCount = 1;
+                this.isCollapsed = this._ranges[0].collapsed;
+                updateAnchorAndFocusFromRange(this, range, false);
+            }
+        };
+
+        selProto.setRanges = function(ranges) {
+            this.removeAllRanges();
+            var rangeCount = ranges.length;
+            if (rangeCount > 1) {
+                createControlSelection(this, ranges);
+            } else if (rangeCount) {
+                this.addRange(ranges[0]);
+            }
+        };
+    } else {
+        module.fail("No means of selecting a Range or TextRange was found");
+        return false;
+    }
+
+    selProto.getRangeAt = function(index) {
+        if (index < 0 || index >= this.rangeCount) {
+            throw new DOMException("INDEX_SIZE_ERR");
+        } else {
+            return this._ranges[index];
+        }
+    };
+
+    var refreshSelection;
+
+    if (useDocumentSelection) {
+        refreshSelection = function(sel) {
+            var range;
+            if (api.isSelectionValid(sel.win)) {
+                range = sel.docSelection.createRange();
+            } else {
+                range = dom.getBody(sel.win.document).createTextRange();
+                range.collapse(true);
+            }
+
+
+            if (sel.docSelection.type == CONTROL) {
+                updateControlSelection(sel);
+            } else if (isTextRange(range)) {
+                updateFromTextRange(sel, range);
+            } else {
+                updateEmptySelection(sel);
+            }
+        };
+    } else if (util.isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == "number") {
+        refreshSelection = function(sel) {
+            if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
+                updateControlSelection(sel);
+            } else {
+                sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
+                if (sel.rangeCount) {
+                    for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                        sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
+                    }
+                    updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackwards(sel.nativeSelection));
+                    sel.isCollapsed = selectionIsCollapsed(sel);
+                } else {
+                    updateEmptySelection(sel);
+                }
+            }
+        };
+    } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && api.features.implementsDomRange) {
+        refreshSelection = function(sel) {
+            var range, nativeSel = sel.nativeSelection;
+            if (nativeSel.anchorNode) {
+                range = getSelectionRangeAt(nativeSel, 0);
+                sel._ranges = [range];
+                sel.rangeCount = 1;
+                updateAnchorAndFocusFromNativeSelection(sel);
+                sel.isCollapsed = selectionIsCollapsed(sel);
+            } else {
+                updateEmptySelection(sel);
+            }
+        };
+    } else {
+        module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
+        return false;
+    }
+
+    selProto.refresh = function(checkForChanges) {
+        var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
+        refreshSelection(this);
+        if (checkForChanges) {
+            var i = oldRanges.length;
+            if (i != this._ranges.length) {
+                return false;
+            }
+            while (i--) {
+                if (!DomRange.rangesEqual(oldRanges[i], this._ranges[i])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    };
+
+    // Removal of a single range
+    var removeRangeManually = function(sel, range) {
+        var ranges = sel.getAllRanges(), removed = false;
+        sel.removeAllRanges();
+        for (var i = 0, len = ranges.length; i < len; ++i) {
+            if (removed || range !== ranges[i]) {
+                sel.addRange(ranges[i]);
+            } else {
+                // According to the draft WHATWG Range spec, the same range may be added to the selection multiple
+                // times. removeRange should only remove the first instance, so the following ensures only the first
+                // instance is removed
+                removed = true;
+            }
+        }
+        if (!sel.rangeCount) {
+            updateEmptySelection(sel);
+        }
+    };
+
+    if (implementsControlRange) {
+        selProto.removeRange = function(range) {
+            if (this.docSelection.type == CONTROL) {
+                var controlRange = this.docSelection.createRange();
+                var rangeElement = getSingleElementFromRange(range);
+
+                // Create a new ControlRange containing all the elements in the selected ControlRange minus the
+                // element contained by the supplied range
+                var doc = dom.getDocument(controlRange.item(0));
+                var newControlRange = dom.getBody(doc).createControlRange();
+                var el, removed = false;
+                for (var i = 0, len = controlRange.length; i < len; ++i) {
+                    el = controlRange.item(i);
+                    if (el !== rangeElement || removed) {
+                        newControlRange.add(controlRange.item(i));
+                    } else {
+                        removed = true;
+                    }
+                }
+                newControlRange.select();
+
+                // Update the wrapped selection based on what's now in the native selection
+                updateControlSelection(this);
+            } else {
+                removeRangeManually(this, range);
+            }
+        };
+    } else {
+        selProto.removeRange = function(range) {
+            removeRangeManually(this, range);
+        };
+    }
+
+    // Detecting if a selection is backwards
+    var selectionIsBackwards;
+    if (!useDocumentSelection && selectionHasAnchorAndFocus && api.features.implementsDomRange) {
+        selectionIsBackwards = function(sel) {
+            var backwards = false;
+            if (sel.anchorNode) {
+                backwards = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
+            }
+            return backwards;
+        };
+
+        selProto.isBackwards = function() {
+            return selectionIsBackwards(this);
+        };
+    } else {
+        selectionIsBackwards = selProto.isBackwards = function() {
+            return false;
+        };
+    }
+
+    // Selection text
+    // This is conformant to the new WHATWG DOM Range draft spec but differs from WebKit and Mozilla's implementation
+    selProto.toString = function() {
+
+        var rangeTexts = [];
+        for (var i = 0, len = this.rangeCount; i < len; ++i) {
+            rangeTexts[i] = "" + this._ranges[i];
+        }
+        return rangeTexts.join("");
+    };
+
+    function assertNodeInSameDocument(sel, node) {
+        if (sel.anchorNode && (dom.getDocument(sel.anchorNode) !== dom.getDocument(node))) {
+            throw new DOMException("WRONG_DOCUMENT_ERR");
+        }
+    }
+
+    // No current browsers conform fully to the HTML 5 draft spec for this method, so Rangy's own method is always used
+    selProto.collapse = function(node, offset) {
+        assertNodeInSameDocument(this, node);
+        var range = api.createRange(dom.getDocument(node));
+        range.collapseToPoint(node, offset);
+        this.removeAllRanges();
+        this.addRange(range);
+        this.isCollapsed = true;
+    };
+
+    selProto.collapseToStart = function() {
+        if (this.rangeCount) {
+            var range = this._ranges[0];
+            this.collapse(range.startContainer, range.startOffset);
+        } else {
+            throw new DOMException("INVALID_STATE_ERR");
+        }
+    };
+
+    selProto.collapseToEnd = function() {
+        if (this.rangeCount) {
+            var range = this._ranges[this.rangeCount - 1];
+            this.collapse(range.endContainer, range.endOffset);
+        } else {
+            throw new DOMException("INVALID_STATE_ERR");
+        }
+    };
+
+    // The HTML 5 spec is very specific on how selectAllChildren should be implemented so the native implementation is
+    // never used by Rangy.
+    selProto.selectAllChildren = function(node) {
+        assertNodeInSameDocument(this, node);
+        var range = api.createRange(dom.getDocument(node));
+        range.selectNodeContents(node);
+        this.removeAllRanges();
+        this.addRange(range);
+    };
+
+    selProto.deleteFromDocument = function() {
+        // Sepcial behaviour required for Control selections
+        if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
+            var controlRange = this.docSelection.createRange();
+            var element;
+            while (controlRange.length) {
+                element = controlRange.item(0);
+                controlRange.remove(element);
+                element.parentNode.removeChild(element);
+            }
+            this.refresh();
+        } else if (this.rangeCount) {
+            var ranges = this.getAllRanges();
+            this.removeAllRanges();
+            for (var i = 0, len = ranges.length; i < len; ++i) {
+                ranges[i].deleteContents();
+            }
+            // The HTML5 spec says nothing about what the selection should contain after calling deleteContents on each
+            // range. Firefox moves the selection to where the final selected range was, so we emulate that
+            this.addRange(ranges[len - 1]);
+        }
+    };
+
+    // The following are non-standard extensions
+    selProto.getAllRanges = function() {
+        return this._ranges.slice(0);
+    };
+
+    selProto.setSingleRange = function(range) {
+        this.setRanges( [range] );
+    };
+
+    selProto.containsNode = function(node, allowPartial) {
+        for (var i = 0, len = this._ranges.length; i < len; ++i) {
+            if (this._ranges[i].containsNode(node, allowPartial)) {
+                return true;
+            }
+        }
+        return false;
+    };
+
+    selProto.toHtml = function() {
+        var html = "";
+        if (this.rangeCount) {
+            var container = DomRange.getRangeDocument(this._ranges[0]).createElement("div");
+            for (var i = 0, len = this._ranges.length; i < len; ++i) {
+                container.appendChild(this._ranges[i].cloneContents());
+            }
+            html = container.innerHTML;
+        }
+        return html;
+    };
+
+    function inspect(sel) {
+        var rangeInspects = [];
+        var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
+        var focus = new DomPosition(sel.focusNode, sel.focusOffset);
+        var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
+
+        if (typeof sel.rangeCount != "undefined") {
+            for (var i = 0, len = sel.rangeCount; i < len; ++i) {
+                rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
+            }
+        }
+        return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
+                ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
+
+    }
+
+    selProto.getName = function() {
+        return "WrappedSelection";
+    };
+
+    selProto.inspect = function() {
+        return inspect(this);
+    };
+
+    selProto.detach = function() {
+        this.win[windowPropertyName] = null;
+        this.win = this.anchorNode = this.focusNode = null;
+    };
+
+    WrappedSelection.inspect = inspect;
+
+    api.Selection = WrappedSelection;
+
+    api.selectionPrototype = selProto;
+
+    api.addCreateMissingNativeApiListener(function(win) {
+        if (typeof win.getSelection == "undefined") {
+            win.getSelection = function() {
+                return api.getSelection(this);
+            };
+        }
+        win = null;
+    });
+});
+
+/**
+ * @license Selection save and restore module for Rangy.
+ * Saves and restores user selections using marker invisible elements in the DOM.
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * http://code.google.com/p/rangy/
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2012, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.2.3
+ * Build date: 26 February 2012
+ */
+rangy.createModule("SaveRestore", function(api, module) {
+    api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
+
+    var dom = api.dom;
+
+    var markerTextChar = "\ufeff";
+
+    function gEBI(id, doc) {
+        return (doc || document).getElementById(id);
+    }
+
+    function insertRangeBoundaryMarker(range, atStart) {
+        var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
+        var markerEl;
+        var doc = dom.getDocument(range.startContainer);
+
+        // Clone the Range and collapse to the appropriate boundary point
+        var boundaryRange = range.cloneRange();
+        boundaryRange.collapse(atStart);
+
+        // Create the marker element containing a single invisible character using DOM methods and insert it
+        markerEl = doc.createElement("span");
+        markerEl.id = markerId;
+        markerEl.style.lineHeight = "0";
+        markerEl.style.display = "none";
+        markerEl.className = "rangySelectionBoundary";
+        markerEl.appendChild(doc.createTextNode(markerTextChar));
+
+        boundaryRange.insertNode(markerEl);
+        boundaryRange.detach();
+        return markerEl;
+    }
+
+    function setRangeBoundary(doc, range, markerId, atStart) {
+        var markerEl = gEBI(markerId, doc);
+        if (markerEl) {
+            range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
+            markerEl.parentNode.removeChild(markerEl);
+        } else {
+            module.warn("Marker element has been removed. Cannot restore selection.");
+        }
+    }
+
+    function compareRanges(r1, r2) {
+        return r2.compareBoundaryPoints(r1.START_TO_START, r1);
+    }
+
+    function saveSelection(win) {
+        win = win || window;
+        var doc = win.document;
+        if (!api.isSelectionValid(win)) {
+            module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
+            return;
+        }
+        var sel = api.getSelection(win);
+        var ranges = sel.getAllRanges();
+        var rangeInfos = [], startEl, endEl, range;
+
+        // Order the ranges by position within the DOM, latest first
+        ranges.sort(compareRanges);
+
+        for (var i = 0, len = ranges.length; i < len; ++i) {
+            range = ranges[i];
+            if (range.collapsed) {
+                endEl = insertRangeBoundaryMarker(range, false);
+                rangeInfos.push({
+                    markerId: endEl.id,
+                    collapsed: true
+                });
+            } else {
+                endEl = insertRangeBoundaryMarker(range, false);
+                startEl = insertRangeBoundaryMarker(range, true);
+
+                rangeInfos[i] = {
+                    startMarkerId: startEl.id,
+                    endMarkerId: endEl.id,
+                    collapsed: false,
+                    backwards: ranges.length == 1 && sel.isBackwards()
+                };
+            }
+        }
+
+        // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
+        // between its markers
+        for (i = len - 1; i >= 0; --i) {
+            range = ranges[i];
+            if (range.collapsed) {
+                range.collapseBefore(gEBI(rangeInfos[i].markerId, doc));
+            } else {
+                range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
+                range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
+            }
+        }
+
+        // Ensure current selection is unaffected
+        sel.setRanges(ranges);
+        return {
+            win: win,
+            doc: doc,
+            rangeInfos: rangeInfos,
+            restored: false
+        };
+    }
+
+    function restoreSelection(savedSelection, preserveDirection) {
+        if (!savedSelection.restored) {
+            var rangeInfos = savedSelection.rangeInfos;
+            var sel = api.getSelection(savedSelection.win);
+            var ranges = [];
+
+            // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
+            // normalization affecting previously restored ranges.
+            for (var len = rangeInfos.length, i = len - 1, rangeInfo, range; i >= 0; --i) {
+                rangeInfo = rangeInfos[i];
+                range = api.createRange(savedSelection.doc);
+                if (rangeInfo.collapsed) {
+                    var markerEl = gEBI(rangeInfo.markerId, savedSelection.doc);
+                    if (markerEl) {
+                        markerEl.style.display = "inline";
+                        var previousNode = markerEl.previousSibling;
+
+                        // Workaround for issue 17
+                        if (previousNode && previousNode.nodeType == 3) {
+                            markerEl.parentNode.removeChild(markerEl);
+                            range.collapseToPoint(previousNode, previousNode.length);
+                        } else {
+                            range.collapseBefore(markerEl);
+                            markerEl.parentNode.removeChild(markerEl);
+                        }
+                    } else {
+                        module.warn("Marker element has been removed. Cannot restore selection.");
+                    }
+                } else {
+                    setRangeBoundary(savedSelection.doc, range, rangeInfo.startMarkerId, true);
+                    setRangeBoundary(savedSelection.doc, range, rangeInfo.endMarkerId, false);
+                }
+
+                // Normalizing range boundaries is only viable if the selection contains only one range. For example,
+                // if the selection contained two ranges that were both contained within the same single text node,
+                // both would alter the same text node when restoring and break the other range.
+                if (len == 1) {
+                    range.normalizeBoundaries();
+                }
+                ranges[i] = range;
+            }
+            if (len == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backwards) {
+                sel.removeAllRanges();
+                sel.addRange(ranges[0], true);
+            } else {
+                sel.setRanges(ranges);
+            }
+
+            savedSelection.restored = true;
+        }
+    }
+
+    function removeMarkerElement(doc, markerId) {
+        var markerEl = gEBI(markerId, doc);
+        if (markerEl) {
+            markerEl.parentNode.removeChild(markerEl);
+        }
+    }
+
+    function removeMarkers(savedSelection) {
+        var rangeInfos = savedSelection.rangeInfos;
+        for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
+            rangeInfo = rangeInfos[i];
+            if (rangeInfo.collapsed) {
+                removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
+            } else {
+                removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
+                removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
+            }
+        }
+    }
+
+    api.saveSelection = saveSelection;
+    api.restoreSelection = restoreSelection;
+    api.removeMarkerElement = removeMarkerElement;
+    api.removeMarkers = removeMarkers;
+});
+
+/*!
+  * Bowser - a browser detector
+  * https://github.com/ded/bowser
+  * MIT License | (c) Dustin Diaz 2011
+  */
+!function (name, definition) {
+  if (typeof define == 'function') define(definition)
+  else if (typeof module != 'undefined' && module.exports) module.exports['browser'] = definition()
+  else this[name] = definition()
+}('bowser', function () {
+  /**
+    * navigator.userAgent =>
+    * Chrome:  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.57 Safari/534.24"
+    * Opera:   "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.7; U; en) Presto/2.7.62 Version/11.01"
+    * Safari:  "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-us) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1"
+    * IE:      "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C)"
+    * Firefox: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0) Gecko/20100101 Firefox/4.0"
+    * iPhone:  "Mozilla/5.0 (iPhone Simulator; U; CPU iPhone OS 4_3_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 Safari/6533.18.5"
+    * iPad:    "Mozilla/5.0 (iPad; U; CPU OS 4_3_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8H7 Safari/6533.18.5",
+    * Android: "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; T-Mobile G2 Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"
+    * Touchpad: "Mozilla/5.0 (hp-tabled;Linux;hpwOS/3.0.5; U; en-US)) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/234.83 Safari/534.6 TouchPad/1.0"
+    * PhantomJS: "Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.5.0 Safari/534.34"
+    */
+
+  var ua = navigator.userAgent
+    , t = true
+    , ie = /msie/i.test(ua)
+    , chrome = /chrome/i.test(ua)
+    , phantom = /phantom/i.test(ua)
+    , safari = /safari/i.test(ua) && !chrome && !phantom
+    , iphone = /iphone/i.test(ua)
+    , ipad = /ipad/i.test(ua)
+    , touchpad = /touchpad/i.test(ua)
+    , android = /android/i.test(ua)
+    , opera = /opera/i.test(ua)
+    , firefox = /firefox/i.test(ua)
+    , gecko = /gecko\//i.test(ua)
+    , seamonkey = /seamonkey\//i.test(ua)
+    , webkitVersion = /version\/(\d+(\.\d+)?)/i
+    , o
+
+  function detect() {
+
+    if (ie) return {
+        msie: t
+      , version: ua.match(/msie (\d+(\.\d+)?);/i)[1]
+    }
+    if (chrome) return {
+        webkit: t
+      , chrome: t
+      , version: ua.match(/chrome\/(\d+(\.\d+)?)/i)[1]
+    }
+    if (phantom) return {
+        webkit: t
+      , phantom: t
+      , version: ua.match(/phantomjs\/(\d+(\.\d+)+)/i)[1]
+    }
+    if (touchpad) return {
+        webkit: t
+      , touchpad: t
+      , version : ua.match(/touchpad\/(\d+(\.\d+)?)/i)[1]
+    }
+    if (iphone || ipad) {
+      o = {
+          webkit: t
+        , mobile: t
+        , ios: t
+        , iphone: iphone
+        , ipad: ipad
+      }
+      // WTF: version is not part of user agent in web apps
+      if (webkitVersion.test(ua)) {
+        o.version = ua.match(webkitVersion)[1]
+      }
+      return o
+    }
+    if (android) return {
+        webkit: t
+      , android: t
+      , mobile: t
+      , version: ua.match(webkitVersion)[1]
+    }
+    if (safari) return {
+        webkit: t
+      , safari: t
+      , version: ua.match(webkitVersion)[1]
+    }
+    if (opera) return {
+        opera: t
+      , version: ua.match(webkitVersion)[1]
+    }
+    if (gecko) {
+      o = {
+          gecko: t
+        , mozilla: t
+        , version: ua.match(/firefox\/(\d+(\.\d+)?)/i)[1]
+      }
+      if (firefox) o.firefox = t
+      return o
+    }
+    if (seamonkey) return {
+        seamonkey: t
+      , version: ua.match(/seamonkey\/(\d+(\.\d+)?)/i)[1]
+    }
+  }
+
+  var bowser = detect()
+
+  // Graded Browser Support
+  // http://developer.yahoo.com/yui/articles/gbs
+  if ((bowser.msie && bowser.version >= 7) ||
+      (bowser.chrome && bowser.version >= 10) ||
+      (bowser.firefox && bowser.version >= 4.0) ||
+      (bowser.safari && bowser.version >= 5) ||
+      (bowser.opera && bowser.version >= 10.0)) {
+    bowser.a = t;
+  }
+
+  else if ((bowser.msie && bowser.version < 7) ||
+      (bowser.chrome && bowser.version < 10) ||
+      (bowser.firefox && bowser.version < 4.0) ||
+      (bowser.safari && bowser.version < 5) ||
+      (bowser.opera && bowser.version < 10.0)) {
+    bowser.c = t
+  } else bowser.x = t
+
+  return bowser
+})
+;(function(window, document, jQuery, undefined) {
+  'use strict';
+
+  var Editable = {};
+var $ = (function() {
+  return jQuery || function() {
+    throw new Error('jQuery-like library not yet implemented');
+  };
+})();
+
+var log, error;
+
+// Allows for safe console logging
+// If the last param is the string "trace" console.trace will be called
+// configuration: disable with config.log = false
+log = function() {
+  if (config.log === false) { return; }
+
+  var args, _ref;
+  args = Array.prototype.slice.call(arguments);
+  if (args.length) {
+    if (args[args.length - 1] === "trace") {
+      args.pop();
+      if ((_ref = window.console) ? _ref.trace : void 0) {
+        console.trace();
+      }
+    }
+  }
+
+  if (args.length === 1) {
+    args = args[0];
+  }
+
+  if (window.console) {
+    return console.log(args);
+  }
+};
+
+// Allows for safe error logging
+// Falls back to console.log if console.error is not available
+error = function() {
+  if (config.logErrors === false) { return; }
+
+  var args;
+  args = Array.prototype.slice.call(arguments);
+  if (args.length === 1) {
+    args = args[0];
+  }
+
+  if (window.console && typeof window.console.error === "function") {
+    return console.error(args);
+  } else if (window.console) {
+    return console.log(args);
+  }
+};
+
+var string = (function() {
+  return {
+    trimRight: function(text) {
+      return text.replace(/\s+$/, '');
+    },
+
+    trimLeft: function(text) {
+      return text.replace(/^\s+/, '');
+    },
+
+    trim: function(text) {
+      return text.replace(/^\s+|\s+$/g, '');
+    },
+
+    isString: function(obj) {
+      return toString.call(obj) === '[object String]';
+    },
+
+    /**
+     * Turn any string into a regular expression.
+     * This can be used to search or replace a string conveniently.
+     */
+    regexp: function(str, flags) {
+      if (!flags) flags = 'g';
+      var escapedStr = str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+      return new RegExp(escapedStr, flags);
+    }
+  };
+})();
+
+/**
+ * The Core module provides the Editable singleton that defines the Editable.JS
+ * API and is the main entry point for Editable.JS.
+ * It also provides the cursor module for cross-browser cursors, and the dom
+ * submodule.
+ *
+ * @module core
+ */
+
+(function() {
+  var isInitialized = false,
+      editableSelector;
+
+  var initialize = function() {
+    if (!isInitialized) {
+      isInitialized = true;
+      editableSelector = '.' + config.editableClass;
+
+      // make sure rangy is initialized. e.g Rangy doesn't initialize
+      // when loaded after the document is ready.
+      if (!rangy.initialized) {
+        rangy.init();
+      }
+
+      dispatcher.setup();
+    }
+  };
+
+  /**
+   * Singleton for the Editable.JS API that is externally visible.
+   * Note that the Editable literal is defined
+   * first in editable.prefix in order for it to be the only externally visible
+   * variable.
+   *
+   * @class Editable
+   * @static
+   */
+  Editable = {
+    /**
+     * Initialzed Editable with a custom configuration
+     */
+    init: function(userConfiguration) {
+      if (isInitialized) {
+        error('Editable is already initialized');
+        return;
+      }
+
+      $.extend(true, config, userConfiguration);
+      initialize();
+    },
+
+
+    /**
+     * Adds the Editable.JS API to the given target elements.
+     * Opposite of {{#crossLink "Editable/remove"}}{{/crossLink}}.
+     * Calls dispatcher.setup to setup all event listeners.
+     *
+     * @method add
+     * @param {HTMLElement|Array(HTMLElement)|String} target A HTMLElement, an
+     *    array of HTMLElement or a query selector representing the target where
+     *    the API should be added on.
+     * @param {Object} [elementConfiguration={}] Configuration options override.
+     * @static
+     * @chainable
+     */
+    add: function(target, elementConfiguration) {
+      initialize();
+      var elemConfig = $.extend(true, {}, config, elementConfiguration);
+      // todo: store element configuration
+      this.enable($(target));
+
+      // todo: check css whitespace settings
+      return this;
+    },
+
+
+    /**
+     * Removes the Editable.JS API from the given target elements.
+     * Opposite of {{#crossLink "Editable/add"}}{{/crossLink}}.
+     *
+     * @method remove
+     * @param {HTMLElement|Array(HTMLElement)|String} target A HTMLElement, an
+     *    array of HTMLElement or a query selector representing the target where
+     *    the API should be removed from.
+     * @static
+     * @chainable
+     */
+    remove: function(target) {
+      var $target = $(target);
+      this.disable($target);
+      $target.removeClass(config.editableDisabledClass);
+      return this;
+    },
+
+
+    /**
+     * Removes the Editable.JS API from the given target elements.
+     * The target elements are marked as disabled.
+     *
+     * @method disable
+     * @param { jQuery element | undefined  } target editable root element(s)
+     *    If no param is specified all editables are disabled.
+     * @static
+     * @chainable
+     */
+    disable: function($elem) {
+      $elem = $elem || $('.' + config.editableClass);
+      $elem
+        .removeAttr('contenteditable')
+        .removeClass(config.editableClass)
+        .addClass(config.editableDisabledClass);
+
+      return this;
+    },
+
+
+    /**
+     * Adds the Editable.JS API to the given target elements.
+     *
+     * @method enable
+     * @param { jQuery element | undefined } target editable root element(s)
+     *    If no param is specified all editables marked as disabled are enabled.
+     * @static
+     * @chainable
+     */
+    enable: function($elem) {
+      $elem = $elem || $('.' + config.editableDisabledClass);
+      $elem
+        .attr('contenteditable', true)
+        .removeClass(config.editableDisabledClass)
+        .addClass(config.editableClass);
+
+      $elem.each(function(index, el) {
+        content.normalizeTags(el);
+        content.normalizeSpaces(el);
+      });
+
+      return this;
+    },
+
+    /**
+     * Set the cursor inside of an editable block.
+     *
+     * @method createCursor
+     * @param position 'beginning', 'end', 'before', 'after'
+     * @static
+     */
+    createCursor: function(element, position) {
+      var cursor;
+      var $host = $(element).closest(editableSelector);
+      position = position || 'beginning';
+
+      if ($host.length) {
+        var range = rangy.createRange();
+
+        if (position === 'beginning' || position === 'end') {
+          range.selectNodeContents(element);
+          range.collapse(position === 'beginning' ? true : false);
+        } else if (element !== $host[0]) {
+          if (position === 'before') {
+            range.setStartBefore(element);
+            range.setEndBefore(element);
+          } else if (position === 'after') {
+            range.setStartAfter(element);
+            range.setEndAfter(element);
+          }
+        } else {
+          error('EditableJS: cannot create cursor outside of an editable block.');
+        }
+
+        cursor = new Cursor($host[0], range);
+      }
+
+      return cursor;
+    },
+
+    createCursorAtBeginning: function(element) {
+      this.createCursor(element, 'beginning');
+    },
+
+    createCursorAtEnd: function(element) {
+      this.createCursor(element, 'end');
+    },
+
+    createCursorBefore: function(element) {
+      this.createCursor(element, 'before');
+    },
+
+    createCursorAfter: function(element) {
+      this.createCursor(element, 'after');
+    },
+
+
+    /**
+     * Subscribe a callback function to a custom event fired by the API.
+     * Opposite of {{#crossLink "Editable/off"}}{{/crossLink}}.
+     *
+     * @method on
+     * @param {String} event The name of the event.
+     * @param {Function} handler The callback to execute in response to the
+     *     event.
+     * @static
+     * @chainable
+     */
+    on: function(event, handler) {
+      initialize();
+      // TODO throw error if event is not one of EVENTS
+      // TODO throw error if handler is not a function
+      dispatcher.addListener(event, handler);
+      return this;
+    },
+
+    /**
+     * Unsubscribe a callback function from a custom event fired by the API.
+     * Opposite of {{#crossLink "Editable/on"}}{{/crossLink}}.
+     *
+     * @method off
+     * @param {String} event The name of the event.
+     * @param {Function|Boolean} handler The callback to remove from the
+     *     event or the special value false to remove all callbacks.
+     * @static
+     * @chainable
+     */
+    off: function(event, handler) {
+      // TODO throw error if event is not one of EVENTS
+      // TODO if handler is flase remove all callbacks
+      dispatcher.removeListener(event, handler);
+      return this;
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/focus:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method focus
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    focus: function(handler) {
+      return this.on('focus', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/blur:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method blur
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    blur: function(handler) {
+      return this.on('blur', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/flow:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method flow
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    flow: function(handler) {
+      return this.on('flow', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/selection:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method selection
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    selection: function(handler) {
+      return this.on('selection', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/cursor:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method cursor
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    cursor: function(handler) {
+      return this.on('cursor', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/newline:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method newline
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    newline: function(handler) {
+      return this.on('newline', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/insert:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method insert
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    insert: function(handler) {
+      return this.on('insert', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/split:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method split
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    split: function(handler) {
+      return this.on('split', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/merge:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method merge
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    merge: function(handler) {
+      return this.on('merge', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/empty:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method empty
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    empty: function(handler) {
+      return this.on('empty', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/switch:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method switch
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    'switch': function(handler) {
+      return this.on('switch', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/move:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method move
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    move: function(handler) {
+      return this.on('move', handler);
+    },
+
+    /**
+     * Subscribe to the {{#crossLink "Editable/clipboard:event"}}{{/crossLink}}
+     * event.
+     *
+     * @method clipboard
+     * @param {Function} handler The callback to execute in response to the
+     *   event.
+     * @static
+     * @chainable
+     */
+    clipboard: function(handler) {
+      return this.on('clipboard', handler);
+    }
+  };
+})();
+
+/**
+ * The Behavior module defines the behavior triggered in response to the Editable.JS
+ * events (see {{#crossLink "Editable"}}{{/crossLink}}).
+ * The behavior can be overwritten by a user with Editable.init() or on
+ * Editable.add() per element.
+ *
+ * @module core
+ * @submodule behavior
+ */
+
+
+var behavior = (function() {
+  /**
+    * Singleton for the behavior module.
+    * Provides default behavior of the Editable.JS API.
+    *
+    * @class Behavior
+    * @static
+    */
+  return {
+    focus: function(element) {
+      log('Default focus behavior');
+    },
+
+    blur: function(element) {
+      log('Default blur behavior');
+      content.cleanInternals(element);
+    },
+
+    flow: function(element, action) {
+      log('Default flow behavior');
+    },
+
+    selection: function(element, selection) {
+      if (selection) {
+        log('Default selection behavior');
+      } else {
+        log('Default selection empty behavior');
+      }
+    },
+
+    cursor: function(element, cursor) {
+      if (cursor) {
+        log('Default cursor behavior');
+      } else {
+        log('Default cursor empty behavior');
+      }
+    },
+
+    newline: function(element, cursor) {
+      log(cursor);
+      log('Default newline behavior');
+
+      var atEnd = cursor.isAtEnd();
+      var br = document.createElement('br');
+      cursor.insertBefore(br);
+
+      if (atEnd) {
+        log('at the end');
+
+        var noWidthSpace = document.createTextNode('\u200B');
+        cursor.insertAfter(noWidthSpace);
+
+        // var trailingBr = document.createElement('br');
+        // trailingBr.setAttribute('type', '-editablejs');
+        // cursor.insertAfter(trailingBr);
+
+      } else {
+        log('not at the end');
+      }
+
+      cursor.setSelection();
+    },
+
+    insert: function(element, direction, cursor) {
+      log('Default insert ' + direction + ' behavior');
+      var parent = element.parentNode;
+      var newElement = element.cloneNode(false);
+      if (newElement.id) newElement.removeAttribute('id');
+
+      switch (direction) {
+      case 'before':
+        parent.insertBefore(newElement, element);
+        element.focus();
+        break;
+      case 'after':
+        parent.insertBefore(newElement, element.nextSibling);
+        newElement.focus();
+        break;
+      }
+    },
+
+    split: function(element, before, after, cursor) {
+      var parent = element.parentNode;
+      var newStart = after.firstChild;
+      parent.insertBefore(before, element);
+      parent.replaceChild(after, element);
+      content.normalizeTags(newStart);
+      content.normalizeSpaces(newStart);
+      newStart.focus();
+    },
+
+    merge: function(element, direction, cursor) {
+      log('Default merge ' + direction + ' behavior');
+      var container, merger, fragment, chunks, i, newChild, range;
+
+      switch (direction) {
+      case 'before':
+        container = block.previous(element);
+        merger = element;
+        break;
+      case 'after':
+        container = element;
+        merger = block.next(element);
+        break;
+      }
+
+      if (!(container && merger))
+        return;
+
+      if (container.childNodes.length > 0)
+        cursor.moveAtTextEnd(container);
+      else
+        cursor.moveAtBeginning(container);
+      cursor.setSelection();
+
+      fragment = document.createDocumentFragment();
+      chunks = merger.childNodes;
+      for (i = 0; i < chunks.length; i++) {
+        fragment.appendChild(chunks[i].cloneNode(true));
+      }
+      newChild = container.appendChild(fragment);
+
+      merger.parentNode.removeChild(merger);
+
+      cursor.save();
+      content.normalizeTags(container);
+      content.normalizeSpaces(container);
+      cursor.restore();
+      cursor.setSelection();
+    },
+
+    empty: function(element) {
+      log('Default empty behavior');
+    },
+
+    'switch': function(element, direction, cursor) {
+      log('Default switch behavior');
+
+      var next, previous;
+
+      switch (direction) {
+      case 'before':
+        previous = block.previous(element);
+        if (previous) {
+          cursor.moveAtTextEnd(previous);
+          cursor.setSelection();
+        }
+        break;
+      case 'after':
+        next = block.next(element);
+        if (next) {
+          cursor.moveAtBeginning(next);
+          cursor.setSelection();
+        }
+        break;
+      }
+    },
+
+    move: function(element, selection, direction) {
+      log('Default move behavior');
+    },
+
+    clipboard: function(element, action, cursor) {
+      log('Default clipboard behavior');
+      var pasteHolder, sel;
+
+      if (action !== 'paste') return;
+
+      element.setAttribute(config.pastingAttribute, true);
+
+      if (cursor.isSelection) {
+        cursor = cursor.deleteContent();
+      }
+
+      pasteHolder = document.createElement('textarea');
+      pasteHolder.setAttribute('style', 'position: absolute; left: -9999px');
+      cursor.insertAfter(pasteHolder);
+      sel = rangy.saveSelection();
+      pasteHolder.focus();
+
+      setTimeout(function() {
+        var pasteValue, pasteElement, cursor;
+        pasteValue = pasteHolder.value;
+        element.removeChild(pasteHolder);
+
+        rangy.restoreSelection(sel);
+        cursor = selectionWatcher.forceCursor();
+        pasteElement = document.createTextNode(pasteValue);
+        content.normalizeSpaces(pasteElement);
+        cursor.insertAfter(pasteElement);
+        cursor.moveAfter(pasteElement);
+        cursor.setSelection();
+
+        element.removeAttribute(config.pastingAttribute);
+      }, 0);
+    }
+  };
+})();
+
+var block = (function() {
+  return {
+    next: function(element) {
+      var next = element.nextElementSibling;
+      if (next && next.getAttribute('contenteditable')) return next;
+      return null;
+    },
+
+    previous: function(element) {
+      var previous = element.previousElementSibling;
+      if (previous && previous.getAttribute('contenteditable')) return previous;
+      return null;
+    }
+  };
+})();
+
+
+/**
+ * Defines all supported event types by Editable.JS and provides default
+ * implementations for them defined in {{#crossLink "Behavior"}}{{/crossLink}}
+ *
+ * @type {Object}
+ */
+var config = {
+  log: false,
+  logErrors: true,
+  editableClass: 'js-editable',
+  editableDisabledClass: 'js-editable-disabled',
+  pastingAttribute: 'data-editable-is-pasting',
+  mouseMoveSelectionChanges: false,
+  boldTag: '',
+  italicTag: '',
+
+  event: {
+    /**
+     * The focus event is triggered when an element gains focus.
+     * The default behavior is to... TODO
+     *
+     * @event focus
+     * @param {HTMLElement} element The element triggering the event.
+     */
+    focus: function(element) {
+      behavior.focus(element);
+    },
+
+    /**
+     * The blur event is triggered when an element looses focus.
+     * The default behavior is to... TODO
+     *
+     * @event blur
+     * @param {HTMLElement} element The element triggering the event.
+     */
+    blur: function(element) {
+      behavior.blur(element);
+    },
+
+    /**
+     * The flow event is triggered when the user starts typing or pause typing.
+     * The default behavior is to... TODO
+     *
+     * @event flow
+     * @param {HTMLElement} element The element triggering the event.
+     * @param {String} action The flow action: "start" or "pause".
+     */
+    flow: function(element, action) {
+      behavior.flow(element, action);
+    },
+
+    /**
+     * The selection event is triggered after the user has selected some
+     * content.
+     * The default behavior is to... TODO
+     *
+     * @event selection
+     * @param {HTMLElement} element The element triggering the event.
+     * @param {Selection} selection The actual Selection object.
+     */
+    selection: function(element, selection) {
+      behavior.selection(element, selection);
+    },
+
+    /**
+     * The cursor event is triggered after cursor position has changed.
+     * The default behavior is to... TODO
+     *
+     * @event cursor
+     * @param {HTMLElement} element The element triggering the event.
+     * @param {Cursor} cursor The actual Cursor object.
+     */
+    cursor: function(element, cursor) {
+      behavior.cursor(element, cursor);
+    },
+
+    /**
+     * The newline event is triggered when a newline should be inserted. This
+     * happens when SHIFT+ENTER key is pressed.
+     * The default behavior is to add a 
+ * + * @event newline + * @param {HTMLElement} element The element triggering the event. + * @param {Cursor} cursor The actual cursor object. + */ + newline: function(element, cursor) { + behavior.newline(element, cursor); + }, + + /** + * The split event is triggered when a block should be splitted into two + * blocks. This happens when ENTER is pressed within a non-empty block. + * The default behavior is to... TODO + * + * @event split + * @param {HTMLElement} element The element triggering the event. + * @param {String} before The HTML string before the split. + * @param {String} after The HTML string after the split. + * @param {Cursor} cursor The actual cursor object. + */ + split: function(element, before, after, cursor) { + behavior.split(element, before, after, cursor); + }, + + + /** + * The insert event is triggered when a new block should be inserted. This + * happens when ENTER key is pressed at the beginning of a block (should + * insert before) or at the end of a block (should insert after). + * The default behavior is to... TODO + * + * @event insert + * @param {HTMLElement} element The element triggering the event. + * @param {String} direction The insert direction: "before" or "after". + * @param {Cursor} cursor The actual cursor object. + */ + insert: function(element, direction, cursor) { + behavior.insert(element, direction, cursor); + }, + + + /** + * The merge event is triggered when two needs to be merged. This happens + * when BACKSPACE is pressed at the beginning of a block (should merge with + * the preceeding block) or DEL is pressed at the end of a block (should + * merge with the following block). + * The default behavior is to... TODO + * + * @event merge + * @param {HTMLElement} element The element triggering the event. + * @param {String} direction The merge direction: "before" or "after". + * @param {Cursor} cursor The actual cursor object. + */ + merge: function(element, direction, cursor) { + behavior.merge(element, direction, cursor); + }, + + /** + * The empty event is triggered when a block is emptied. + * The default behavior is to... TODO + * + * @event empty + * @param {HTMLElement} element The element triggering the event. + */ + empty: function(element) { + behavior.empty(element); + }, + + /** + * The switch event is triggered when the user switches to another block. + * This happens when an ARROW key is pressed near the boundaries of a block. + * The default behavior is to... TODO + * + * @event switch + * @param {HTMLElement} element The element triggering the event. + * @param {String} direction The switch direction: "before" or "after". + * @param {Cursor} cursor The actual cursor object.* + */ + 'switch': function(element, direction, cursor) { + behavior.switch(element, direction, cursor); + }, + + /** + * The move event is triggered when the user moves a selection in a block. + * This happens when the user selects some (or all) content in a block and + * an ARROW key is pressed (up: drag before, down: drag after). + * The default behavior is to... TODO + * + * @event move + * @param {HTMLElement} element The element triggering the event. + * @param {Selection} selection The actual Selection object. + * @param {String} direction The move direction: "before" or "after". + */ + move: function(element, selection, direction) { + behavior.move(element, selection, direction); + }, + + /** + * The clipboard event is triggered when the user copies, pastes or cuts + * a selection within a block. + * The default behavior is to... TODO + * + * @event clipboard + * @param {HTMLElement} element The element triggering the event. + * @param {String} action The clipboard action: "copy", "paste", "cut". + * @param {Cursor} cursor The actual cursor object. + */ + clipboard: function(element, action, cursor) { + behavior.clipboard(element, action, cursor); + } + } +}; + + +var content = (function() { + + var restoreRange = function(host, range, func) { + range = rangeSaveRestore.save(range); + func.call(content); + return rangeSaveRestore.restore(host, range); + }; + + return { + /** + * Remove empty tags and merge consecutive tags (they must have the same + * attributes). + * + * @method normalizeTags + * @param {HTMLElement} element The element to process. + */ + normalizeTags: function(element) { + var i, j, node, sibling; + + var fragment = document.createDocumentFragment(); + + for (i = 0; i < element.childNodes.length; i++) { + node = element.childNodes[i]; + if (!node) continue; + + // skip empty tags, so they'll get removed + if (node.nodeName !== 'BR' && !node.textContent) continue; + + if (node.nodeType === 1 && node.nodeName !== 'BR') { + sibling = node; + while ((sibling = sibling.nextSibling) !== null) { + if (!parser.isSameNode(sibling, node)) + break; + + for (j = 0; j < sibling.childNodes.length; j++) { + node.appendChild(sibling.childNodes[j].cloneNode(true)); + } + + sibling.parentNode.removeChild(sibling); + } + + this.normalizeTags(node); + } + + fragment.appendChild(node.cloneNode(true)); + } + + while (element.firstChild) { + element.removeChild(element.firstChild); + } + element.appendChild(fragment); + }, + + /** + * Clean the element from character, tags, etc... added by the plugin logic. + * + * @method cleanInternals + * @param {HTMLElement} element The element to process. + */ + cleanInternals: function(element) { + element.innerHTML = element.innerHTML.replace(/\u200B/g, '
'); + }, + + /** + * Convert the first and last space to a non breaking space charcter to + * prevent visual collapse by some browser. + * + * @method normalizeSpaces + * @param {HTMLElement} element The element to process. + */ + normalizeSpaces: function(element) { + var nonBreakingSpace = '\u00A0'; + + if (!element) return; + + if (element.nodeType === 3) { + element.nodeValue = element.nodeValue.replace(/^(\s)/, nonBreakingSpace).replace(/(\s)$/, nonBreakingSpace); + } + else { + this.normalizeSpaces(element.firstChild); + this.normalizeSpaces(element.lastChild); + } + }, + + /** + * Get all tags that start or end inside the range + */ + getTags: function(host, range, filterFunc) { + var tags = this.getInnerTags(range, filterFunc); + + // get all tags that surround the range + var node = range.commonAncestorContainer; + while (node !== host) { + if (!filterFunc || filterFunc(node)) { + tags.push(node); + } + node = node.parentNode; + } + return tags; + }, + + getTagsByName: function(host, range, tagName) { + return this.getTags(host, range, function(node) { + return node.nodeName === tagName.toUpperCase(); + }); + }, + + /** + * Get all tags that start or end inside the range + */ + getInnerTags: function(range, filterFunc) { + return range.getNodes([1], filterFunc); + }, + + /** + * Transform an array of elements into a an array + * of tagnames in uppercase + * + * @return example: ['STRONG', 'B'] + */ + getTagNames: function(elements) { + var names = []; + if (!elements) return names; + + for (var i = 0; i < elements.length; i++) { + names.push(elements[i].nodeName); + } + return names; + }, + + isAffectedBy: function(host, range, tagName) { + var elem; + var tags = this.getTags(host, range); + for (var i = 0; i < tags.length; i++) { + elem = tags[i]; + if (elem.nodeName === tagName.toUpperCase()) { + return true; + } + } + + return false; + }, + + /** + * Check if the range selects all of the elements contents, + * not less or more. + * + * @param visible: Only compare visible text. That way it does not + * matter if the user selects an additional whitespace or not. + */ + isExactSelection: function(range, elem, visible) { + var elemRange = rangy.createRange(); + elemRange.selectNodeContents(elem); + if (range.intersectsRange(elemRange)) { + var rangeText = range.toString(); + var elemText = $(elem).text(); + + if (visible) { + rangeText = string.trim(rangeText); + elemText = string.trim(elemText); + } + + return rangeText !== '' && rangeText === elemText; + } else { + return false; + } + }, + + expandTo: function(host, range, elem) { + range.selectNodeContents(elem); + return range; + }, + + toggleTag: function(host, range, elem) { + var elems = this.getTagsByName(host, range, elem.nodeName); + + if (elems.length === 1 && + this.isExactSelection(range, elems[0], 'visible')) { + return this.removeFormatting(host, range, elem.nodeName); + } + + return this.forceWrap(host, range, elem); + }, + + isWrappable: function(range) { + return range.canSurroundContents(); + }, + + forceWrap: function(host, range, elem) { + range = restoreRange(host, range, function(){ + this.nuke(host, range, elem.nodeName); + }); + + // remove all tags if the range is not wrappable + if (!this.isWrappable(range)) { + range = restoreRange(host, range, function(){ + this.nuke(host, range); + }); + } + + this.wrap(range, elem); + return range; + }, + + wrap: function(range, elem) { + elem = string.isString(elem) ? + $(elem)[0] : + elem; + + if (this.isWrappable(range)) { + var a = range.surroundContents(elem); + } else { + console.log('content.wrap(): can not surround range'); + } + }, + + unwrap: function(elem) { + $(elem).contents().unwrap(); + }, + + removeFormatting: function(host, range, tagName) { + return restoreRange(host, range, function(){ + this.nuke(host, range, tagName); + }); + }, + + /** + * Unwrap all tags this range is affected by. + * Can also affect content outside of the range. + */ + nuke: function(host, range, tagName) { + var tags = this.getTags(host, range); + for (var i = 0; i < tags.length; i++) { + var elem = tags[i]; + if ( !tagName || elem.nodeName === tagName.toUpperCase() ) { + this.unwrap(elem); + } + } + }, + + /** + * Insert a single character (or string) before or after the + * the range. + */ + insertCharacter: function(range, character, atStart) { + var insertEl = document.createTextNode(character); + + var boundaryRange = range.cloneRange(); + boundaryRange.collapse(atStart); + boundaryRange.insertNode(insertEl); + boundaryRange.detach(); + + if (atStart) { + range.setStartBefore(insertEl); + } else { + range.setEndAfter(insertEl); + } + range.normalizeBoundaries(); + }, + + /** + * Surround the range with characters like start and end quotes. + * + * @method surround + */ + surround: function(host, range, startCharacter, endCharacter) { + if (!endCharacter) endCharacter = startCharacter; + this.insertCharacter(range, endCharacter, false); + this.insertCharacter(range, startCharacter, true); + return range; + }, + + /** + * Removes a character from the text within a range. + * + * @method deleteCharacter + */ + deleteCharacter: function(host, range, character) { + if (this.containsString(range, character)) { + range.splitBoundaries(); + range = restoreRange(host, range, function() { + var charRegexp = string.regexp(character); + + var textNodes = range.getNodes([3], function(node) { + return node.nodeValue.search(charRegexp) >= 0; + }); + + for (var i = 0; i < textNodes.length; i++) { + var node = textNodes[i]; + node.nodeValue = node.nodeValue.replace(charRegexp, ''); + } + }); + range.normalizeBoundaries(); + } + + return range; + }, + + containsString: function(range, str) { + var text = range.toString(); + return text.indexOf(str) >= 0; + }, + + /** + * Unwrap all tags this range is affected by. + * Can also affect content outside of the range. + */ + nukeTag: function(host, range, tagName) { + var tags = this.getTags(host, range); + for (var i = 0; i < tags.length; i++) { + var elem = tags[i]; + if (elem.nodeName === tagName) + this.unwrap(elem); + } + } + }; +})(); + +/** + * The Cursor module provides a cross-browser abstraction layer for cursor. + * + * @module core + * @submodule cursor + */ + +var Cursor = (function() { + + /** + * Class for the Cursor module. + * + * @class Cursor + * @constructor + */ + var Cursor = function(editableHost, rangyRange) { + this.host = editableHost; + this.range = rangyRange; + this.isCursor = true; + }; + + Cursor.prototype = (function() { + return { + isAtEnd: function() { + return parser.isEndOfHost( + this.host, + this.range.endContainer, + this.range.endOffset); + }, + + isAtTextEnd: function() { + return parser.isTextEndOfHost( + this.host, + this.range.endContainer, + this.range.endOffset); + }, + + isAtBeginning: function() { + return parser.isBeginningOfHost( + this.host, + this.range.startContainer, + this.range.startOffset); + }, + + /** + * Insert content before the cursor + * + * @param DOM node or document fragment + */ + insertBefore: function(element) { + if (parser.isDocumentFragmentWithoutChildren(element)) return; + + var preceedingElement = element; + if (element.nodeType === 11) { // DOCUMENT_FRAGMENT_NODE + var lastIndex = element.childNodes.length - 1; + preceedingElement = element.childNodes[lastIndex]; + } + + this.range.insertNode(element); + this.range.setStartAfter(preceedingElement); + this.range.setEndAfter(preceedingElement); + }, + + /** + * Insert content after the cursor + * + * @param DOM node or document fragment + */ + insertAfter: function(element) { + if (parser.isDocumentFragmentWithoutChildren(element)) return; + this.range.insertNode(element); + }, + + setSelection: function() { + rangy.getSelection().setSingleRange(this.range); + }, + + before: function() { + var fragment = null; + var range = this.range.cloneRange(); + range.setStartBefore(this.host); + fragment = range.cloneContents(); + range.detach(); + return fragment; + }, + + after: function() { + var fragment = null; + var range = this.range.cloneRange(); + range.setEndAfter(this.host); + fragment = range.cloneContents(); + range.detach(); + return fragment; + }, + + /** + * Get the BoundingClientRect of the cursor. + * The returned values are absolute to document.body. + */ + getCoordinates: function() { + var coords = this.range.nativeRange.getBoundingClientRect(); + + // code from mdn: https://developer.mozilla.org/en-US/docs/Web/API/window.scrollX + var x = (window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft; + var y = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop; + + // translate into absolute positions + return { + top: coords.top + y, + bottom: coords.bottom + y, + left: coords.left + x, + right: coords.right + x, + height: coords.height, + width: coords.width + }; + }, + + detach: function() { + this.range.detach(); + }, + + moveBefore: function(element) { + this.setHost(element); + this.range.setStartBefore(element); + this.range.setEndBefore(element); + if (this.isSelection) return new Cursor(this.host, this.range); + }, + + moveAfter: function(element) { + this.setHost(element); + this.range.setStartAfter(element); + this.range.setEndAfter(element); + if (this.isSelection) return new Cursor(this.host, this.range); + }, + + moveAtBeginning: function(element) { + if (!element) element = this.host; + this.setHost(element); + this.range.selectNodeContents(element); + this.range.collapse(true); + if (this.isSelection) return new Cursor(this.host, this.range); + }, + + moveAtEnd: function(element) { + if (!element) element = this.host; + this.setHost(element); + this.range.selectNodeContents(element); + this.range.collapse(false); + if (this.isSelection) return new Cursor(this.host, this.range); + }, + + moveAtTextEnd: function(element) { + return this.moveAtEnd(parser.latestChild(element)); + }, + + setHost: function(element) { + this.host = parser.getHost(element); + if (!this.host) { + error('Can not set cursor outside of an editable block'); + } + }, + + save: function() { + this.savedRangeInfo = rangeSaveRestore.save(this.range); + this.savedRangeInfo.host = this.host; + }, + + restore: function() { + if (this.savedRangeInfo) { + this.host = this.savedRangeInfo.host; + this.range = rangeSaveRestore.restore(this.host, this.savedRangeInfo); + this.savedRangeInfo = undefined; + } else { + error('Could not restore selection'); + } + }, + + equals: function(cursor) { + if (!cursor) return false; + + if (!cursor.host) return false; + if (!cursor.host.isEqualNode(this.host)) return false; + + if (!cursor.range) return false; + if (!cursor.range.equals(this.range)) return false; + + return true; + } + }; + })(); + + return Cursor; +})(); + + +/** + * The Dispatcher module is responsible for dealing with events and their handlers. + * + * @module core + * @submodule dispatcher + */ + +var dispatcher = (function() { + /** + * Contains the list of event listeners grouped by event type. + * + * @private + * @type {Object} + */ + var listeners = {}, + editableSelector; + + /** + * Sets up events that are triggered on modifying an element. + * + * @method setupElementEvents + * @param {HTMLElement} $document: The document element. + * @param {Function} notifier: The callback to be triggered when the event is caught. + */ + var setupElementEvents = function($document, notifier) { + $document.on('focus.editable', editableSelector, function(event) { + if (this.getAttribute(config.pastingAttribute)) return; + notifier('focus', this); + }).on('blur.editable', editableSelector, function(event) { + if (this.getAttribute(config.pastingAttribute)) return; + notifier('blur', this); + }).on('copy.editable', editableSelector, function(event) { + log('Copy'); + notifier('clipboard', this, 'copy', selectionWatcher.getFreshSelection()); + }).on('cut.editable', editableSelector, function(event) { + log('Cut'); + notifier('clipboard', this, 'cut', selectionWatcher.getFreshSelection()); + }).on('paste.editable', editableSelector, function(event) { + log('Paste'); + notifier('clipboard', this, 'paste', selectionWatcher.getFreshSelection()); + }); + }; + + /** + * Sets up events that are triggered on keyboard events. + * Keyboard definitions are in {{#crossLink "Keyboard"}}{{/crossLink}}. + * + * @method setupKeyboardEvents + * @param {HTMLElement} $document: The document element. + * @param {Function} notifier: The callback to be triggered when the event is caught. + */ + var setupKeyboardEvents = function($document, notifier) { + var dispatchSwitchEvent = function(event, element, direction) { + var cursor; + + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) + return; + + cursor = selectionWatcher.getSelection(); + if (!cursor || cursor.isSelection) return; + + // Detect if the browser moved the cursor in the next tick. + // If the cursor stays at its position, fire the switch event. + setTimeout(function() { + var newCursor = selectionWatcher.forceCursor(); + if (newCursor.equals(cursor)) { + event.preventDefault(); + event.stopPropagation(); + notifier('switch', element, direction, newCursor); + } + }, 1); + }; + + $document.on('keydown.editable', editableSelector, function(event) { + keyboard.dispatchKeyEvent(event, this); + }); + + keyboard.on('left', function(event) { + log('Left key pressed'); + dispatchSwitchEvent(event, this, 'before'); + }).on('up', function(event) { + log('Up key pressed'); + dispatchSwitchEvent(event, this, 'before'); + }).on('right', function(event) { + log('Right key pressed'); + dispatchSwitchEvent(event, this, 'after'); + }).on('down', function(event) { + log('Down key pressed'); + dispatchSwitchEvent(event, this, 'after'); + }).on('tab', function(event) { + log('Tab key pressed'); + }).on('shiftTab', function(event) { + log('Shift+Tab key pressed'); + }).on('esc', function(event) { + log('Esc key pressed'); + }).on('backspace', function(event) { + log('Backspace key pressed'); + + var range = selectionWatcher.getFreshRange(); + if (range.isCursor) { + var cursor = range.getCursor(); + if ( cursor.isAtBeginning() ) { + event.preventDefault(); + event.stopPropagation(); + notifier('merge', this, 'before', cursor); + } + } + }).on('delete', function(event) { + log('Delete key pressed'); + + var range = selectionWatcher.getFreshRange(); + if (range.isCursor) { + var cursor = range.getCursor(); + if (cursor.isAtTextEnd()) { + event.preventDefault(); + event.stopPropagation(); + notifier('merge', this, 'after', cursor); + } + } + }).on('enter', function(event) { + log('Enter key pressed'); + + event.preventDefault(); + event.stopPropagation(); + var range = selectionWatcher.getFreshRange(); + var cursor = range.forceCursor(); + + if (cursor.isAtTextEnd()) { + notifier('insert', this, 'after', cursor); + } else if (cursor.isAtBeginning()) { + notifier('insert', this, 'before', cursor); + } else { + notifier('split', this, cursor.before(), cursor.after(), cursor); + } + + }).on('shiftEnter', function(event) { + log('Shift+Enter key pressed'); + event.preventDefault(); + event.stopPropagation(); + var cursor = selectionWatcher.forceCursor(); + notifier('newline', this, cursor); + }); + }; + + /** + * Sets up events that are triggered on a selection change. + * + * @method setupSelectionChangeEvents + * @param {HTMLElement} $document: The document element. + * @param {Function} notifier: The callback to be triggered when the event is caught. + */ + var setupSelectionChangeEvents = function($document, notifier) { + var selectionDirty = false; + var suppressSelectionChanges = false; + + // fires on mousemove (thats probably a bit too much) + // catches changes like 'select all' from context menu + $document.on('selectionchange.editable', function(event) { + if (suppressSelectionChanges) { + selectionDirty = true; + } else { + notifier(); + } + }); + + // listen for selection changes by mouse so we can + // suppress the selectionchange event and only fire the + // change event on mouseup + $document.on('mousedown.editable', editableSelector, function(event) { + if (config.mouseMoveSelectionChanges === false) { + suppressSelectionChanges = true; + + // Without this timeout the previous selection is active + // until the mouseup event (no. not good). + setTimeout(notifier, 0); + } + + $document.on('mouseup.editableSelection', function(event) { + $document.off('.editableSelection'); + suppressSelectionChanges = false; + + if (selectionDirty) { + selectionDirty = false; + notifier(); + } + }); + }); + }; + + /** + * Fallback solution to support selection change events on browsers that don't + * support selectionChange. + * + * @method setupSelectionChangeFallback + * @param {HTMLElement} $document: The document element. + * @param {Function} notifier: The callback to be triggered when the event is caught. + */ + var setupSelectionChangeFallback = function($document, notifier) { + // listen for selection changes by mouse + $document.on('mouseup.editableSelection', function(event) { + + // In Opera when clicking outside of a block + // it does not update the selection as it should + // without the timeout + setTimeout(notifier, 0); + }); + + // listen for selection changes by keys + $document.on('keyup.editable', editableSelector, function(event) { + + // when pressing Command + Shift + Left for example the keyup is only triggered + // after at least two keys are released. Strange. The culprit seems to be the + // Command key. Do we need a workaround? + notifier(); + }); + }; + + return { + addListener: function(event, listener) { + if (listeners[event] === undefined) { + listeners[event] = []; + } + + listeners[event].unshift(listener); + }, + + removeListener: function(event, listener) { + var eventListeners = listeners[event]; + if (eventListeners === undefined) return; + + for (var i=0, len=eventListeners.length; i < len; i++) { + if (eventListeners[i] === listener){ + eventListeners.splice(i, 1); + break; + } + } + }, + + notifyListeners: function(event) { + var eventListeners = listeners[event]; + if (eventListeners === undefined) return; + + for (var i=0, len=eventListeners.length; i < len; i++) { + if (eventListeners[i].apply( + Editable, + Array.prototype.slice.call(arguments).splice(1) + ) === false) + break; + } + }, + + /** + * Sets up all events that Editable.JS is catching. + * + * @method setup + */ + setup: function() { + + var $document = $(document); + var eventType = null; + editableSelector = '.' + config.editableClass; + + listeners = {}; + // TODO check the config.event object to prevent + // registering invalid handlers + for (eventType in config.event) { + this.addListener(eventType, config.event[eventType]); + } + + // setup all events notifications + setupElementEvents($document, this.notifyListeners); + setupKeyboardEvents($document, this.notifyListeners); + + // cache selectionChanged function for simplicity + var selectionChanged = selectionWatcher.selectionChanged; + + if (browserFeatures.selectionchange) { + setupSelectionChangeEvents($document, selectionChanged); + } else { + setupSelectionChangeFallback($document, selectionChanged); + } + } + }; +})(); + +var browserFeatures = (function() { + /** + * Check for contenteditable support + * + * (from Modernizr) + * this is known to false positive in some mobile browsers + * here is a whitelist of verified working browsers: + * https://github.com/NielsLeenheer/html5test/blob/549f6eac866aa861d9649a0707ff2c0157895706/scripts/engine.js#L2083 + */ + var contenteditable = typeof document.documentElement.contentEditable !== 'undefined'; + + /** + * Check selectionchange event (currently supported in IE, Chrome and Safari) + * + * To handle selectionchange in firefox see CKEditor selection object + * https://github.com/ckeditor/ckeditor-dev/blob/master/core/selection.js#L388 + */ + var selectionchange = (function() { + + // not exactly feature detection... is it? + return !(bowser.gecko || bowser.opera); + })(); + + + return { + contenteditable: contenteditable, + selectionchange: selectionchange + }; + +})(); + +jQuery.fn.editable = function(add) { + if (add === undefined || add) { + Editable.add(this); + } else { + Editable.remove(this); + } + + return this; +}; + +/** + * The Keyboard module defines an event API for key events. + * @module core + * @submodule keyboard + */ + +var keyboard = (function() { + var key = { + left: 37, + up: 38, + right: 39, + down: 40, + tab: 9, + esc: 27, + backspace: 8, + 'delete': 46, + enter: 13 + }; + + var listeners = {}; + + var addListener = function(event, listener) { + if (listeners[event] === undefined) { + listeners[event] = []; + } + + listeners[event].push(listener); + }; + + var notifyListeners = function(event, context) { + var eventListeners = listeners[event]; + if (eventListeners === undefined) return; + + for (var i=0, len=eventListeners.length; i < len; i++) { + if (eventListeners[i].apply( + context, + Array.prototype.slice.call(arguments).splice(2) + ) === false) + break; + } + }; + + /** + * Singleton that defines its own API for keyboard events and dispatches + * keyboard events from the browser to this API. + * The keyboard API events are handled by {{#crossLink "Dispatcher"}}{{/crossLink}}. + * @class Keyboard + * @static + */ + return { + dispatchKeyEvent: function(event, target) { + switch (event.keyCode) { + + case key.left: + notifyListeners('left', target, event); + break; + + case key.right: + notifyListeners('right', target, event); + break; + + case key.up: + notifyListeners('up', target, event); + break; + + case key.down: + notifyListeners('down', target, event); + break; + + case key.tab: + if (event.shiftKey) { + notifyListeners('shiftTab', target, event); + } else { + notifyListeners('tab', target, event); + } + break; + + case key.esc: + notifyListeners('esc', target, event); + break; + + case key.backspace: + notifyListeners('backspace', target, event); + break; + + case key['delete']: + notifyListeners('delete', target, event); + break; + + case key.enter: + if (event.shiftKey) { + notifyListeners('shiftEnter', target, event); + } else { + notifyListeners('enter', target, event); + } + break; + + } + }, + + on: function(event, handler) { + addListener(event, handler); + return this; + }, + + // export key-codes for testing + key: key + }; +})(); + +/** + * The parser module provides helper methods to parse html-chunks + * manipulations and helpers for common tasks. + * + * @module core + * @submodule parser + */ + + +/** DOM NODE TYPES: + * + * 'ELEMENT_NODE': 1 + * 'ATTRIBUTE_NODE': 2 + * 'TEXT_NODE': 3 + * 'CDATA_SECTION_NODE': 4 + * 'ENTITY_REFERENCE_NODE': 5 + * 'ENTITY_NODE': 6 + * 'PROCESSING_INSTRUCTION_NODE': 7 + * 'COMMENT_NODE': 8 + * 'DOCUMENT_NODE': 9 + * 'DOCUMENT_TYPE_NODE': 10 + * 'DOCUMENT_FRAGMENT_NODE': 11 + * 'NOTATION_NODE': 12 + */ + +var parser = (function() { + /** + * Singleton that provides DOM lookup helpers. + * @class Parser + * @static + */ + return { + + /** + * Get the editableJS host block of a node. + * + * @method getHost + * @param {DOM Node} + * @return {DOM Node} + */ + getHost: function(node) { + var editableSelector = '.' + config.editableClass; + var hostNode = $(node).closest(editableSelector); + return hostNode.length ? hostNode[0] : undefined; + }, + + /** + * Get the index of a node. + * So that parent.childNodes[ getIndex(node) ] would return the node again + * + * @method getNodeIndex + * @param {HTMLElement} + */ + getNodeIndex: function(node) { + var index = 0; + while ((node = node.previousSibling) !== null) { + index += 1; + } + return index; + }, + + /** + * Check if node contains text or element nodes + * whitespace counts too! + * + * @method isVoid + * @param {HTMLElement} + */ + isVoid: function(node) { + var child, i, len; + var childNodes = node.childNodes; + + for (i = 0, len = childNodes.length; i < len; i++) { + child = childNodes[i]; + + if (child.nodeType === 3 && !this.isVoidTextNode(child)) { + return false; + } else if (child.nodeType === 1) { + return false; + } + } + return true; + }, + + /** + * Check if node is a text node and completely empty without any whitespace + * + * @method isVoidTextNode + * @param {HTMLElement} + */ + isVoidTextNode: function(node) { + return node.nodeType === 3 && !node.nodeValue; + }, + + /** + * Check if node is a text node and contains nothing but whitespace + * + * @method isWhitespaceOnly + * @param {HTMLElement} + */ + isWhitespaceOnly: function(node) { + return node.nodeType === 3 && this.lastOffsetWithContent(node) === 0; + }, + + isLinebreak: function(node) { + return node.nodeType === 1 && node.tagName === 'BR'; + }, + + /** + * Returns the last offset where the cursor can be positioned to + * be at the visible end of its container. + * Currently works only for empty text nodes (not empty tags) + * + * @method isWhitespaceOnly + * @param {HTMLElement} + */ + lastOffsetWithContent: function(node) { + if (node.nodeType === 3) { + return string.trimRight(node.nodeValue).length; + } else { + var i, + childNodes = node.childNodes; + + for (i = childNodes.length - 1; i >= 0; i--) { + node = childNodes[i]; + if (this.isWhitespaceOnly(node) || this.isLinebreak(node)) { + continue; + } else { + // The offset starts at 0 before the first element + // and ends with the length after the last element. + return i + 1; + } + } + return 0; + } + }, + + isBeginningOfHost: function(host, container, offset) { + if (container === host) { + return this.isStartOffset(container, offset); + } + + if (this.isStartOffset(container, offset)) { + var parentContainer = container.parentNode; + + // The index of the element simulates a range offset + // right before the element. + var offsetInParent = this.getNodeIndex(container); + return this.isBeginningOfHost(host, parentContainer, offsetInParent); + } else { + return false; + } + }, + + isEndOfHost: function(host, container, offset) { + if (container === host) { + return this.isEndOffset(container, offset); + } + + if (this.isEndOffset(container, offset)) { + var parentContainer = container.parentNode; + + // The index of the element plus one simulates a range offset + // right after the element. + var offsetInParent = this.getNodeIndex(container) + 1; + return this.isEndOfHost(host, parentContainer, offsetInParent); + } else { + return false; + } + }, + + isStartOffset: function(container, offset) { + if (container.nodeType === 3) { + return offset === 0; + } else { + if (container.childNodes.length === 0) + return true; + else + return container.childNodes[offset] === container.firstChild; + } + }, + + isEndOffset: function(container, offset) { + if (container.nodeType === 3) { + return offset === container.length; + } else { + if (container.childNodes.length === 0) + return true; + else if (offset > 0) + return container.childNodes[offset - 1] === container.lastChild; + else + return false; + } + }, + + isTextEndOfHost: function(host, container, offset) { + if (container === host) { + return this.isTextEndOffset(container, offset); + } + + if (this.isTextEndOffset(container, offset)) { + var parentContainer = container.parentNode; + + // The index of the element plus one simulates a range offset + // right after the element. + var offsetInParent = this.getNodeIndex(container) + 1; + return this.isTextEndOfHost(host, parentContainer, offsetInParent); + } else { + return false; + } + }, + + isTextEndOffset: function(container, offset) { + if (container.nodeType === 3) { + var text = string.trimRight(container.nodeValue); + return offset >= text.length; + } else if (container.childNodes.length === 0) { + return true; + } else { + var lastOffset = this.lastOffsetWithContent(container); + return offset >= lastOffset; + } + }, + + isSameNode: function(target, source) { + var i, len, attr; + + if (target.nodeType !== source.nodeType) + return false; + + if (target.nodeName !== source.nodeName) + return false; + + for (i = 0, len = target.attributes.length; i < len; i++) { + attr = target.attributes[i]; + if (source.getAttribute(attr.name) !== attr.value) + return false; + } + + return true; + }, + + /** + * Return the deepest last child of a node. + * + * @method latestChild + * @param {HTMLElement} container The container to iterate on. + * @return {HTMLElement} THe deepest last child in the container. + */ + latestChild: function(container) { + if (container.lastChild) + return this.latestChild(container.lastChild); + else + return container; + }, + + /** + * Checks if a documentFragment has no children. + * Fragments without children can cause errors if inserted into ranges. + * + * @method isDocumentFragmentWithoutChildren + * @param {HTMLElement} DOM node. + * @return {Boolean} + */ + isDocumentFragmentWithoutChildren: function(fragment) { + if (fragment && + fragment.nodeType === 11 && + fragment.childNodes.length === 0) { + return true; + } + return false; + } + }; +})(); + +/** RangeContainer + * + * primarily used to compare ranges + * its designed to work with undefined ranges as well + * so we can easily compare them without checking for undefined + * all the time + */ +var RangeContainer = function(editableHost, rangyRange) { + this.host = editableHost && editableHost.jquery ? + editableHost[0] : + editableHost; + this.range = rangyRange; + this.isAnythingSelected = (rangyRange !== undefined); + this.isCursor = (this.isAnythingSelected && rangyRange.collapsed); + this.isSelection = (this.isAnythingSelected && !this.isCursor); +}; + +RangeContainer.prototype.getCursor = function() { + if (this.isCursor) { + return new Cursor(this.host, this.range); + } +}; + +RangeContainer.prototype.getSelection = function() { + if (this.isSelection) { + return new Selection(this.host, this.range); + } +}; + +RangeContainer.prototype.forceCursor = function() { + if (this.isSelection) { + var selection = this.getSelection(); + return selection.deleteContent(); + } else { + return this.getCursor(); + } +}; + +RangeContainer.prototype.isDifferentFrom = function(otherRangeContainer) { + otherRangeContainer = otherRangeContainer || new RangeContainer(); + var self = this.range; + var other = otherRangeContainer.range; + if (self && other) { + return !self.equals(other); + } else if (!self && !other) { + return false; + } else { + return true; + } +}; + + +/** + * Inspired by the Selection save and restore module for Rangy by Tim Down + * Saves and restores ranges using invisible marker elements in the DOM. + */ +var rangeSaveRestore = (function() { + var boundaryMarkerId = 0; + + // (U+FEFF) zero width no-break space + var markerTextChar = '\ufeff'; + + var getMarker = function(host, id) { + return host.querySelector('#'+ id); + }; + + return { + + insertRangeBoundaryMarker: function(range, atStart) { + var markerId = 'editable-range-boundary-' + (boundaryMarkerId += 1); + var markerEl; + var doc = window.document; + + // Clone the Range and collapse to the appropriate boundary point + var boundaryRange = range.cloneRange(); + boundaryRange.collapse(atStart); + + // Create the marker element containing a single invisible character using DOM methods and insert it + markerEl = doc.createElement('span'); + markerEl.id = markerId; + markerEl.style.lineHeight = '0'; + markerEl.style.display = 'none'; + // markerEl.className = "rangySelectionBoundary"; + markerEl.appendChild(doc.createTextNode(markerTextChar)); + + boundaryRange.insertNode(markerEl); + boundaryRange.detach(); + return markerEl; + }, + + setRangeBoundary: function(host, range, markerId, atStart) { + var markerEl = getMarker(host, markerId); + if (markerEl) { + range[atStart ? 'setStartBefore' : 'setEndBefore'](markerEl); + markerEl.parentNode.removeChild(markerEl); + } else { + console.log('Marker element has been removed. Cannot restore selection.'); + } + }, + + save: function(range) { + var doc = window.document; + var rangeInfo, startEl, endEl; + + // insert markers + if (range.collapsed) { + endEl = this.insertRangeBoundaryMarker(range, false); + rangeInfo = { + markerId: endEl.id, + collapsed: true + }; + } else { + endEl = this.insertRangeBoundaryMarker(range, false); + startEl = this.insertRangeBoundaryMarker(range, true); + + rangeInfo = { + startMarkerId: startEl.id, + endMarkerId: endEl.id, + collapsed: false + }; + } + + // Adjust each range's boundaries to lie between its markers + if (range.collapsed) { + range.collapseBefore(endEl); + } else { + range.setEndBefore(endEl); + range.setStartAfter(startEl); + } + + return rangeInfo; + }, + + restore: function(host, rangeInfo) { + if (rangeInfo.restored) return; + + var range = rangy.createRange(); + if (rangeInfo.collapsed) { + var markerEl = getMarker(host, rangeInfo.markerId); + if (markerEl) { + markerEl.style.display = 'inline'; + var previousNode = markerEl.previousSibling; + + // Workaround for rangy issue 17 + if (previousNode && previousNode.nodeType === 3) { + markerEl.parentNode.removeChild(markerEl); + range.collapseToPoint(previousNode, previousNode.length); + } else { + range.collapseBefore(markerEl); + markerEl.parentNode.removeChild(markerEl); + } + } else { + console.log('Marker element has been removed. Cannot restore selection.'); + } + } else { + this.setRangeBoundary(host, range, rangeInfo.startMarkerId, true); + this.setRangeBoundary(host, range, rangeInfo.endMarkerId, false); + } + + range.normalizeBoundaries(); + return range; + } + }; +})(); + +/** + * The SelectionWatcher module watches for selection changes inside + * of editable blocks. + * + * @module core + * @submodule selectionWatcher + */ +var selectionWatcher = (function() { + + var rangySelection, + currentSelection, + currentRange; + + /** + * Return a RangeContainer if the current selection is within an editable + * otherwise return an empty RangeContainer + */ + var getRangeContainer = function() { + rangySelection = rangy.getSelection(); + + // rangeCount is 0 or 1 in all browsers except firefox + // firefox can work with multiple ranges + // (on a mac hold down the command key to select multiple ranges) + if (rangySelection.rangeCount) { + var range = rangySelection.getRangeAt(0); + var hostNode = parser.getHost(range.commonAncestorContainer); + if (hostNode) { + return new RangeContainer(hostNode, range); + } + } + + // return an empty range container + return new RangeContainer(); + }; + + return { + + /** + * Gets a fresh RangeContainer with the current selection or cursor. + * + * @return RangeContainer instance + */ + getFreshRange: function() { + return getRangeContainer(); + }, + + /** + * Gets a fresh RangeContainer with the current selection or cursor. + * + * @return Either a Cursor or Selection instance or undefined if + * there is neither a selection or cursor. + */ + getFreshSelection: function() { + var range = getRangeContainer(); + + return range.isCursor ? + range.getCursor() : + range.getSelection(); + }, + + /** + * Get the selection set by the last selectionChanged event. + * Sometimes the event does not fire fast enough and the seleciton + * you get is not the one the user sees. + * In those cases use #getFreshSelection() + * + * @return Either a Cursor or Selection instance or undefined if + * there is neither a selection or cursor. + */ + getSelection: function() { + return currentSelection; + }, + + forceCursor: function() { + var range = getRangeContainer(); + return range.forceCursor(); + }, + + selectionChanged: function() { + var newRange = getRangeContainer(); + if (newRange.isDifferentFrom(currentRange)) { + var lastSelection = currentSelection; + currentRange = newRange; + + // empty selection or cursor + if (lastSelection) { + if (lastSelection.isCursor && !currentRange.isCursor) { + dispatcher.notifyListeners('cursor', lastSelection.host); + } else if (lastSelection.isSelection && !currentRange.isSelection) { + dispatcher.notifyListeners('selection', lastSelection.host); + } + } + + // set new selection or cursor and fire event + if (currentRange.isCursor) { + currentSelection = new Cursor(currentRange.host, currentRange.range); + dispatcher.notifyListeners('cursor', currentSelection.host, currentSelection); + } else if (currentRange.isSelection) { + currentSelection = new Selection(currentRange.host, currentRange.range); + dispatcher.notifyListeners('selection', currentSelection.host, currentSelection); + } else { + currentSelection = undefined; + } + } + } + }; +})(); + +/** + * The Selection module provides a cross-browser abstraction layer for range + * and selection. + * + * @module core + * @submodule selection + */ + +var Selection = (function() { + + /** + * Class that represents a selection and provides functionality to access or + * modify the selection. + * + * @class Selection + * @constructor + */ + var Selection = function(editableHost, rangyRange) { + this.host = editableHost; + this.range = rangyRange; + this.isSelection = true; + }; + + // add Cursor prototpye to Selection prototype chain + var Base = function() {}; + Base.prototype = Cursor.prototype; + Selection.prototype = $.extend(new Base(), { + /** + * Get the text inside the selection. + * + * @method text + */ + text: function() { + return this.range.toString(); + }, + + /** + * Get the html inside the selection. + * + * @method html + */ + html: function() { + return this.range.toHtml(); + }, + + /** + * + * @method isAllSelected + */ + isAllSelected: function() { + return parser.isBeginningOfHost( + this.host, + this.range.startContainer, + this.range.startOffset) && + parser.isTextEndOfHost( + this.host, + this.range.endContainer, + this.range.endOffset); + }, + + /** + * Get the ClientRects of this selection. + * Use this if you want more precision than getBoundingClientRect can give. + */ + getRects: function() { + var coords = this.range.nativeRange.getClientRects(); + + // todo: translate into absolute positions + // just like Cursor#getCoordinates() + return coords; + }, + + /** + * + * @method link + */ + link: function(href, attrs) { + var $link = $(''); + if (href) $link.attr('href', href); + for (var name in attrs) { + $link.attr(name, attrs[name]); + } + + this.forceWrap($link[0]); + }, + + unlink: function() { + this.removeFormatting('a'); + }, + + toggleLink: function(href, attrs) { + var links = this.getTagsByName('a'); + if (links.length >= 1) { + var firstLink = links[0]; + if (this.isExactSelection(firstLink, 'visible')) { + this.unlink(); + } else { + this.expandTo(firstLink); + } + } else { + this.link(href, attrs); + } + }, + + toggle: function(elem) { + this.range = content.toggleTag(this.host, this.range, elem); + this.setSelection(); + }, + + /** + * + * @method makeBold + */ + makeBold: function() { + var $bold = $(config.boldTag); + this.forceWrap($bold[0]); + }, + + toggleBold: function() { + var $bold = $(config.boldTag); + this.toggle($bold[0]); + }, + + /** + * + * @method giveEmphasis + */ + giveEmphasis: function() { + var $em = $(config.italicTag); + this.forceWrap($em[0]); + }, + + toggleEmphasis: function() { + var $italic = $(config.italicTag); + this.toggle($italic[0]); + }, + + /** + * Surround the selection with characters like quotes. + * + * @method surround + * @param {String} E.g. '«' + * @param {String} E.g. '»' + */ + surround: function(startCharacter, endCharacter) { + this.range = content.surround(this.host, this.range, startCharacter, endCharacter); + this.setSelection(); + }, + + removeSurround: function(startCharacter, endCharacter) { + this.range = content.deleteCharacter(this.host, this.range, startCharacter); + this.range = content.deleteCharacter(this.host, this.range, endCharacter); + this.setSelection(); + }, + + toggleSurround: function(startCharacter, endCharacter) { + if (this.containsString(startCharacter) && + this.containsString(endCharacter)) { + this.removeSurround(startCharacter, endCharacter); + } else { + this.surround(startCharacter, endCharacter); + } + }, + + /** + * @method removeFormatting + * @param {String} tagName. E.g. 'a' to remove all links. + */ + removeFormatting: function(tagName) { + this.range = content.removeFormatting(this.host, this.range, tagName); + this.setSelection(); + }, + + /** + * Delete the contents inside the range. After that the selection will be a + * cursor. + * + * @method deleteContent + * @return Cursor instance + */ + deleteContent: function() { + this.range.deleteContents(); + return new Cursor(this.host, this.range); + }, + + /** + * Expand the current selection. + * + * @method expandTo + * @param {DOM Node} + */ + expandTo: function(elem) { + this.range = content.expandTo(this.host, this.range, elem); + this.setSelection(); + }, + + /** + * Wrap the selection with the specified tag. If any other tag with + * the same tagName is affecting the selection this tag will be + * remove first. + * + * @method forceWrap + */ + forceWrap: function(elem) { + this.range = content.forceWrap(this.host, this.range, elem); + this.setSelection(); + }, + + /** + * Get all tags that affect the current selection. Optionally pass a + * method to filter the returned elements. + * + * @method getTags + * @param {Function filter(node)} [Optional] Method to filter the returned + * DOM Nodes. + * @return {Array of DOM Nodes} + */ + getTags: function(filterFunc) { + return content.getTags(this.host, this.range, filterFunc); + }, + + /** + * Get all tags of the specified type that affect the current selection. + * + * @method getTagsByName + * @param {String} tagName. E.g. 'a' to get all links. + * @return {Array of DOM Nodes} + */ + getTagsByName: function(tagName) { + return content.getTagsByName(this.host, this.range, tagName); + }, + + /** + * Check if the selection is the same as the elements contents. + * + * @method isExactSelection + * @param {DOM Node} + * @param {flag: undefined or 'visible'} if 'visible' is passed + * whitespaces at the beginning or end of the selection will + * be ignored. + * @return {Boolean} + */ + isExactSelection: function(elem, onlyVisible) { + return content.isExactSelection(this.range, elem, onlyVisible); + }, + + /** + * Check if the selection contains the passed string. + * + * @method containsString + * @return {Boolean} + */ + containsString: function(str) { + return content.containsString(this.range, str); + }, + + /** + * Delete all occurences of the specified character from the + * selection. + * + * @method deleteCharacter + */ + deleteCharacter: function(character) { + this.range = content.deleteCharacter(this.host, this.range, character); + this.setSelection(); + } + }); + + return Selection; +})(); + + window.Editable = Editable; +})(window, document, window.jQuery); \ No newline at end of file diff --git a/editable.min.js b/editable.min.js new file mode 100644 index 00000000..7ef23480 --- /dev/null +++ b/editable.min.js @@ -0,0 +1,3 @@ +window.rangy=function(){function e(e,t){var n=typeof e[t];return n==f||!(n!=d||!e[t])||"unknown"==n}function t(e,t){return!(typeof e[t]!=d||!e[t])}function n(e,t){return typeof e[t]!=l}function r(e){return function(t,n){for(var r=n.length;r--;)if(!e(t,n[r]))return!1;return!0}}function o(e){return e&&v(e,m)&&R(e,p)}function i(e){window.alert("Rangy not supported in your browser. Reason: "+e),N.initialized=!0,N.supported=!1}function s(e){var t="Rangy warning: "+e;N.config.alertOnWarn?window.alert(t):typeof window.console!=l&&typeof window.console.log!=l&&window.console.log(t)}function a(){if(!N.initialized){var n,r=!1,s=!1;e(document,"createRange")&&(n=document.createRange(),v(n,g)&&R(n,h)&&(r=!0),n.detach());var a=t(document,"body")?document.body:document.getElementsByTagName("body")[0];a&&e(a,"createTextRange")&&(n=a.createTextRange(),o(n)&&(s=!0)),r||s||i("Neither Range nor TextRange are implemented"),N.initialized=!0,N.features={implementsDomRange:r,implementsTextRange:s};for(var c=S.concat(y),u=0,d=c.length;d>u;++u)try{c[u](N)}catch(f){t(window,"console")&&e(window.console,"log")&&window.console.log("Init listener threw an exception. Continuing.",f)}}}function c(e){e=e||window,a();for(var t=0,n=E.length;n>t;++t)E[t](e)}function u(e){this.name=e,this.initialized=!1,this.supported=!1}var d="object",f="function",l="undefined",h=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer","START_TO_START","START_TO_END","END_TO_START","END_TO_END"],g=["setStart","setStartBefore","setStartAfter","setEnd","setEndBefore","setEndAfter","collapse","selectNode","selectNodeContents","compareBoundaryPoints","deleteContents","extractContents","cloneContents","insertNode","surroundContents","cloneRange","toString","detach"],p=["boundingHeight","boundingLeft","boundingTop","boundingWidth","htmlText","text"],m=["collapse","compareEndPoints","duplicate","getBookmark","moveToBookmark","moveToElementText","parentElement","pasteHTML","select","setEndPoint","getBoundingClientRect"],v=r(e),C=r(t),R=r(n),N={version:"1.2.3",initialized:!1,supported:!0,util:{isHostMethod:e,isHostObject:t,isHostProperty:n,areHostMethods:v,areHostObjects:C,areHostProperties:R,isTextRange:o},features:{},modules:{},config:{alertOnWarn:!1,preferTextRange:!1}};N.fail=i,N.warn=s,{}.hasOwnProperty?N.util.extend=function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])}:i("hasOwnProperty not supported");var y=[],S=[];N.init=a,N.addInitListener=function(e){N.initialized?e(N):y.push(e)};var E=[];N.addCreateMissingNativeApiListener=function(e){E.push(e)},N.createMissingNativeApi=c,u.prototype.fail=function(e){throw this.initialized=!0,this.supported=!1,Error("Module '"+this.name+"' failed to load: "+e)},u.prototype.warn=function(e){N.warn("Module "+this.name+": "+e)},u.prototype.createError=function(e){return Error("Error in Rangy "+this.name+" module: "+e)},N.createModule=function(e,t){var n=new u(e);N.modules[e]=n,S.push(function(e){t(e,n),n.initialized=!0,n.supported=!0})},N.requireModules=function(e){for(var t,n,r=0,o=e.length;o>r;++r){if(n=e[r],t=N.modules[n],!(t&&t instanceof u))throw Error("Module '"+n+"' not found");if(!t.supported)throw Error("Module '"+n+"' not supported")}};var w=!1,T=function(){w||(w=!0,N.initialized||a())};return typeof window==l?(i("No window found"),void 0):typeof document==l?(i("No document found"),void 0):(e(document,"addEventListener")&&document.addEventListener("DOMContentLoaded",T,!1),e(window,"addEventListener")?window.addEventListener("load",T,!1):e(window,"attachEvent")?window.attachEvent("onload",T):i("Window does not have required addEventListener or attachEvent method"),N)}(),rangy.createModule("DomUtil",function(e,t){function n(e){var t;return typeof e.namespaceURI==T||null===(t=e.namespaceURI)||"http://www.w3.org/1999/xhtml"==t}function r(e){var t=e.parentNode;return 1==t.nodeType?t:null}function o(e){for(var t=0;e=e.previousSibling;)t++;return t}function i(e){var t;return u(e)?e.length:(t=e.childNodes)?t.length:0}function s(e,t){var n,r=[];for(n=e;n;n=n.parentNode)r.push(n);for(n=t;n;n=n.parentNode)if(_(r,n))return n;return null}function a(e,t,n){for(var r=n?t:t.parentNode;r;){if(r===e)return!0;r=r.parentNode}return!1}function c(e,t,n){for(var r,o=n?e:e.parentNode;o;){if(r=o.parentNode,r===t)return o;o=r}return null}function u(e){var t=e.nodeType;return 3==t||4==t||8==t}function d(e,t){var n=t.nextSibling,r=t.parentNode;return n?r.insertBefore(e,n):r.appendChild(e),e}function f(e,t){var n=e.cloneNode(!1);return n.deleteData(0,t),e.deleteData(t,e.length-t),d(n,e),n}function l(e){if(9==e.nodeType)return e;if(typeof e.ownerDocument!=T)return e.ownerDocument;if(typeof e.document!=T)return e.document;if(e.parentNode)return l(e.parentNode);throw Error("getDocument: no document found for node")}function h(e){var t=l(e);if(typeof t.defaultView!=T)return t.defaultView;if(typeof t.parentWindow!=T)return t.parentWindow;throw Error("Cannot get a window object for node")}function g(e){if(typeof e.contentDocument!=T)return e.contentDocument;if(typeof e.contentWindow!=T)return e.contentWindow.document;throw Error("getIframeWindow: No Document object found for iframe element")}function p(e){if(typeof e.contentWindow!=T)return e.contentWindow;if(typeof e.contentDocument!=T)return e.contentDocument.defaultView;throw Error("getIframeWindow: No Window object found for iframe element")}function m(e){return b.isHostObject(e,"body")?e.body:e.getElementsByTagName("body")[0]}function v(e){for(var t;t=e.parentNode;)e=t;return e}function C(e,t,n,r){var i,a,u,d,f;if(e==n)return t===r?0:r>t?-1:1;if(i=c(n,e,!0))return o(i)>=t?-1:1;if(i=c(e,n,!0))return r>o(i)?-1:1;if(a=s(e,n),u=e===a?a:c(e,a,!0),d=n===a?a:c(n,a,!0),u===d)throw Error("comparePoints got to case 4 and childA and childB are the same!");for(f=a.firstChild;f;){if(f===u)return-1;if(f===d)return 1;f=f.nextSibling}throw Error("Should not be here!")}function R(e){for(var t,n=l(e).createDocumentFragment();t=e.firstChild;)n.appendChild(t);return n}function N(e){if(!e)return"[No node]";if(u(e))return'"'+e.data+'"';if(1==e.nodeType){var t=e.id?' id="'+e.id+'"':"";return"<"+e.nodeName+t+">["+e.childNodes.length+"]"}return e.nodeName}function y(e){this.root=e,this._next=e}function S(e){return new y(e)}function E(e,t){this.node=e,this.offset=t}function w(e){this.code=this[e],this.codeName=e,this.message="DOMException: "+this.codeName}var T="undefined",b=e.util;b.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||t.fail("document missing a Node creation method"),b.isHostMethod(document,"getElementsByTagName")||t.fail("document missing getElementsByTagName method");var O=document.createElement("div");b.areHostMethods(O,["insertBefore","appendChild","cloneNode"]||!b.areHostObjects(O,["previousSibling","nextSibling","childNodes","parentNode"]))||t.fail("Incomplete Element implementation"),b.isHostProperty(O,"innerHTML")||t.fail("Element is missing innerHTML property");var D=document.createTextNode("test");b.areHostMethods(D,["splitText","deleteData","insertData","appendData","cloneNode"]||!b.areHostObjects(O,["previousSibling","nextSibling","childNodes","parentNode"])||!b.areHostProperties(D,["data"]))||t.fail("Incomplete Text Node implementation");var _=function(e,t){for(var n=e.length;n--;)if(e[n]===t)return!0;return!1};y.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var e,t,n=this._current=this._next;if(this._current)if(e=n.firstChild)this._next=e;else{for(t=null;n!==this.root&&!(t=n.nextSibling);)n=n.parentNode;this._next=t}return this._current},detach:function(){this._current=this._next=this.root=null}},E.prototype={equals:function(e){return this.node===e.node&this.offset==e.offset},inspect:function(){return"[DomPosition("+N(this.node)+":"+this.offset+")]"}},w.prototype={INDEX_SIZE_ERR:1,HIERARCHY_REQUEST_ERR:3,WRONG_DOCUMENT_ERR:4,NO_MODIFICATION_ALLOWED_ERR:7,NOT_FOUND_ERR:8,NOT_SUPPORTED_ERR:9,INVALID_STATE_ERR:11},w.prototype.toString=function(){return this.message},e.dom={arrayContains:_,isHtmlNamespace:n,parentElement:r,getNodeIndex:o,getNodeLength:i,getCommonAncestor:s,isAncestorOf:a,getClosestAncestorIn:c,isCharacterDataNode:u,insertAfter:d,splitDataNode:f,getDocument:l,getWindow:h,getIframeWindow:p,getIframeDocument:g,getBody:m,getRootContainer:v,comparePoints:C,inspectNode:N,fragmentFromNodeChildren:R,createIterator:S,DomPosition:E},e.DOMException=w}),rangy.createModule("DomRange",function(e){function t(e,t){return 3!=e.nodeType&&(P.isAncestorOf(e,t.startContainer,!0)||P.isAncestorOf(e,t.endContainer,!0))}function n(e){return P.getDocument(e.startContainer)}function r(e,t,n){var r=e._listeners[t];if(r)for(var o=0,i=r.length;i>o;++o)r[o].call(e,{target:e,args:n})}function o(e){return new L(e.parentNode,P.getNodeIndex(e))}function i(e){return new L(e.parentNode,P.getNodeIndex(e)+1)}function s(e,t,n){var r=11==e.nodeType?e.firstChild:e;return P.isCharacterDataNode(t)?n==t.length?P.insertAfter(e,t):t.parentNode.insertBefore(e,0==n?t:P.splitDataNode(t,n)):n>=t.childNodes.length?t.appendChild(e):t.insertBefore(e,t.childNodes[n]),r}function a(e){for(var t,r,o,i=n(e.range).createDocumentFragment();r=e.next();){if(t=e.isPartiallySelectedSubtree(),r=r.cloneNode(!t),t&&(o=e.getSubtreeIterator(),r.appendChild(a(o)),o.detach(!0)),10==r.nodeType)throw new W("HIERARCHY_REQUEST_ERR");i.appendChild(r)}return i}function c(e,t,n){var r,o;n=n||{stop:!1};for(var i,s;i=e.next();)if(e.isPartiallySelectedSubtree()){if(t(i)===!1)return n.stop=!0,void 0;if(s=e.getSubtreeIterator(),c(s,t,n),s.detach(!0),n.stop)return}else for(r=P.createIterator(i);o=r.next();)if(t(o)===!1)return n.stop=!0,void 0}function u(e){for(var t;e.next();)e.isPartiallySelectedSubtree()?(t=e.getSubtreeIterator(),u(t),t.detach(!0)):e.remove()}function d(e){for(var t,r,o=n(e.range).createDocumentFragment();t=e.next();){if(e.isPartiallySelectedSubtree()?(t=t.cloneNode(!1),r=e.getSubtreeIterator(),t.appendChild(d(r)),r.detach(!0)):e.remove(),10==t.nodeType)throw new W("HIERARCHY_REQUEST_ERR");o.appendChild(t)}return o}function f(e,t,n){var r,o=!(!t||!t.length),i=!!n;o&&(r=RegExp("^("+t.join("|")+")$"));var s=[];return c(new h(e,!1),function(e){o&&!r.test(e.nodeType)||i&&!n(e)||s.push(e)}),s}function l(e){var t=e.getName===void 0?"Range":e.getName();return"["+t+"("+P.inspectNode(e.startContainer)+":"+e.startOffset+", "+P.inspectNode(e.endContainer)+":"+e.endOffset+")]"}function h(e,t){if(this.range=e,this.clonePartiallySelectedTextNodes=t,!e.collapsed){this.sc=e.startContainer,this.so=e.startOffset,this.ec=e.endContainer,this.eo=e.endOffset;var n=e.commonAncestorContainer;this.sc===this.ec&&P.isCharacterDataNode(this.sc)?(this.isSingleCharacterDataNode=!0,this._first=this._last=this._next=this.sc):(this._first=this._next=this.sc!==n||P.isCharacterDataNode(this.sc)?P.getClosestAncestorIn(this.sc,n,!0):this.sc.childNodes[this.so],this._last=this.ec!==n||P.isCharacterDataNode(this.ec)?P.getClosestAncestorIn(this.ec,n,!0):this.ec.childNodes[this.eo-1])}}function g(e){this.code=this[e],this.codeName=e,this.message="RangeException: "+this.codeName}function p(e,t,n){this.nodes=f(e,t,n),this._next=this.nodes[0],this._position=0}function m(e){return function(t,n){for(var r,o=n?t:t.parentNode;o;){if(r=o.nodeType,P.arrayContains(e,r))return o;o=o.parentNode}return null}}function v(e,t){if(Q(e,t))throw new g("INVALID_NODE_TYPE_ERR")}function C(e){if(!e.startContainer)throw new W("INVALID_STATE_ERR")}function R(e,t){if(!P.arrayContains(t,e.nodeType))throw new g("INVALID_NODE_TYPE_ERR")}function N(e,t){if(0>t||t>(P.isCharacterDataNode(e)?e.length:e.childNodes.length))throw new W("INDEX_SIZE_ERR")}function y(e,t){if(Y(e,!0)!==Y(t,!0))throw new W("WRONG_DOCUMENT_ERR")}function S(e){if(K(e,!0))throw new W("NO_MODIFICATION_ALLOWED_ERR")}function E(e,t){if(!e)throw new W(t)}function w(e){return!P.arrayContains(z,e.nodeType)&&!Y(e,!0)}function T(e,t){return(P.isCharacterDataNode(e)?e.length:e.childNodes.length)>=t}function b(e){return!!e.startContainer&&!!e.endContainer&&!w(e.startContainer)&&!w(e.endContainer)&&T(e.startContainer,e.startOffset)&&T(e.endContainer,e.endOffset)}function O(e){if(C(e),!b(e))throw Error("Range error: Range is no longer valid after DOM mutation ("+e.inspect()+")")}function D(){}function _(e){e.START_TO_START=et,e.START_TO_END=tt,e.END_TO_END=nt,e.END_TO_START=rt,e.NODE_BEFORE=ot,e.NODE_AFTER=it,e.NODE_BEFORE_AND_AFTER=st,e.NODE_INSIDE=at}function A(e){_(e),_(e.prototype)}function x(e,t){return function(){O(this);var n,r,o=this.startContainer,s=this.startOffset,a=this.commonAncestorContainer,u=new h(this,!0);o!==a&&(n=P.getClosestAncestorIn(o,a,!0),r=i(n),o=r.node,s=r.offset),c(u,S),u.reset();var d=e(u);return u.detach(),t(this,o,s,o,s),d}}function B(n,r,s){function a(e,t){return function(n){C(this),R(n,F),R(q(n),z);var r=(e?o:i)(n);(t?c:f)(this,r.node,r.offset)}}function c(e,t,n){var o=e.endContainer,i=e.endOffset;(t!==e.startContainer||n!==e.startOffset)&&((q(t)!=q(o)||1==P.comparePoints(t,n,o,i))&&(o=t,i=n),r(e,t,n,o,i))}function f(e,t,n){var o=e.startContainer,i=e.startOffset;(t!==e.endContainer||n!==e.endOffset)&&((q(t)!=q(o)||-1==P.comparePoints(t,n,o,i))&&(o=t,i=n),r(e,o,i,t,n))}function l(e,t,n){(t!==e.startContainer||n!==e.startOffset||t!==e.endContainer||n!==e.endOffset)&&r(e,t,n,t,n)}n.prototype=new D,e.util.extend(n.prototype,{setStart:function(e,t){C(this),v(e,!0),N(e,t),c(this,e,t)},setEnd:function(e,t){C(this),v(e,!0),N(e,t),f(this,e,t)},setStartBefore:a(!0,!0),setStartAfter:a(!1,!0),setEndBefore:a(!0,!1),setEndAfter:a(!1,!1),collapse:function(e){O(this),e?r(this,this.startContainer,this.startOffset,this.startContainer,this.startOffset):r(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(e){C(this),v(e,!0),r(this,e,0,e,P.getNodeLength(e))},selectNode:function(e){C(this),v(e,!1),R(e,F);var t=o(e),n=i(e);r(this,t.node,t.offset,n.node,n.offset)},extractContents:x(d,r),deleteContents:x(u,r),canSurroundContents:function(){O(this),S(this.startContainer),S(this.endContainer);var e=new h(this,!0),n=e._first&&t(e._first,this)||e._last&&t(e._last,this);return e.detach(),!n},detach:function(){s(this)},splitBoundaries:function(){O(this);var e=this.startContainer,t=this.startOffset,n=this.endContainer,o=this.endOffset,i=e===n;P.isCharacterDataNode(n)&&o>0&&n.length>o&&P.splitDataNode(n,o),P.isCharacterDataNode(e)&&t>0&&e.length>t&&(e=P.splitDataNode(e,t),i?(o-=t,n=e):n==e.parentNode&&o>=P.getNodeIndex(e)&&o++,t=0),r(this,e,t,n,o)},normalizeBoundaries:function(){O(this);var e=this.startContainer,t=this.startOffset,n=this.endContainer,o=this.endOffset,i=function(e){var t=e.nextSibling;t&&t.nodeType==e.nodeType&&(n=e,o=e.length,e.appendData(t.data),t.parentNode.removeChild(t))},s=function(r){var i=r.previousSibling;if(i&&i.nodeType==r.nodeType){e=r;var s=r.length;if(t=i.length,r.insertData(0,i.data),i.parentNode.removeChild(i),e==n)o+=t,n=e;else if(n==r.parentNode){var a=P.getNodeIndex(r);o==a?(n=r,o=s):o>a&&o--}}},a=!0;if(P.isCharacterDataNode(n))n.length==o&&i(n);else{if(o>0){var c=n.childNodes[o-1];c&&P.isCharacterDataNode(c)&&i(c)}a=!this.collapsed}if(a){if(P.isCharacterDataNode(e))0==t&&s(e);else if(e.childNodes.length>t){var u=e.childNodes[t];u&&P.isCharacterDataNode(u)&&s(u)}}else e=n,t=o;r(this,e,t,n,o)},collapseToPoint:function(e,t){C(this),v(e,!0),N(e,t),l(this,e,t)}}),A(n)}function I(e){e.collapsed=e.startContainer===e.endContainer&&e.startOffset===e.endOffset,e.commonAncestorContainer=e.collapsed?e.startContainer:P.getCommonAncestor(e.startContainer,e.endContainer)}function k(e,t,n,o,i){var s=e.startContainer!==t||e.startOffset!==n,a=e.endContainer!==o||e.endOffset!==i;e.startContainer=t,e.startOffset=n,e.endContainer=o,e.endOffset=i,I(e),r(e,"boundarychange",{startMoved:s,endMoved:a})}function M(e){C(e),e.startContainer=e.startOffset=e.endContainer=e.endOffset=null,e.collapsed=e.commonAncestorContainer=null,r(e,"detach",null),e._listeners=null}function H(e){this.startContainer=e,this.startOffset=0,this.endContainer=e,this.endOffset=0,this._listeners={boundarychange:[],detach:[]},I(this)}e.requireModules(["DomUtil"]);var P=e.dom,L=P.DomPosition,W=e.DOMException;h.prototype={_current:null,_next:null,_first:null,_last:null,isSingleCharacterDataNode:!1,reset:function(){this._current=null,this._next=this._first},hasNext:function(){return!!this._next},next:function(){var e=this._current=this._next;return e&&(this._next=e!==this._last?e.nextSibling:null,P.isCharacterDataNode(e)&&this.clonePartiallySelectedTextNodes&&(e===this.ec&&(e=e.cloneNode(!0)).deleteData(this.eo,e.length-this.eo),this._current===this.sc&&(e=e.cloneNode(!0)).deleteData(0,this.so))),e},remove:function(){var e,t,n=this._current;!P.isCharacterDataNode(n)||n!==this.sc&&n!==this.ec?n.parentNode&&n.parentNode.removeChild(n):(e=n===this.sc?this.so:0,t=n===this.ec?this.eo:n.length,e!=t&&n.deleteData(e,t-e))},isPartiallySelectedSubtree:function(){var e=this._current;return t(e,this.range)},getSubtreeIterator:function(){var e;if(this.isSingleCharacterDataNode)e=this.range.cloneRange(),e.collapse();else{e=new H(n(this.range));var t=this._current,r=t,o=0,i=t,s=P.getNodeLength(t);P.isAncestorOf(t,this.sc,!0)&&(r=this.sc,o=this.so),P.isAncestorOf(t,this.ec,!0)&&(i=this.ec,s=this.eo),k(e,r,o,i,s)}return new h(e,this.clonePartiallySelectedTextNodes)},detach:function(e){e&&this.range.detach(),this.range=this._current=this._next=this._first=this._last=this.sc=this.so=this.ec=this.eo=null}},g.prototype={BAD_BOUNDARYPOINTS_ERR:1,INVALID_NODE_TYPE_ERR:2},g.prototype.toString=function(){return this.message},p.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){return this._current=this._next,this._next=this.nodes[++this._position],this._current},detach:function(){this._current=this._next=this.nodes=null}};var F=[1,3,4,5,7,8,10],z=[2,9,11],U=[5,6,10,12],V=[1,3,4,5,7,8,10,11],j=[1,3,4,5,7,8],q=P.getRootContainer,Y=m([9,11]),K=m(U),Q=m([6,10,12]),$=document.createElement("style"),X=!1;try{$.innerHTML="x",X=3==$.firstChild.nodeType}catch(G){}e.features.htmlParsingConforms=X;var Z=X?function(e){var t=this.startContainer,n=P.getDocument(t);if(!t)throw new W("INVALID_STATE_ERR");var r=null;return 1==t.nodeType?r=t:P.isCharacterDataNode(t)&&(r=P.parentElement(t)),r=null===r||"HTML"==r.nodeName&&P.isHtmlNamespace(P.getDocument(r).documentElement)&&P.isHtmlNamespace(r)?n.createElement("body"):r.cloneNode(!1),r.innerHTML=e,P.fragmentFromNodeChildren(r)}:function(e){C(this);var t=n(this),r=t.createElement("body");return r.innerHTML=e,P.fragmentFromNodeChildren(r)},J=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],et=0,tt=1,nt=2,rt=3,ot=0,it=1,st=2,at=3;D.prototype={attachListener:function(e,t){this._listeners[e].push(t)},compareBoundaryPoints:function(e,t){O(this),y(this.startContainer,t.startContainer);var n,r,o,i,s=e==rt||e==et?"start":"end",a=e==tt||e==et?"start":"end";return n=this[s+"Container"],r=this[s+"Offset"],o=t[a+"Container"],i=t[a+"Offset"],P.comparePoints(n,r,o,i)},insertNode:function(e){if(O(this),R(e,V),S(this.startContainer),P.isAncestorOf(e,this.startContainer,!0))throw new W("HIERARCHY_REQUEST_ERR");var t=s(e,this.startContainer,this.startOffset);this.setStartBefore(t)},cloneContents:function(){O(this);var e,t;if(this.collapsed)return n(this).createDocumentFragment();if(this.startContainer===this.endContainer&&P.isCharacterDataNode(this.startContainer))return e=this.startContainer.cloneNode(!0),e.data=e.data.slice(this.startOffset,this.endOffset),t=n(this).createDocumentFragment(),t.appendChild(e),t;var r=new h(this,!0);return e=a(r),r.detach(),e},canSurroundContents:function(){O(this),S(this.startContainer),S(this.endContainer);var e=new h(this,!0),n=e._first&&t(e._first,this)||e._last&&t(e._last,this);return e.detach(),!n},surroundContents:function(e){if(R(e,j),!this.canSurroundContents())throw new g("BAD_BOUNDARYPOINTS_ERR");var t=this.extractContents();if(e.hasChildNodes())for(;e.lastChild;)e.removeChild(e.lastChild);s(e,this.startContainer,this.startOffset),e.appendChild(t),this.selectNode(e)},cloneRange:function(){O(this);for(var e,t=new H(n(this)),r=J.length;r--;)e=J[r],t[e]=this[e];return t},toString:function(){O(this);var e=this.startContainer;if(e===this.endContainer&&P.isCharacterDataNode(e))return 3==e.nodeType||4==e.nodeType?e.data.slice(this.startOffset,this.endOffset):"";var t=[],n=new h(this,!0);return c(n,function(e){(3==e.nodeType||4==e.nodeType)&&t.push(e.data)}),n.detach(),t.join("")},compareNode:function(e){O(this);var t=e.parentNode,n=P.getNodeIndex(e);if(!t)throw new W("NOT_FOUND_ERR");var r=this.comparePoint(t,n),o=this.comparePoint(t,n+1);return 0>r?o>0?st:ot:o>0?it:at},comparePoint:function(e,t){return O(this),E(e,"HIERARCHY_REQUEST_ERR"),y(e,this.startContainer),0>P.comparePoints(e,t,this.startContainer,this.startOffset)?-1:P.comparePoints(e,t,this.endContainer,this.endOffset)>0?1:0},createContextualFragment:Z,toHtml:function(){O(this);var e=n(this).createElement("div");return e.appendChild(this.cloneContents()),e.innerHTML},intersectsNode:function(e,t){if(O(this),E(e,"NOT_FOUND_ERR"),P.getDocument(e)!==n(this))return!1;var r=e.parentNode,o=P.getNodeIndex(e);E(r,"NOT_FOUND_ERR");var i=P.comparePoints(r,o,this.endContainer,this.endOffset),s=P.comparePoints(r,o+1,this.startContainer,this.startOffset);return t?0>=i&&s>=0:0>i&&s>0},isPointInRange:function(e,t){return O(this),E(e,"HIERARCHY_REQUEST_ERR"),y(e,this.startContainer),P.comparePoints(e,t,this.startContainer,this.startOffset)>=0&&0>=P.comparePoints(e,t,this.endContainer,this.endOffset)},intersectsRange:function(e,t){if(O(this),n(e)!=n(this))throw new W("WRONG_DOCUMENT_ERR");var r=P.comparePoints(this.startContainer,this.startOffset,e.endContainer,e.endOffset),o=P.comparePoints(this.endContainer,this.endOffset,e.startContainer,e.startOffset);return t?0>=r&&o>=0:0>r&&o>0},intersection:function(e){if(this.intersectsRange(e)){var t=P.comparePoints(this.startContainer,this.startOffset,e.startContainer,e.startOffset),n=P.comparePoints(this.endContainer,this.endOffset,e.endContainer,e.endOffset),r=this.cloneRange();return-1==t&&r.setStart(e.startContainer,e.startOffset),1==n&&r.setEnd(e.endContainer,e.endOffset),r}return null},union:function(e){if(this.intersectsRange(e,!0)){var t=this.cloneRange();return-1==P.comparePoints(e.startContainer,e.startOffset,this.startContainer,this.startOffset)&&t.setStart(e.startContainer,e.startOffset),1==P.comparePoints(e.endContainer,e.endOffset,this.endContainer,this.endOffset)&&t.setEnd(e.endContainer,e.endOffset),t}throw new g("Ranges do not intersect")},containsNode:function(e,t){return t?this.intersectsNode(e,!1):this.compareNode(e)==at},containsNodeContents:function(e){return this.comparePoint(e,0)>=0&&0>=this.comparePoint(e,P.getNodeLength(e))},containsRange:function(e){return this.intersection(e).equals(e)},containsNodeText:function(e){var t=this.cloneRange();t.selectNode(e);var n=t.getNodes([3]);if(n.length>0){t.setStart(n[0],0);var r=n.pop();t.setEnd(r,r.length);var o=this.containsRange(t);return t.detach(),o}return this.containsNodeContents(e)},createNodeIterator:function(e,t){return O(this),new p(this,e,t)},getNodes:function(e,t){return O(this),f(this,e,t)},getDocument:function(){return n(this)},collapseBefore:function(e){C(this),this.setEndBefore(e),this.collapse(!1)},collapseAfter:function(e){C(this),this.setStartAfter(e),this.collapse(!0)},getName:function(){return"DomRange"},equals:function(e){return H.rangesEqual(this,e)},isValid:function(){return b(this)},inspect:function(){return l(this)}},B(H,k,M),e.rangePrototype=D.prototype,H.rangeProperties=J,H.RangeIterator=h,H.copyComparisonConstants=A,H.createPrototypeRange=B,H.inspect=l,H.getRangeDocument=n,H.rangesEqual=function(e,t){return e.startContainer===t.startContainer&&e.startOffset===t.startOffset&&e.endContainer===t.endContainer&&e.endOffset===t.endOffset},e.DomRange=H,e.RangeException=g}),rangy.createModule("WrappedRange",function(e){function t(e){var t=e.parentElement(),n=e.duplicate();n.collapse(!0);var r=n.parentElement();n=e.duplicate(),n.collapse(!1);var o=n.parentElement(),i=r==o?r:s.getCommonAncestor(r,o);return i==t?i:s.getCommonAncestor(t,i)}function n(e){return 0==e.compareEndPoints("StartToEnd",e)}function r(e,t,n,r){var o=e.duplicate();o.collapse(n);var i=o.parentElement();if(s.isAncestorOf(t,i,!0)||(i=t),!i.canHaveHTML)return new a(i.parentNode,s.getNodeIndex(i));var c,u,d,f,l,h=s.getDocument(i).createElement("span"),g=n?"StartToStart":"StartToEnd";do i.insertBefore(h,h.previousSibling),o.moveToElementText(h);while((c=o.compareEndPoints(g,e))>0&&h.previousSibling);if(l=h.nextSibling,-1==c&&l&&s.isCharacterDataNode(l)){o.setEndPoint(n?"EndToStart":"EndToEnd",e);var p;if(/[\r\n]/.test(l.data)){var m=o.duplicate(),v=m.text.replace(/\r\n/g,"\r").length;for(p=m.moveStart("character",v);-1==(c=m.compareEndPoints("StartToEnd",m));)p++,m.moveStart("character",1)}else p=o.text.length;f=new a(l,p)}else u=(r||!n)&&h.previousSibling,d=(r||n)&&h.nextSibling,f=d&&s.isCharacterDataNode(d)?new a(d,0):u&&s.isCharacterDataNode(u)?new a(u,u.length):new a(i,s.getNodeIndex(h));return h.parentNode.removeChild(h),f}function o(e,t){var n,r,o,i,a=e.offset,c=s.getDocument(e.node),u=c.body.createTextRange(),d=s.isCharacterDataNode(e.node);return d?(n=e.node,r=n.parentNode):(i=e.node.childNodes,n=i.length>a?i[a]:null,r=e.node),o=c.createElement("span"),o.innerHTML="&#feff;",n?r.insertBefore(o,n):r.appendChild(o),u.moveToElementText(o),u.collapse(!t),r.removeChild(o),d&&u[t?"moveStart":"moveEnd"]("character",a),u}e.requireModules(["DomUtil","DomRange"]);var i,s=e.dom,a=s.DomPosition,c=e.DomRange;if(!e.features.implementsDomRange||e.features.implementsTextRange&&e.config.preferTextRange){if(e.features.implementsTextRange){i=function(e){this.textRange=e,this.refresh()},i.prototype=new c(document),i.prototype.refresh=function(){var e,o,i=t(this.textRange);n(this.textRange)?o=e=r(this.textRange,i,!0,!0):(e=r(this.textRange,i,!0,!1),o=r(this.textRange,i,!1,!1)),this.setStart(e.node,e.offset),this.setEnd(o.node,o.offset)},c.copyComparisonConstants(i);var u=function(){return this}();u.Range===void 0&&(u.Range=i),e.createNativeRange=function(e){return e=e||document,e.body.createTextRange()}}}else(function(){function t(e){for(var t,n=d.length;n--;)t=d[n],e[t]=e.nativeRange[t]}function n(e,t,n,r,o){var i=e.startContainer!==t||e.startOffset!=n,s=e.endContainer!==r||e.endOffset!=o;(i||s)&&(e.setEnd(r,o),e.setStart(t,n))}function r(e){e.nativeRange.detach(),e.detached=!0;for(var t,n=d.length;n--;)t=d[n],e[t]=null}var o,a,u,d=c.rangeProperties;i=function(e){if(!e)throw Error("Range must be specified");this.nativeRange=e,t(this)},c.createPrototypeRange(i,n,r),o=i.prototype,o.selectNode=function(e){this.nativeRange.selectNode(e),t(this)},o.deleteContents=function(){this.nativeRange.deleteContents(),t(this)},o.extractContents=function(){var e=this.nativeRange.extractContents();return t(this),e},o.cloneContents=function(){return this.nativeRange.cloneContents()},o.surroundContents=function(e){this.nativeRange.surroundContents(e),t(this)},o.collapse=function(e){this.nativeRange.collapse(e),t(this)},o.cloneRange=function(){return new i(this.nativeRange.cloneRange())},o.refresh=function(){t(this)},o.toString=function(){return""+this.nativeRange};var f=document.createTextNode("test");s.getBody(document).appendChild(f);var l=document.createRange();l.setStart(f,0),l.setEnd(f,0);try{l.setStart(f,1),a=!0,o.setStart=function(e,n){this.nativeRange.setStart(e,n),t(this)},o.setEnd=function(e,n){this.nativeRange.setEnd(e,n),t(this)},u=function(e){return function(n){this.nativeRange[e](n),t(this)}}}catch(h){a=!1,o.setStart=function(e,n){try{this.nativeRange.setStart(e,n)}catch(r){this.nativeRange.setEnd(e,n),this.nativeRange.setStart(e,n)}t(this)},o.setEnd=function(e,n){try{this.nativeRange.setEnd(e,n)}catch(r){this.nativeRange.setStart(e,n),this.nativeRange.setEnd(e,n)}t(this)},u=function(e,n){return function(r){try{this.nativeRange[e](r)}catch(o){this.nativeRange[n](r),this.nativeRange[e](r)}t(this)}}}o.setStartBefore=u("setStartBefore","setEndBefore"),o.setStartAfter=u("setStartAfter","setEndAfter"),o.setEndBefore=u("setEndBefore","setStartBefore"),o.setEndAfter=u("setEndAfter","setStartAfter"),l.selectNodeContents(f),o.selectNodeContents=l.startContainer==f&&l.endContainer==f&&0==l.startOffset&&l.endOffset==f.length?function(e){this.nativeRange.selectNodeContents(e),t(this)}:function(e){this.setStart(e,0),this.setEnd(e,c.getEndOffset(e))},l.selectNodeContents(f),l.setEnd(f,3);var g=document.createRange();g.selectNodeContents(f),g.setEnd(f,4),g.setStart(f,2),o.compareBoundaryPoints=-1==l.compareBoundaryPoints(l.START_TO_END,g)&1==l.compareBoundaryPoints(l.END_TO_START,g)?function(e,t){return t=t.nativeRange||t,e==t.START_TO_END?e=t.END_TO_START:e==t.END_TO_START&&(e=t.START_TO_END),this.nativeRange.compareBoundaryPoints(e,t)}:function(e,t){return this.nativeRange.compareBoundaryPoints(e,t.nativeRange||t)},e.util.isHostMethod(l,"createContextualFragment")&&(o.createContextualFragment=function(e){return this.nativeRange.createContextualFragment(e)}),s.getBody(document).removeChild(f),l.detach(),g.detach()})(),e.createNativeRange=function(e){return e=e||document,e.createRange()};e.features.implementsTextRange&&(i.rangeToTextRange=function(e){if(e.collapsed){var t=o(new a(e.startContainer,e.startOffset),!0);return t}var n=o(new a(e.startContainer,e.startOffset),!0),r=o(new a(e.endContainer,e.endOffset),!1),i=s.getDocument(e.startContainer).body.createTextRange();return i.setEndPoint("StartToStart",n),i.setEndPoint("EndToEnd",r),i}),i.prototype.getName=function(){return"WrappedRange"},e.WrappedRange=i,e.createRange=function(t){return t=t||document,new i(e.createNativeRange(t))},e.createRangyRange=function(e){return e=e||document,new c(e)},e.createIframeRange=function(t){return e.createRange(s.getIframeDocument(t))},e.createIframeRangyRange=function(t){return e.createRangyRange(s.getIframeDocument(t))},e.addCreateMissingNativeApiListener(function(t){var n=t.document;n.createRange===void 0&&(n.createRange=function(){return e.createRange(this)}),n=t=null})}),rangy.createModule("WrappedSelection",function(e,t){function n(e){return(e||window).getSelection()}function r(e){return(e||window).document.selection}function o(e,t,n){var r=n?"end":"start",o=n?"start":"end";e.anchorNode=t[r+"Container"],e.anchorOffset=t[r+"Offset"],e.focusNode=t[o+"Container"],e.focusOffset=t[o+"Offset"]}function i(e){var t=e.nativeSelection;e.anchorNode=t.anchorNode,e.anchorOffset=t.anchorOffset,e.focusNode=t.focusNode,e.focusOffset=t.focusOffset}function s(e){e.anchorNode=e.focusNode=null,e.anchorOffset=e.focusOffset=0,e.rangeCount=0,e.isCollapsed=!0,e._ranges.length=0}function a(t){var n;return t instanceof w?(n=t._selectionNativeRange,n||(n=e.createNativeRange(S.getDocument(t.startContainer)),n.setEnd(t.endContainer,t.endOffset),n.setStart(t.startContainer,t.startOffset),t._selectionNativeRange=n,t.attachListener("detach",function(){this._selectionNativeRange=null}))):t instanceof T?n=t.nativeRange:e.features.implementsDomRange&&t instanceof S.getWindow(t.startContainer).Range&&(n=t),n}function c(e){if(!e.length||1!=e[0].nodeType)return!1;for(var t=1,n=e.length;n>t;++t)if(!S.isAncestorOf(e[0],e[t]))return!1;return!0}function u(e){var t=e.getNodes();if(!c(t))throw Error("getSingleElementFromRange: range "+e.inspect()+" did not consist of a single element");return t[0]}function d(e){return!!e&&e.text!==void 0}function f(e,t){var n=new T(t);e._ranges=[n],o(e,n,!1),e.rangeCount=1,e.isCollapsed=n.collapsed}function l(t){if(t._ranges.length=0,"None"==t.docSelection.type)s(t);else{var n=t.docSelection.createRange();if(d(n))f(t,n);else{t.rangeCount=n.length;for(var r,i=S.getDocument(n.item(0)),a=0;t.rangeCount>a;++a)r=e.createRange(i),r.selectNode(n.item(a)),t._ranges.push(r);t.isCollapsed=1==t.rangeCount&&t._ranges[0].collapsed,o(t,t._ranges[t.rangeCount-1],!1) +}}}function h(e,t){for(var n=e.docSelection.createRange(),r=u(t),o=S.getDocument(n.item(0)),i=S.getBody(o).createControlRange(),s=0,a=n.length;a>s;++s)i.add(n.item(s));try{i.add(r)}catch(c){throw Error("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)")}i.select(),l(e)}function g(e,t,n){this.nativeSelection=e,this.docSelection=t,this._ranges=[],this.win=n,this.refresh()}function p(e,t){for(var n,r=S.getDocument(t[0].startContainer),o=S.getBody(r).createControlRange(),i=0;rangeCount>i;++i){n=u(t[i]);try{o.add(n)}catch(s){throw Error("setRanges(): Element within the one of the specified Ranges could not be added to control selection (does it have layout?)")}}o.select(),l(e)}function m(e,t){if(e.anchorNode&&S.getDocument(e.anchorNode)!==S.getDocument(t))throw new b("WRONG_DOCUMENT_ERR")}function v(e){var t=[],n=new O(e.anchorNode,e.anchorOffset),r=new O(e.focusNode,e.focusOffset),o="function"==typeof e.getName?e.getName():"Selection";if(e.rangeCount!==void 0)for(var i=0,s=e.rangeCount;s>i;++i)t[i]=w.inspect(e.getRangeAt(i));return"["+o+"(Ranges: "+t.join(", ")+")(anchor: "+n.inspect()+", focus: "+r.inspect()+"]"}e.requireModules(["DomUtil","DomRange","WrappedRange"]),e.config.checkSelectionRanges=!0;var C,R,N="boolean",y="_rangySelection",S=e.dom,E=e.util,w=e.DomRange,T=e.WrappedRange,b=e.DOMException,O=S.DomPosition,D="Control",_=e.util.isHostMethod(window,"getSelection"),A=e.util.isHostObject(document,"selection"),x=A&&(!_||e.config.preferTextRange);x?(C=r,e.isSelectionValid=function(e){var t=(e||window).document,n=t.selection;return"None"!=n.type||S.getDocument(n.createRange().parentElement())==t}):_?(C=n,e.isSelectionValid=function(){return!0}):t.fail("Neither document.selection or window.getSelection() detected."),e.getNativeSelection=C;var B=C(),I=e.createNativeRange(document),k=S.getBody(document),M=E.areHostObjects(B,["anchorNode","focusNode"]&&E.areHostProperties(B,["anchorOffset","focusOffset"]));e.features.selectionHasAnchorAndFocus=M;var H=E.isHostMethod(B,"extend");e.features.selectionHasExtend=H;var P="number"==typeof B.rangeCount;e.features.selectionHasRangeCount=P;var L=!1,W=!0;E.areHostMethods(B,["addRange","getRangeAt","removeAllRanges"])&&"number"==typeof B.rangeCount&&e.features.implementsDomRange&&function(){var e=document.createElement("iframe");e.frameBorder=0,e.style.position="absolute",e.style.left="-10000px",k.appendChild(e);var t=S.getIframeDocument(e);t.open(),t.write("12"),t.close();var n=S.getIframeWindow(e).getSelection(),r=t.documentElement,o=r.lastChild,i=o.firstChild,s=t.createRange();s.setStart(i,1),s.collapse(!0),n.addRange(s),W=1==n.rangeCount,n.removeAllRanges();var a=s.cloneRange();s.setStart(i,0),a.setEnd(i,2),n.addRange(s),n.addRange(a),L=2==n.rangeCount,s.detach(),a.detach(),k.removeChild(e)}(),e.features.selectionSupportsMultipleRanges=L,e.features.collapsedNonEditableSelectionsSupported=W;var F,z=!1;k&&E.isHostMethod(k,"createControlRange")&&(F=k.createControlRange(),E.areHostProperties(F,["item","add"])&&(z=!0)),e.features.implementsControlRange=z,R=M?function(e){return e.anchorNode===e.focusNode&&e.anchorOffset===e.focusOffset}:function(e){return e.rangeCount?e.getRangeAt(e.rangeCount-1).collapsed:!1};var U;E.isHostMethod(B,"getRangeAt")?U=function(e,t){try{return e.getRangeAt(t)}catch(n){return null}}:M&&(U=function(t){var n=S.getDocument(t.anchorNode),r=e.createRange(n);return r.setStart(t.anchorNode,t.anchorOffset),r.setEnd(t.focusNode,t.focusOffset),r.collapsed!==this.isCollapsed&&(r.setStart(t.focusNode,t.focusOffset),r.setEnd(t.anchorNode,t.anchorOffset)),r}),e.getSelection=function(e){e=e||window;var t=e[y],n=C(e),o=A?r(e):null;return t?(t.nativeSelection=n,t.docSelection=o,t.refresh(e)):(t=new g(n,o,e),e[y]=t),t},e.getIframeSelection=function(t){return e.getSelection(S.getIframeWindow(t))};var V=g.prototype;if(!x&&M&&E.areHostMethods(B,["removeAllRanges","addRange"])){V.removeAllRanges=function(){this.nativeSelection.removeAllRanges(),s(this)};var j=function(t,n){var r=w.getRangeDocument(n),o=e.createRange(r);o.collapseToPoint(n.endContainer,n.endOffset),t.nativeSelection.addRange(a(o)),t.nativeSelection.extend(n.startContainer,n.startOffset),t.refresh()};V.addRange=P?function(t,n){if(z&&A&&this.docSelection.type==D)h(this,t);else if(n&&H)j(this,t);else{var r;if(L?r=this.rangeCount:(this.removeAllRanges(),r=0),this.nativeSelection.addRange(a(t)),this.rangeCount=this.nativeSelection.rangeCount,this.rangeCount==r+1){if(e.config.checkSelectionRanges){var i=U(this.nativeSelection,this.rangeCount-1);i&&!w.rangesEqual(i,t)&&(t=new T(i))}this._ranges[this.rangeCount-1]=t,o(this,t,K(this.nativeSelection)),this.isCollapsed=R(this)}else this.refresh()}}:function(e,t){t&&H?j(this,e):(this.nativeSelection.addRange(a(e)),this.refresh())},V.setRanges=function(e){if(z&&e.length>1)p(this,e);else{this.removeAllRanges();for(var t=0,n=e.length;n>t;++t)this.addRange(e[t])}}}else{if(!(E.isHostMethod(B,"empty")&&E.isHostMethod(I,"select")&&z&&x))return t.fail("No means of selecting a Range or TextRange was found"),!1;V.removeAllRanges=function(){try{if(this.docSelection.empty(),"None"!=this.docSelection.type){var e;if(this.anchorNode)e=S.getDocument(this.anchorNode);else if(this.docSelection.type==D){var t=this.docSelection.createRange();t.length&&(e=S.getDocument(t.item(0)).body.createTextRange())}if(e){var n=e.body.createTextRange();n.select(),this.docSelection.empty()}}}catch(r){}s(this)},V.addRange=function(e){this.docSelection.type==D?h(this,e):(T.rangeToTextRange(e).select(),this._ranges[0]=e,this.rangeCount=1,this.isCollapsed=this._ranges[0].collapsed,o(this,e,!1))},V.setRanges=function(e){this.removeAllRanges();var t=e.length;t>1?p(this,e):t&&this.addRange(e[0])}}V.getRangeAt=function(e){if(0>e||e>=this.rangeCount)throw new b("INDEX_SIZE_ERR");return this._ranges[e]};var q;if(x)q=function(t){var n;e.isSelectionValid(t.win)?n=t.docSelection.createRange():(n=S.getBody(t.win.document).createTextRange(),n.collapse(!0)),t.docSelection.type==D?l(t):d(n)?f(t,n):s(t)};else if(E.isHostMethod(B,"getRangeAt")&&"number"==typeof B.rangeCount)q=function(t){if(z&&A&&t.docSelection.type==D)l(t);else if(t._ranges.length=t.rangeCount=t.nativeSelection.rangeCount,t.rangeCount){for(var n=0,r=t.rangeCount;r>n;++n)t._ranges[n]=new e.WrappedRange(t.nativeSelection.getRangeAt(n));o(t,t._ranges[t.rangeCount-1],K(t.nativeSelection)),t.isCollapsed=R(t)}else s(t)};else{if(!M||typeof B.isCollapsed!=N||typeof I.collapsed!=N||!e.features.implementsDomRange)return t.fail("No means of obtaining a Range or TextRange from the user's selection was found"),!1;q=function(e){var t,n=e.nativeSelection;n.anchorNode?(t=U(n,0),e._ranges=[t],e.rangeCount=1,i(e),e.isCollapsed=R(e)):s(e)}}V.refresh=function(e){var t=e?this._ranges.slice(0):null;if(q(this),e){var n=t.length;if(n!=this._ranges.length)return!1;for(;n--;)if(!w.rangesEqual(t[n],this._ranges[n]))return!1;return!0}};var Y=function(e,t){var n=e.getAllRanges(),r=!1;e.removeAllRanges();for(var o=0,i=n.length;i>o;++o)r||t!==n[o]?e.addRange(n[o]):r=!0;e.rangeCount||s(e)};V.removeRange=z?function(e){if(this.docSelection.type==D){for(var t,n=this.docSelection.createRange(),r=u(e),o=S.getDocument(n.item(0)),i=S.getBody(o).createControlRange(),s=!1,a=0,c=n.length;c>a;++a)t=n.item(a),t!==r||s?i.add(n.item(a)):s=!0;i.select(),l(this)}else Y(this,e)}:function(e){Y(this,e)};var K;!x&&M&&e.features.implementsDomRange?(K=function(e){var t=!1;return e.anchorNode&&(t=1==S.comparePoints(e.anchorNode,e.anchorOffset,e.focusNode,e.focusOffset)),t},V.isBackwards=function(){return K(this)}):K=V.isBackwards=function(){return!1},V.toString=function(){for(var e=[],t=0,n=this.rangeCount;n>t;++t)e[t]=""+this._ranges[t];return e.join("")},V.collapse=function(t,n){m(this,t);var r=e.createRange(S.getDocument(t));r.collapseToPoint(t,n),this.removeAllRanges(),this.addRange(r),this.isCollapsed=!0},V.collapseToStart=function(){if(!this.rangeCount)throw new b("INVALID_STATE_ERR");var e=this._ranges[0];this.collapse(e.startContainer,e.startOffset)},V.collapseToEnd=function(){if(!this.rangeCount)throw new b("INVALID_STATE_ERR");var e=this._ranges[this.rangeCount-1];this.collapse(e.endContainer,e.endOffset)},V.selectAllChildren=function(t){m(this,t);var n=e.createRange(S.getDocument(t));n.selectNodeContents(t),this.removeAllRanges(),this.addRange(n)},V.deleteFromDocument=function(){if(z&&A&&this.docSelection.type==D){for(var e,t=this.docSelection.createRange();t.length;)e=t.item(0),t.remove(e),e.parentNode.removeChild(e);this.refresh()}else if(this.rangeCount){var n=this.getAllRanges();this.removeAllRanges();for(var r=0,o=n.length;o>r;++r)n[r].deleteContents();this.addRange(n[o-1])}},V.getAllRanges=function(){return this._ranges.slice(0)},V.setSingleRange=function(e){this.setRanges([e])},V.containsNode=function(e,t){for(var n=0,r=this._ranges.length;r>n;++n)if(this._ranges[n].containsNode(e,t))return!0;return!1},V.toHtml=function(){var e="";if(this.rangeCount){for(var t=w.getRangeDocument(this._ranges[0]).createElement("div"),n=0,r=this._ranges.length;r>n;++n)t.appendChild(this._ranges[n].cloneContents());e=t.innerHTML}return e},V.getName=function(){return"WrappedSelection"},V.inspect=function(){return v(this)},V.detach=function(){this.win[y]=null,this.win=this.anchorNode=this.focusNode=null},g.inspect=v,e.Selection=g,e.selectionPrototype=V,e.addCreateMissingNativeApiListener(function(t){t.getSelection===void 0&&(t.getSelection=function(){return e.getSelection(this)}),t=null})}),rangy.createModule("SaveRestore",function(e,t){function n(e,t){return(t||document).getElementById(e)}function r(e,t){var n,r="selectionBoundary_"+ +new Date+"_"+(""+Math.random()).slice(2),o=d.getDocument(e.startContainer),i=e.cloneRange();return i.collapse(t),n=o.createElement("span"),n.id=r,n.style.lineHeight="0",n.style.display="none",n.className="rangySelectionBoundary",n.appendChild(o.createTextNode(f)),i.insertNode(n),i.detach(),n}function o(e,r,o,i){var s=n(o,e);s?(r[i?"setStartBefore":"setEndBefore"](s),s.parentNode.removeChild(s)):t.warn("Marker element has been removed. Cannot restore selection.")}function i(e,t){return t.compareBoundaryPoints(e.START_TO_START,e)}function s(o){o=o||window;var s=o.document;if(!e.isSelectionValid(o))return t.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus."),void 0;var a,c,u,d=e.getSelection(o),f=d.getAllRanges(),l=[];f.sort(i);for(var h=0,g=f.length;g>h;++h)u=f[h],u.collapsed?(c=r(u,!1),l.push({markerId:c.id,collapsed:!0})):(c=r(u,!1),a=r(u,!0),l[h]={startMarkerId:a.id,endMarkerId:c.id,collapsed:!1,backwards:1==f.length&&d.isBackwards()});for(h=g-1;h>=0;--h)u=f[h],u.collapsed?u.collapseBefore(n(l[h].markerId,s)):(u.setEndBefore(n(l[h].endMarkerId,s)),u.setStartAfter(n(l[h].startMarkerId,s)));return d.setRanges(f),{win:o,doc:s,rangeInfos:l,restored:!1}}function a(r,i){if(!r.restored){for(var s,a,c=r.rangeInfos,u=e.getSelection(r.win),d=[],f=c.length,l=f-1;l>=0;--l){if(s=c[l],a=e.createRange(r.doc),s.collapsed){var h=n(s.markerId,r.doc);if(h){h.style.display="inline";var g=h.previousSibling;g&&3==g.nodeType?(h.parentNode.removeChild(h),a.collapseToPoint(g,g.length)):(a.collapseBefore(h),h.parentNode.removeChild(h))}else t.warn("Marker element has been removed. Cannot restore selection.")}else o(r.doc,a,s.startMarkerId,!0),o(r.doc,a,s.endMarkerId,!1);1==f&&a.normalizeBoundaries(),d[l]=a}1==f&&i&&e.features.selectionHasExtend&&c[0].backwards?(u.removeAllRanges(),u.addRange(d[0],!0)):u.setRanges(d),r.restored=!0}}function c(e,t){var r=n(t,e);r&&r.parentNode.removeChild(r)}function u(e){for(var t,n=e.rangeInfos,r=0,o=n.length;o>r;++r)t=n[r],t.collapsed?c(e.doc,t.markerId):(c(e.doc,t.startMarkerId),c(e.doc,t.endMarkerId))}e.requireModules(["DomUtil","DomRange","WrappedRange"]);var d=e.dom,f="";e.saveSelection=s,e.restoreSelection=a,e.removeMarkerElement=c,e.removeMarkers=u}),!function(e,t){"function"==typeof define?define(t):"undefined"!=typeof module&&module.exports?module.exports.browser=t():this[e]=t()}("bowser",function(){function e(){return o?{msie:r,version:n.match(/msie (\d+(\.\d+)?);/i)[1]}:i?{webkit:r,chrome:r,version:n.match(/chrome\/(\d+(\.\d+)?)/i)[1]}:s?{webkit:r,phantom:r,version:n.match(/phantomjs\/(\d+(\.\d+)+)/i)[1]}:d?{webkit:r,touchpad:r,version:n.match(/touchpad\/(\d+(\.\d+)?)/i)[1]}:c||u?(t={webkit:r,mobile:r,ios:r,iphone:c,ipad:u},m.test(n)&&(t.version=n.match(m)[1]),t):f?{webkit:r,android:r,mobile:r,version:n.match(m)[1]}:a?{webkit:r,safari:r,version:n.match(m)[1]}:l?{opera:r,version:n.match(m)[1]}:g?(t={gecko:r,mozilla:r,version:n.match(/firefox\/(\d+(\.\d+)?)/i)[1]},h&&(t.firefox=r),t):p?{seamonkey:r,version:n.match(/seamonkey\/(\d+(\.\d+)?)/i)[1]}:void 0}var t,n=navigator.userAgent,r=!0,o=/msie/i.test(n),i=/chrome/i.test(n),s=/phantom/i.test(n),a=/safari/i.test(n)&&!i&&!s,c=/iphone/i.test(n),u=/ipad/i.test(n),d=/touchpad/i.test(n),f=/android/i.test(n),l=/opera/i.test(n),h=/firefox/i.test(n),g=/gecko\//i.test(n),p=/seamonkey\//i.test(n),m=/version\/(\d+(\.\d+)?)/i,v=e();return v.msie&&v.version>=7||v.chrome&&v.version>=10||v.firefox&&v.version>=4||v.safari&&v.version>=5||v.opera&&v.version>=10?v.a=r:v.msie&&7>v.version||v.chrome&&10>v.version||v.firefox&&4>v.version||v.safari&&5>v.version||v.opera&&10>v.version?v.c=r:v.x=r,v}),function(e,t,n,r){"use strict";var o,i,s={},a=function(){return n||function(){throw Error("jQuery-like library not yet implemented")}}();o=function(){if(f.log!==!1){var t,n;return t=Array.prototype.slice.call(arguments),t.length&&"trace"===t[t.length-1]&&(t.pop(),((n=e.console)?n.trace:void 0)&&console.trace()),1===t.length&&(t=t[0]),e.console?console.log(t):r}},i=function(){if(f.logErrors!==!1){var t;return t=Array.prototype.slice.call(arguments),1===t.length&&(t=t[0]),e.console&&"function"==typeof e.console.error?console.error(t):e.console?console.log(t):r}};var c=function(){return{trimRight:function(e){return e.replace(/\s+$/,"")},trimLeft:function(e){return e.replace(/^\s+/,"")},trim:function(e){return e.replace(/^\s+|\s+$/g,"")},isString:function(e){return"[object String]"===toString.call(e)},regexp:function(e,t){t||(t="g");var n=e.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&");return RegExp(n,t)}}}();(function(){var e,t=!1,n=function(){t||(t=!0,e="."+f.editableClass,rangy.initialized||rangy.init(),g.setup())};s={init:function(e){return t?(i("Editable is already initialized"),r):(a.extend(!0,f,e),n(),r)},add:function(e,t){return n(),a.extend(!0,{},f,t),this.enable(a(e)),this},remove:function(e){var t=a(e);return this.disable(t),t.removeClass(f.editableDisabledClass),this},disable:function(e){return e=e||a("."+f.editableClass),e.removeAttr("contenteditable").removeClass(f.editableClass).addClass(f.editableDisabledClass),this},enable:function(e){return e=e||a("."+f.editableDisabledClass),e.attr("contenteditable",!0).removeClass(f.editableDisabledClass).addClass(f.editableClass),e.each(function(e,t){l.normalizeTags(t),l.normalizeSpaces(t)}),this},createCursor:function(t,n){var r,o=a(t).closest(e);if(n=n||"beginning",o.length){var s=rangy.createRange();"beginning"===n||"end"===n?(s.selectNodeContents(t),s.collapse("beginning"===n?!0:!1)):t!==o[0]?"before"===n?(s.setStartBefore(t),s.setEndBefore(t)):"after"===n&&(s.setStartAfter(t),s.setEndAfter(t)):i("EditableJS: cannot create cursor outside of an editable block."),r=new h(o[0],s)}return r},createCursorAtBeginning:function(e){this.createCursor(e,"beginning")},createCursorAtEnd:function(e){this.createCursor(e,"end")},createCursorBefore:function(e){this.createCursor(e,"before")},createCursorAfter:function(e){this.createCursor(e,"after")},on:function(e,t){return n(),g.addListener(e,t),this},off:function(e,t){return g.removeListener(e,t),this},focus:function(e){return this.on("focus",e)},blur:function(e){return this.on("blur",e)},flow:function(e){return this.on("flow",e)},selection:function(e){return this.on("selection",e)},cursor:function(e){return this.on("cursor",e)},newline:function(e){return this.on("newline",e)},insert:function(e){return this.on("insert",e)},split:function(e){return this.on("split",e)},merge:function(e){return this.on("merge",e)},empty:function(e){return this.on("empty",e)},"switch":function(e){return this.on("switch",e)},move:function(e){return this.on("move",e)},clipboard:function(e){return this.on("clipboard",e)}}})();var u=function(){return{focus:function(){o("Default focus behavior")},blur:function(e){o("Default blur behavior"),l.cleanInternals(e)},flow:function(){o("Default flow behavior")},selection:function(e,t){t?o("Default selection behavior"):o("Default selection empty behavior")},cursor:function(e,t){t?o("Default cursor behavior"):o("Default cursor empty behavior")},newline:function(e,n){o(n),o("Default newline behavior");var r=n.isAtEnd(),i=t.createElement("br");if(n.insertBefore(i),r){o("at the end");var s=t.createTextNode("​");n.insertAfter(s)}else o("not at the end");n.setSelection()},insert:function(e,t){o("Default insert "+t+" behavior");var n=e.parentNode,r=e.cloneNode(!1);switch(r.id&&r.removeAttribute("id"),t){case"before":n.insertBefore(r,e),e.focus();break;case"after":n.insertBefore(r,e.nextSibling),r.focus()}},split:function(e,t,n){var r=e.parentNode,o=n.firstChild;r.insertBefore(t,e),r.replaceChild(n,e),l.normalizeTags(o),l.normalizeSpaces(o),o.focus()},merge:function(e,n,r){o("Default merge "+n+" behavior");var i,s,a,c,u,f;switch(n){case"before":i=d.previous(e),s=e;break;case"after":i=e,s=d.next(e)}if(i&&s){for(i.childNodes.length>0?r.moveAtTextEnd(i):r.moveAtBeginning(i),r.setSelection(),a=t.createDocumentFragment(),c=s.childNodes,u=0;c.length>u;u++)a.appendChild(c[u].cloneNode(!0));f=i.appendChild(a),s.parentNode.removeChild(s),r.save(),l.normalizeTags(i),l.normalizeSpaces(i),r.restore(),r.setSelection()}},empty:function(){o("Default empty behavior")},"switch":function(e,t,n){o("Default switch behavior");var r,i;switch(t){case"before":i=d.previous(e),i&&(n.moveAtTextEnd(i),n.setSelection());break;case"after":r=d.next(e),r&&(n.moveAtBeginning(r),n.setSelection())}},move:function(){o("Default move behavior")},clipboard:function(e,n,r){o("Default clipboard behavior");var i,s;"paste"===n&&(e.setAttribute(f.pastingAttribute,!0),r.isSelection&&(r=r.deleteContent()),i=t.createElement("textarea"),i.setAttribute("style","position: absolute; left: -9999px"),r.insertAfter(i),s=rangy.saveSelection(),i.focus(),setTimeout(function(){var n,r,o;n=i.value,e.removeChild(i),rangy.restoreSelection(s),o=N.forceCursor(),r=t.createTextNode(n),l.normalizeSpaces(r),o.insertAfter(r),o.moveAfter(r),o.setSelection(),e.removeAttribute(f.pastingAttribute)},0))}}}(),d=function(){return{next:function(e){var t=e.nextElementSibling;return t&&t.getAttribute("contenteditable")?t:null},previous:function(e){var t=e.previousElementSibling;return t&&t.getAttribute("contenteditable")?t:null}}}(),f={log:!1,logErrors:!0,editableClass:"js-editable",editableDisabledClass:"js-editable-disabled",pastingAttribute:"data-editable-is-pasting",mouseMoveSelectionChanges:!1,boldTag:"",italicTag:"",event:{focus:function(e){u.focus(e)},blur:function(e){u.blur(e)},flow:function(e,t){u.flow(e,t)},selection:function(e,t){u.selection(e,t)},cursor:function(e,t){u.cursor(e,t)},newline:function(e,t){u.newline(e,t)},split:function(e,t,n,r){u.split(e,t,n,r)},insert:function(e,t,n){u.insert(e,t,n)},merge:function(e,t,n){u.merge(e,t,n)},empty:function(e){u.empty(e)},"switch":function(e,t,n){u.switch(e,t,n)},move:function(e,t,n){u.move(e,t,n)},clipboard:function(e,t,n){u.clipboard(e,t,n)}}},l=function(){var e=function(e,t,n){return t=R.save(t),n.call(l),R.restore(e,t)};return{normalizeTags:function(e){var n,r,o,i,s=t.createDocumentFragment();for(n=0;e.childNodes.length>n;n++)if(o=e.childNodes[n],o&&("BR"===o.nodeName||o.textContent)){if(1===o.nodeType&&"BR"!==o.nodeName){for(i=o;null!==(i=i.nextSibling)&&v.isSameNode(i,o);){for(r=0;i.childNodes.length>r;r++)o.appendChild(i.childNodes[r].cloneNode(!0));i.parentNode.removeChild(i)}this.normalizeTags(o)}s.appendChild(o.cloneNode(!0))}for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(s)},cleanInternals:function(e){e.innerHTML=e.innerHTML.replace(/\u200B/g,"
")},normalizeSpaces:function(e){var t=" ";e&&(3===e.nodeType?e.nodeValue=e.nodeValue.replace(/^(\s)/,t).replace(/(\s)$/,t):(this.normalizeSpaces(e.firstChild),this.normalizeSpaces(e.lastChild)))},getTags:function(e,t,n){for(var r=this.getInnerTags(t,n),o=t.commonAncestorContainer;o!==e;)(!n||n(o))&&r.push(o),o=o.parentNode;return r},getTagsByName:function(e,t,n){return this.getTags(e,t,function(e){return e.nodeName===n.toUpperCase()})},getInnerTags:function(e,t){return e.getNodes([1],t)},getTagNames:function(e){var t=[];if(!e)return t;for(var n=0;e.length>n;n++)t.push(e[n].nodeName);return t},isAffectedBy:function(e,t,n){for(var r,o=this.getTags(e,t),i=0;o.length>i;i++)if(r=o[i],r.nodeName===n.toUpperCase())return!0;return!1},isExactSelection:function(e,t,n){var r=rangy.createRange();if(r.selectNodeContents(t),e.intersectsRange(r)){var o=""+e,i=a(t).text();return n&&(o=c.trim(o),i=c.trim(i)),""!==o&&o===i}return!1},expandTo:function(e,t,n){return t.selectNodeContents(n),t},toggleTag:function(e,t,n){var r=this.getTagsByName(e,t,n.nodeName);return 1===r.length&&this.isExactSelection(t,r[0],"visible")?this.removeFormatting(e,t,n.nodeName):this.forceWrap(e,t,n)},isWrappable:function(e){return e.canSurroundContents()},forceWrap:function(t,n,r){return n=e(t,n,function(){this.nuke(t,n,r.nodeName)}),this.isWrappable(n)||(n=e(t,n,function(){this.nuke(t,n)})),this.wrap(n,r),n},wrap:function(e,t){t=c.isString(t)?a(t)[0]:t,this.isWrappable(e)?e.surroundContents(t):console.log("content.wrap(): can not surround range")},unwrap:function(e){a(e).contents().unwrap()},removeFormatting:function(t,n,r){return e(t,n,function(){this.nuke(t,n,r)})},nuke:function(e,t,n){for(var r=this.getTags(e,t),o=0;r.length>o;o++){var i=r[o];n&&i.nodeName!==n.toUpperCase()||this.unwrap(i)}},insertCharacter:function(e,n,r){var o=t.createTextNode(n),i=e.cloneRange();i.collapse(r),i.insertNode(o),i.detach(),r?e.setStartBefore(o):e.setEndAfter(o),e.normalizeBoundaries()},surround:function(e,t,n,r){return r||(r=n),this.insertCharacter(t,r,!1),this.insertCharacter(t,n,!0),t},deleteCharacter:function(t,n,r){return this.containsString(n,r)&&(n.splitBoundaries(),n=e(t,n,function(){for(var e=c.regexp(r),t=n.getNodes([3],function(t){return t.nodeValue.search(e)>=0}),o=0;t.length>o;o++){var i=t[o];i.nodeValue=i.nodeValue.replace(e,"")}}),n.normalizeBoundaries()),n},containsString:function(e,t){var n=""+e;return n.indexOf(t)>=0},nukeTag:function(e,t,n){for(var r=this.getTags(e,t),o=0;r.length>o;o++){var i=r[o];i.nodeName===n&&this.unwrap(i)}}}}(),h=function(){var n=function(e,t){this.host=e,this.range=t,this.isCursor=!0};return n.prototype=function(){return{isAtEnd:function(){return v.isEndOfHost(this.host,this.range.endContainer,this.range.endOffset)},isAtTextEnd:function(){return v.isTextEndOfHost(this.host,this.range.endContainer,this.range.endOffset)},isAtBeginning:function(){return v.isBeginningOfHost(this.host,this.range.startContainer,this.range.startOffset)},insertBefore:function(e){if(!v.isDocumentFragmentWithoutChildren(e)){var t=e;if(11===e.nodeType){var n=e.childNodes.length-1;t=e.childNodes[n]}this.range.insertNode(e),this.range.setStartAfter(t),this.range.setEndAfter(t)}},insertAfter:function(e){v.isDocumentFragmentWithoutChildren(e)||this.range.insertNode(e)},setSelection:function(){rangy.getSelection().setSingleRange(this.range)},before:function(){var e=null,t=this.range.cloneRange();return t.setStartBefore(this.host),e=t.cloneContents(),t.detach(),e},after:function(){var e=null,t=this.range.cloneRange();return t.setEndAfter(this.host),e=t.cloneContents(),t.detach(),e},getCoordinates:function(){var n=this.range.nativeRange.getBoundingClientRect(),o=e.pageXOffset!==r?e.pageXOffset:(t.documentElement||t.body.parentNode||t.body).scrollLeft,i=e.pageYOffset!==r?e.pageYOffset:(t.documentElement||t.body.parentNode||t.body).scrollTop;return{top:n.top+i,bottom:n.bottom+i,left:n.left+o,right:n.right+o,height:n.height,width:n.width}},detach:function(){this.range.detach()},moveBefore:function(e){return this.setHost(e),this.range.setStartBefore(e),this.range.setEndBefore(e),this.isSelection?new n(this.host,this.range):r},moveAfter:function(e){return this.setHost(e),this.range.setStartAfter(e),this.range.setEndAfter(e),this.isSelection?new n(this.host,this.range):r},moveAtBeginning:function(e){return e||(e=this.host),this.setHost(e),this.range.selectNodeContents(e),this.range.collapse(!0),this.isSelection?new n(this.host,this.range):r},moveAtEnd:function(e){return e||(e=this.host),this.setHost(e),this.range.selectNodeContents(e),this.range.collapse(!1),this.isSelection?new n(this.host,this.range):r},moveAtTextEnd:function(e){return this.moveAtEnd(v.latestChild(e))},setHost:function(e){this.host=v.getHost(e),this.host||i("Can not set cursor outside of an editable block")},save:function(){this.savedRangeInfo=R.save(this.range),this.savedRangeInfo.host=this.host},restore:function(){this.savedRangeInfo?(this.host=this.savedRangeInfo.host,this.range=R.restore(this.host,this.savedRangeInfo),this.savedRangeInfo=r):i("Could not restore selection")},equals:function(e){return e?e.host?e.host.isEqualNode(this.host)?e.range?e.range.equals(this.range)?!0:!1:!1:!1:!1:!1}}}(),n}(),g=function(){var e,n={},i=function(t,n){t.on("focus.editable",e,function(){this.getAttribute(f.pastingAttribute)||n("focus",this)}).on("blur.editable",e,function(){this.getAttribute(f.pastingAttribute)||n("blur",this)}).on("copy.editable",e,function(){o("Copy"),n("clipboard",this,"copy",N.getFreshSelection())}).on("cut.editable",e,function(){o("Cut"),n("clipboard",this,"cut",N.getFreshSelection())}).on("paste.editable",e,function(){o("Paste"),n("clipboard",this,"paste",N.getFreshSelection())})},c=function(t,n){var r=function(e,t,r){var o;e.altKey||e.ctrlKey||e.metaKey||e.shiftKey||(o=N.getSelection(),o&&!o.isSelection&&setTimeout(function(){var i=N.forceCursor();i.equals(o)&&(e.preventDefault(),e.stopPropagation(),n("switch",t,r,i))},1))};t.on("keydown.editable",e,function(e){m.dispatchKeyEvent(e,this)}),m.on("left",function(e){o("Left key pressed"),r(e,this,"before")}).on("up",function(e){o("Up key pressed"),r(e,this,"before")}).on("right",function(e){o("Right key pressed"),r(e,this,"after")}).on("down",function(e){o("Down key pressed"),r(e,this,"after")}).on("tab",function(){o("Tab key pressed")}).on("shiftTab",function(){o("Shift+Tab key pressed")}).on("esc",function(){o("Esc key pressed")}).on("backspace",function(e){o("Backspace key pressed");var t=N.getFreshRange();if(t.isCursor){var r=t.getCursor();r.isAtBeginning()&&(e.preventDefault(),e.stopPropagation(),n("merge",this,"before",r))}}).on("delete",function(e){o("Delete key pressed");var t=N.getFreshRange();if(t.isCursor){var r=t.getCursor();r.isAtTextEnd()&&(e.preventDefault(),e.stopPropagation(),n("merge",this,"after",r))}}).on("enter",function(e){o("Enter key pressed"),e.preventDefault(),e.stopPropagation();var t=N.getFreshRange(),r=t.forceCursor();r.isAtTextEnd()?n("insert",this,"after",r):r.isAtBeginning()?n("insert",this,"before",r):n("split",this,r.before(),r.after(),r)}).on("shiftEnter",function(e){o("Shift+Enter key pressed"),e.preventDefault(),e.stopPropagation();var t=N.forceCursor();n("newline",this,t)})},u=function(t,n){var r=!1,o=!1;t.on("selectionchange.editable",function(){o?r=!0:n()}),t.on("mousedown.editable",e,function(){f.mouseMoveSelectionChanges===!1&&(o=!0,setTimeout(n,0)),t.on("mouseup.editableSelection",function(){t.off(".editableSelection"),o=!1,r&&(r=!1,n())})})},d=function(t,n){t.on("mouseup.editableSelection",function(){setTimeout(n,0)}),t.on("keyup.editable",e,function(){n()})};return{addListener:function(e,t){n[e]===r&&(n[e]=[]),n[e].unshift(t)},removeListener:function(e,t){var o=n[e];if(o!==r)for(var i=0,s=o.length;s>i;i++)if(o[i]===t){o.splice(i,1);break}},notifyListeners:function(e){var t=n[e];if(t!==r)for(var o=0,i=t.length;i>o&&t[o].apply(s,Array.prototype.slice.call(arguments).splice(1))!==!1;o++);},setup:function(){var r=a(t),o=null;e="."+f.editableClass,n={};for(o in f.event)this.addListener(o,f.event[o]);i(r,this.notifyListeners),c(r,this.notifyListeners);var s=N.selectionChanged;p.selectionchange?u(r,s):d(r,s)}}}(),p=function(){var e=t.documentElement.contentEditable!==r,n=function(){return!(bowser.gecko||bowser.opera)}();return{contenteditable:e,selectionchange:n}}();n.fn.editable=function(e){return e===r||e?s.add(this):s.remove(this),this};var m=function(){var e={left:37,up:38,right:39,down:40,tab:9,esc:27,backspace:8,"delete":46,enter:13},t={},n=function(e,n){t[e]===r&&(t[e]=[]),t[e].push(n)},o=function(e,n){var o=t[e];if(o!==r)for(var i=0,s=o.length;s>i&&o[i].apply(n,Array.prototype.slice.call(arguments).splice(2))!==!1;i++);};return{dispatchKeyEvent:function(t,n){switch(t.keyCode){case e.left:o("left",n,t);break;case e.right:o("right",n,t);break;case e.up:o("up",n,t);break;case e.down:o("down",n,t);break;case e.tab:t.shiftKey?o("shiftTab",n,t):o("tab",n,t);break;case e.esc:o("esc",n,t);break;case e.backspace:o("backspace",n,t);break;case e["delete"]:o("delete",n,t);break;case e.enter:t.shiftKey?o("shiftEnter",n,t):o("enter",n,t)}},on:function(e,t){return n(e,t),this},key:e}}(),v=function(){return{getHost:function(e){var t="."+f.editableClass,n=a(e).closest(t);return n.length?n[0]:r},getNodeIndex:function(e){for(var t=0;null!==(e=e.previousSibling);)t+=1;return t},isVoid:function(e){var t,n,r,o=e.childNodes;for(n=0,r=o.length;r>n;n++){if(t=o[n],3===t.nodeType&&!this.isVoidTextNode(t))return!1;if(1===t.nodeType)return!1}return!0},isVoidTextNode:function(e){return 3===e.nodeType&&!e.nodeValue},isWhitespaceOnly:function(e){return 3===e.nodeType&&0===this.lastOffsetWithContent(e)},isLinebreak:function(e){return 1===e.nodeType&&"BR"===e.tagName},lastOffsetWithContent:function(e){if(3===e.nodeType)return c.trimRight(e.nodeValue).length;var t,n=e.childNodes;for(t=n.length-1;t>=0;t--)if(e=n[t],!this.isWhitespaceOnly(e)&&!this.isLinebreak(e))return t+1;return 0},isBeginningOfHost:function(e,t,n){if(t===e)return this.isStartOffset(t,n);if(this.isStartOffset(t,n)){var r=t.parentNode,o=this.getNodeIndex(t);return this.isBeginningOfHost(e,r,o)}return!1},isEndOfHost:function(e,t,n){if(t===e)return this.isEndOffset(t,n);if(this.isEndOffset(t,n)){var r=t.parentNode,o=this.getNodeIndex(t)+1;return this.isEndOfHost(e,r,o)}return!1},isStartOffset:function(e,t){return 3===e.nodeType?0===t:0===e.childNodes.length?!0:e.childNodes[t]===e.firstChild},isEndOffset:function(e,t){return 3===e.nodeType?t===e.length:0===e.childNodes.length?!0:t>0?e.childNodes[t-1]===e.lastChild:!1},isTextEndOfHost:function(e,t,n){if(t===e)return this.isTextEndOffset(t,n);if(this.isTextEndOffset(t,n)){var r=t.parentNode,o=this.getNodeIndex(t)+1;return this.isTextEndOfHost(e,r,o)}return!1},isTextEndOffset:function(e,t){if(3===e.nodeType){var n=c.trimRight(e.nodeValue);return t>=n.length}if(0===e.childNodes.length)return!0;var r=this.lastOffsetWithContent(e);return t>=r},isSameNode:function(e,t){var n,r,o;if(e.nodeType!==t.nodeType)return!1;if(e.nodeName!==t.nodeName)return!1;for(n=0,r=e.attributes.length;r>n;n++)if(o=e.attributes[n],t.getAttribute(o.name)!==o.value)return!1;return!0},latestChild:function(e){return e.lastChild?this.latestChild(e.lastChild):e},isDocumentFragmentWithoutChildren:function(e){return e&&11===e.nodeType&&0===e.childNodes.length?!0:!1}}}(),C=function(e,t){this.host=e&&e.jquery?e[0]:e,this.range=t,this.isAnythingSelected=t!==r,this.isCursor=this.isAnythingSelected&&t.collapsed,this.isSelection=this.isAnythingSelected&&!this.isCursor};C.prototype.getCursor=function(){return this.isCursor?new h(this.host,this.range):r},C.prototype.getSelection=function(){return this.isSelection?new y(this.host,this.range):r +},C.prototype.forceCursor=function(){if(this.isSelection){var e=this.getSelection();return e.deleteContent()}return this.getCursor()},C.prototype.isDifferentFrom=function(e){e=e||new C;var t=this.range,n=e.range;return t&&n?!t.equals(n):t||n?!0:!1};var R=function(){var t=0,n="",r=function(e,t){return e.querySelector("#"+t)};return{insertRangeBoundaryMarker:function(r,o){var i,s="editable-range-boundary-"+(t+=1),a=e.document,c=r.cloneRange();return c.collapse(o),i=a.createElement("span"),i.id=s,i.style.lineHeight="0",i.style.display="none",i.appendChild(a.createTextNode(n)),c.insertNode(i),c.detach(),i},setRangeBoundary:function(e,t,n,o){var i=r(e,n);i?(t[o?"setStartBefore":"setEndBefore"](i),i.parentNode.removeChild(i)):console.log("Marker element has been removed. Cannot restore selection.")},save:function(t){e.document;var n,r,o;return t.collapsed?(o=this.insertRangeBoundaryMarker(t,!1),n={markerId:o.id,collapsed:!0}):(o=this.insertRangeBoundaryMarker(t,!1),r=this.insertRangeBoundaryMarker(t,!0),n={startMarkerId:r.id,endMarkerId:o.id,collapsed:!1}),t.collapsed?t.collapseBefore(o):(t.setEndBefore(o),t.setStartAfter(r)),n},restore:function(e,t){if(!t.restored){var n=rangy.createRange();if(t.collapsed){var o=r(e,t.markerId);if(o){o.style.display="inline";var i=o.previousSibling;i&&3===i.nodeType?(o.parentNode.removeChild(o),n.collapseToPoint(i,i.length)):(n.collapseBefore(o),o.parentNode.removeChild(o))}else console.log("Marker element has been removed. Cannot restore selection.")}else this.setRangeBoundary(e,n,t.startMarkerId,!0),this.setRangeBoundary(e,n,t.endMarkerId,!1);return n.normalizeBoundaries(),n}}}}(),N=function(){var e,t,n,o=function(){if(e=rangy.getSelection(),e.rangeCount){var t=e.getRangeAt(0),n=v.getHost(t.commonAncestorContainer);if(n)return new C(n,t)}return new C};return{getFreshRange:function(){return o()},getFreshSelection:function(){var e=o();return e.isCursor?e.getCursor():e.getSelection()},getSelection:function(){return t},forceCursor:function(){var e=o();return e.forceCursor()},selectionChanged:function(){var e=o();if(e.isDifferentFrom(n)){var i=t;n=e,i&&(i.isCursor&&!n.isCursor?g.notifyListeners("cursor",i.host):i.isSelection&&!n.isSelection&&g.notifyListeners("selection",i.host)),n.isCursor?(t=new h(n.host,n.range),g.notifyListeners("cursor",t.host,t)):n.isSelection?(t=new y(n.host,n.range),g.notifyListeners("selection",t.host,t)):t=r}}}}(),y=function(){var e=function(e,t){this.host=e,this.range=t,this.isSelection=!0},t=function(){};return t.prototype=h.prototype,e.prototype=a.extend(new t,{text:function(){return""+this.range},html:function(){return this.range.toHtml()},isAllSelected:function(){return v.isBeginningOfHost(this.host,this.range.startContainer,this.range.startOffset)&&v.isTextEndOfHost(this.host,this.range.endContainer,this.range.endOffset)},getRects:function(){var e=this.range.nativeRange.getClientRects();return e},link:function(e,t){var n=a("
");e&&n.attr("href",e);for(var r in t)n.attr(r,t[r]);this.forceWrap(n[0])},unlink:function(){this.removeFormatting("a")},toggleLink:function(e,t){var n=this.getTagsByName("a");if(n.length>=1){var r=n[0];this.isExactSelection(r,"visible")?this.unlink():this.expandTo(r)}else this.link(e,t)},toggle:function(e){this.range=l.toggleTag(this.host,this.range,e),this.setSelection()},makeBold:function(){var e=a(f.boldTag);this.forceWrap(e[0])},toggleBold:function(){var e=a(f.boldTag);this.toggle(e[0])},giveEmphasis:function(){var e=a(f.italicTag);this.forceWrap(e[0])},toggleEmphasis:function(){var e=a(f.italicTag);this.toggle(e[0])},surround:function(e,t){this.range=l.surround(this.host,this.range,e,t),this.setSelection()},removeSurround:function(e,t){this.range=l.deleteCharacter(this.host,this.range,e),this.range=l.deleteCharacter(this.host,this.range,t),this.setSelection()},toggleSurround:function(e,t){this.containsString(e)&&this.containsString(t)?this.removeSurround(e,t):this.surround(e,t)},removeFormatting:function(e){this.range=l.removeFormatting(this.host,this.range,e),this.setSelection()},deleteContent:function(){return this.range.deleteContents(),new h(this.host,this.range)},expandTo:function(e){this.range=l.expandTo(this.host,this.range,e),this.setSelection()},forceWrap:function(e){this.range=l.forceWrap(this.host,this.range,e),this.setSelection()},getTags:function(e){return l.getTags(this.host,this.range,e)},getTagsByName:function(e){return l.getTagsByName(this.host,this.range,e)},isExactSelection:function(e,t){return l.isExactSelection(this.range,e,t)},containsString:function(e){return l.containsString(this.range,e)},deleteCharacter:function(e){this.range=l.deleteCharacter(this.host,this.range,e),this.setSelection()}}),e}();e.Editable=s}(window,document,window.jQuery); \ No newline at end of file diff --git a/package.json b/package.json index b288ded5..780152a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "editable.js", - "version": "0.0.1", + "name": "EditableJS", + "version": "0.1.0", "dependencies": {}, "devDependencies": { "grunt": "~0.4.1", @@ -14,7 +14,8 @@ "grunt-regarde": "~0.1.1", "grunt-open": "~0.2.0", "matchdep": "~0.1.1", - "grunt-karma": "~0.3.0" + "grunt-karma": "~0.3.0", + "grunt-bump": "0.0.13" }, "engines": { "node": ">=0.8.0"