spec/dummy/tmp/cache/assets/development/sprockets/93186b72533f30d0ff1585bc551c9377 in storytime-0.0.4 vs spec/dummy/tmp/cache/assets/development/sprockets/93186b72533f30d0ff1585bc551c9377 in storytime-1.0.0

- old
+ new

@@ -1,10 +1,8 @@ {I" class:ETI"BundledAsset;FI"logical_path;TI"storytime/application.js;TI" pathname;TI"Z/Users/ben/flyover/projects/storytime/app/assets/javascripts/storytime/application.js;FI"content_type;TI"application/javascript;TI" -mtime;Tl+TI" length;Ti‘ - I" digest;TI"%df70927f3f8aac5b0b53e8190571db79;FI" source;TI"‘ - // This is a manifest file that'll be compiled into application.js, which will include all the files +mtime;Tl++#FTI" length;TiCงI" digest;TI"%e527b0c3e91a338b56ae67dd385d347e;FI" source;TI"Cง// This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. // @@ -24,9536 +22,18 @@ -window.Storytime || (window.Storytime = {}) -window.Storytime.Dashboard = {} -; -/** - * @license wysihtml5 v0.3.0 - * https://github.com/xing/wysihtml5 - * - * Author: Christopher Blum (https://github.com/tiff) - * - * Copyright (C) 2012 XING AG - * Licensed under the MIT license (MIT) - * - */ -var wysihtml5 = { - version: "0.3.0", - - // namespaces - commands: {}, - dom: {}, - quirks: {}, - toolbar: {}, - lang: {}, - selection: {}, - views: {}, - - INVISIBLE_SPACE: "\uFEFF", - - EMPTY_FUNCTION: function() {}, - - ELEMENT_NODE: 1, - TEXT_NODE: 3, - - BACKSPACE_KEY: 8, - ENTER_KEY: 13, - ESCAPE_KEY: 27, - SPACE_KEY: 32, - DELETE_KEY: 46 -};/** - * @license Rangy, a cross-browser JavaScript range and selection library - * http://code.google.com/p/rangy/ - * - * Copyright 2011, Tim Down - * Licensed under the MIT license. - * Version: 1.2.2 - * Build date: 13 November 2011 - */ -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.2", - 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 assertRangeValid(range) { - assertNotDetached(range); - if (isOrphan(range.startContainer) || isOrphan(range.endContainer) || - !isValidOffset(range.startContainer, range.startOffset) || - !isValidOffset(range.endContainer, range.endOffset)) { - 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 = "<b>x</b>"; - 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); - }, - - 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): - - <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul> - - 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 <pre> 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"); - body.appendChild(iframe); - - var iframeDoc = dom.getIframeDocument(iframe); - iframeDoc.open(); - iframeDoc.write("<html><head></head><body>12</body></html>"); - 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; - }); -}); -/* - Base.js, version 1.1a - Copyright 2006-2010, Dean Edwards - License: http://www.opensource.org/licenses/mit-license.php -*/ - -var Base = function() { - // dummy -}; - -Base.extend = function(_instance, _static) { // subclass - var extend = Base.prototype.extend; - - // build the prototype - Base._prototyping = true; - var proto = new this; - extend.call(proto, _instance); - proto.base = function() { - // call this method from any other method to invoke that method's ancestor - }; - delete Base._prototyping; - - // create the wrapper for the constructor function - //var constructor = proto.constructor.valueOf(); //-dean - var constructor = proto.constructor; - var klass = proto.constructor = function() { - if (!Base._prototyping) { - if (this._constructing || this.constructor == klass) { // instantiation - this._constructing = true; - constructor.apply(this, arguments); - delete this._constructing; - } else if (arguments[0] != null) { // casting - return (arguments[0].extend || extend).call(arguments[0], proto); - } - } - }; - - // build the class interface - klass.ancestor = this; - klass.extend = this.extend; - klass.forEach = this.forEach; - klass.implement = this.implement; - klass.prototype = proto; - klass.toString = this.toString; - klass.valueOf = function(type) { - //return (type == "object") ? klass : constructor; //-dean - return (type == "object") ? klass : constructor.valueOf(); - }; - extend.call(klass, _static); - // class initialisation - if (typeof klass.init == "function") klass.init(); - return klass; -}; - -Base.prototype = { - extend: function(source, value) { - if (arguments.length > 1) { // extending with a name/value pair - var ancestor = this[source]; - if (ancestor && (typeof value == "function") && // overriding a method? - // the valueOf() comparison is to avoid circular references - (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) && - /\bbase\b/.test(value)) { - // get the underlying method - var method = value.valueOf(); - // override - value = function() { - var previous = this.base || Base.prototype.base; - this.base = ancestor; - var returnValue = method.apply(this, arguments); - this.base = previous; - return returnValue; - }; - // point to the underlying method - value.valueOf = function(type) { - return (type == "object") ? value : method; - }; - value.toString = Base.toString; - } - this[source] = value; - } else if (source) { // extending with an object literal - var extend = Base.prototype.extend; - // if this object has a customised extend method then use it - if (!Base._prototyping && typeof this != "function") { - extend = this.extend || extend; - } - var proto = {toSource: null}; - // do the "toString" and other methods manually - var hidden = ["constructor", "toString", "valueOf"]; - // if we are prototyping then include the constructor - var i = Base._prototyping ? 0 : 1; - while (key = hidden[i++]) { - if (source[key] != proto[key]) { - extend.call(this, key, source[key]); - - } - } - // copy each of the source object's properties to this object - for (var key in source) { - if (!proto[key]) extend.call(this, key, source[key]); - } - } - return this; - } -}; - -// initialise -Base = Base.extend({ - constructor: function() { - this.extend(arguments[0]); - } -}, { - ancestor: Object, - version: "1.1", - - forEach: function(object, block, context) { - for (var key in object) { - if (this.prototype[key] === undefined) { - block.call(context, object[key], key, object); - } - } - }, - - implement: function() { - for (var i = 0; i < arguments.length; i++) { - if (typeof arguments[i] == "function") { - // if it's a function, call it - arguments[i](this.prototype); - } else { - // add the interface using the extend method - this.prototype.extend(arguments[i]); - } - } - return this; - }, - - toString: function() { - return String(this.valueOf()); - } -});/** - * Detect browser support for specific features - */ -wysihtml5.browser = (function() { - var userAgent = navigator.userAgent, - testElement = document.createElement("div"), - // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect - isIE = userAgent.indexOf("MSIE") !== -1 && userAgent.indexOf("Opera") === -1, - isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1, - isWebKit = userAgent.indexOf("AppleWebKit/") !== -1, - isChrome = userAgent.indexOf("Chrome/") !== -1, - isOpera = userAgent.indexOf("Opera/") !== -1; - - function iosVersion(userAgent) { - return ((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [, 0])[1]; - } - - return { - // Static variable needed, publicly accessible, to be able override it in unit tests - USER_AGENT: userAgent, - - /** - * Exclude browsers that are not capable of displaying and handling - * contentEditable as desired: - * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable - * - IE < 8 create invalid markup and crash randomly from time to time - * - * @return {Boolean} - */ - supported: function() { - var userAgent = this.USER_AGENT.toLowerCase(), - // Essential for making html elements editable - hasContentEditableSupport = "contentEditable" in testElement, - // Following methods are needed in order to interact with the contentEditable area - hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState, - // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+ - hasQuerySelectorSupport = document.querySelector && document.querySelectorAll, - // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05) - isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1; - - return hasContentEditableSupport - && hasEditingApiSupport - && hasQuerySelectorSupport - && !isIncompatibleMobileBrowser; - }, - - isTouchDevice: function() { - return this.supportsEvent("touchmove"); - }, - - isIos: function() { - var userAgent = this.USER_AGENT.toLowerCase(); - return userAgent.indexOf("webkit") !== -1 && userAgent.indexOf("mobile") !== -1; - }, - - /** - * Whether the browser supports sandboxed iframes - * Currently only IE 6+ offers such feature <iframe security="restricted"> - * - * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx - * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx - * - * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage) - */ - supportsSandboxedIframes: function() { - return isIE; - }, - - /** - * IE6+7 throw a mixed content warning when the src of an iframe - * is empty/unset or about:blank - * window.querySelector is implemented as of IE8 - */ - throwsMixedContentWarningWhenIframeSrcIsEmpty: function() { - return !("querySelector" in document); - }, - - /** - * Whether the caret is correctly displayed in contentEditable elements - * Firefox sometimes shows a huge caret in the beginning after focusing - */ - displaysCaretInEmptyContentEditableCorrectly: function() { - return !isGecko; - }, - - /** - * Opera and IE are the only browsers who offer the css value - * in the original unit, thx to the currentStyle object - * All other browsers provide the computed style in px via window.getComputedStyle - */ - hasCurrentStyleProperty: function() { - return "currentStyle" in testElement; - }, - - /** - * Whether the browser inserts a <br> when pressing enter in a contentEditable element - */ - insertsLineBreaksOnReturn: function() { - return isGecko; - }, - - supportsPlaceholderAttributeOn: function(element) { - return "placeholder" in element; - }, - - supportsEvent: function(eventName) { - return "on" + eventName in testElement || (function() { - testElement.setAttribute("on" + eventName, "return;"); - return typeof(testElement["on" + eventName]) === "function"; - })(); - }, - - /** - * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe - */ - supportsEventsInIframeCorrectly: function() { - return !isOpera; - }, - - /** - * Chrome & Safari only fire the ondrop/ondragend/... events when the ondragover event is cancelled - * with event.preventDefault - * Firefox 3.6 fires those events anyway, but the mozilla doc says that the dragover/dragenter event needs - * to be cancelled - */ - firesOnDropOnlyWhenOnDragOverIsCancelled: function() { - return isWebKit || isGecko; - }, - - /** - * Whether the browser supports the event.dataTransfer property in a proper way - */ - supportsDataTransfer: function() { - try { - // Firefox doesn't support dataTransfer in a safe way, it doesn't strip script code in the html payload (like Chrome does) - return isWebKit && (window.Clipboard || window.DataTransfer).prototype.getData; - } catch(e) { - return false; - } - }, - - /** - * Everything below IE9 doesn't know how to treat HTML5 tags - * - * @param {Object} context The document object on which to check HTML5 support - * - * @example - * wysihtml5.browser.supportsHTML5Tags(document); - */ - supportsHTML5Tags: function(context) { - var element = context.createElement("div"), - html5 = "<article>foo</article>"; - element.innerHTML = html5; - return element.innerHTML.toLowerCase() === html5; - }, - - /** - * Checks whether a document supports a certain queryCommand - * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree - * in oder to report correct results - * - * @param {Object} doc Document object on which to check for a query command - * @param {String} command The query command to check for - * @return {Boolean} - * - * @example - * wysihtml5.browser.supportsCommand(document, "bold"); - */ - supportsCommand: (function() { - // Following commands are supported but contain bugs in some browsers - var buggyCommands = { - // formatBlock fails with some tags (eg. <blockquote>) - "formatBlock": isIE, - // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets - // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>) - // IE and Opera act a bit different here as they convert the entire content of the current block element into a list - "insertUnorderedList": isIE || isOpera || isWebKit, - "insertOrderedList": isIE || isOpera || isWebKit - }; - - // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands - var supported = { - "insertHTML": isGecko - }; - - return function(doc, command) { - var isBuggy = buggyCommands[command]; - if (!isBuggy) { - // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled - try { - return doc.queryCommandSupported(command); - } catch(e1) {} - - try { - return doc.queryCommandEnabled(command); - } catch(e2) { - return !!supported[command]; - } - } - return false; - }; - })(), - - /** - * IE: URLs starting with: - * www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://, - * nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url: - * will automatically be auto-linked when either the user inserts them via copy&paste or presses the - * space bar when the caret is directly after such an url. - * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll - * (related blog post on msdn - * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx). - */ - doesAutoLinkingInContentEditable: function() { - return isIE; - }, - - /** - * As stated above, IE auto links urls typed into contentEditable elements - * Since IE9 it's possible to prevent this behavior - */ - canDisableAutoLinking: function() { - return this.supportsCommand(document, "AutoUrlDetect"); - }, - - /** - * IE leaves an empty paragraph in the contentEditable element after clearing it - * Chrome/Safari sometimes an empty <div> - */ - clearsContentEditableCorrectly: function() { - return isGecko || isOpera || isWebKit; - }, - - /** - * IE gives wrong results for getAttribute - */ - supportsGetAttributeCorrectly: function() { - var td = document.createElement("td"); - return td.getAttribute("rowspan") != "1"; - }, - - /** - * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them. - * Chrome and Safari both don't support this - */ - canSelectImagesInContentEditable: function() { - return isGecko || isIE || isOpera; - }, - - /** - * When the caret is in an empty list (<ul><li>|</li></ul>) which is the first child in an contentEditable container - * pressing backspace doesn't remove the entire list as done in other browsers - */ - clearsListsInContentEditableCorrectly: function() { - return isGecko || isIE || isWebKit; - }, - - /** - * All browsers except Safari and Chrome automatically scroll the range/caret position into view - */ - autoScrollsToCaret: function() { - return !isWebKit; - }, - - /** - * Check whether the browser automatically closes tags that don't need to be opened - */ - autoClosesUnclosedTags: function() { - var clonedTestElement = testElement.cloneNode(false), - returnValue, - innerHTML; - - clonedTestElement.innerHTML = "<p><div></div>"; - innerHTML = clonedTestElement.innerHTML.toLowerCase(); - returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>"; - - // Cache result by overwriting current function - this.autoClosesUnclosedTags = function() { return returnValue; }; - - return returnValue; - }, - - /** - * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists - */ - supportsNativeGetElementsByClassName: function() { - return String(document.getElementsByClassName).indexOf("[native code]") !== -1; - }, - - /** - * As of now (19.04.2011) only supported by Firefox 4 and Chrome - * See https://developer.mozilla.org/en/DOM/Selection/modify - */ - supportsSelectionModify: function() { - return "getSelection" in window && "modify" in window.getSelection(); - }, - - /** - * Whether the browser supports the classList object for fast className manipulation - * See https://developer.mozilla.org/en/DOM/element.classList - */ - supportsClassList: function() { - return "classList" in testElement; - }, - - /** - * Opera needs a white space after a <br> in order to position the caret correctly - */ - needsSpaceAfterLineBreak: function() { - return isOpera; - }, - - /** - * Whether the browser supports the speech api on the given element - * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/ - * - * @example - * var input = document.createElement("input"); - * if (wysihtml5.browser.supportsSpeechApiOn(input)) { - * // ... - * } - */ - supportsSpeechApiOn: function(input) { - var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [, 0]; - return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input); - }, - - /** - * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest - * See https://connect.microsoft.com/ie/feedback/details/650112 - * or try the POC http://tifftiff.de/ie9_crash/ - */ - crashesWhenDefineProperty: function(property) { - return isIE && (property === "XMLHttpRequest" || property === "XDomainRequest"); - }, - - /** - * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element - */ - doesAsyncFocus: function() { - return isIE; - }, - - /** - * In IE it's impssible for the user and for the selection library to set the caret after an <img> when it's the lastChild in the document - */ - hasProblemsSettingCaretAfterImg: function() { - return isIE; - }, - - hasUndoInContextMenu: function() { - return isGecko || isChrome || isOpera; - } - }; -})();wysihtml5.lang.array = function(arr) { - return { - /** - * Check whether a given object exists in an array - * - * @example - * wysihtml5.lang.array([1, 2]).contains(1); - * // => true - */ - contains: function(needle) { - if (arr.indexOf) { - return arr.indexOf(needle) !== -1; - } else { - for (var i=0, length=arr.length; i<length; i++) { - if (arr[i] === needle) { return true; } - } - return false; - } - }, - - /** - * Substract one array from another - * - * @example - * wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]); - * // => [1, 2] - */ - without: function(arrayToSubstract) { - arrayToSubstract = wysihtml5.lang.array(arrayToSubstract); - var newArr = [], - i = 0, - length = arr.length; - for (; i<length; i++) { - if (!arrayToSubstract.contains(arr[i])) { - newArr.push(arr[i]); - } - } - return newArr; - }, - - /** - * Return a clean native array - * - * Following will convert a Live NodeList to a proper Array - * @example - * var childNodes = wysihtml5.lang.array(document.body.childNodes).get(); - */ - get: function() { - var i = 0, - length = arr.length, - newArray = []; - for (; i<length; i++) { - newArray.push(arr[i]); - } - return newArray; - } - }; -};wysihtml5.lang.Dispatcher = Base.extend( - /** @scope wysihtml5.lang.Dialog.prototype */ { - observe: function(eventName, handler) { - this.events = this.events || {}; - this.events[eventName] = this.events[eventName] || []; - this.events[eventName].push(handler); - return this; - }, - - on: function() { - return this.observe.apply(this, wysihtml5.lang.array(arguments).get()); - }, - - fire: function(eventName, payload) { - this.events = this.events || {}; - var handlers = this.events[eventName] || [], - i = 0; - for (; i<handlers.length; i++) { - handlers[i].call(this, payload); - } - return this; - }, - - stopObserving: function(eventName, handler) { - this.events = this.events || {}; - var i = 0, - handlers, - newHandlers; - if (eventName) { - handlers = this.events[eventName] || [], - newHandlers = []; - for (; i<handlers.length; i++) { - if (handlers[i] !== handler && handler) { - newHandlers.push(handlers[i]); - } - } - this.events[eventName] = newHandlers; - } else { - // Clean up all events - this.events = {}; - } - return this; - } -});wysihtml5.lang.object = function(obj) { - return { - /** - * @example - * wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get(); - * // => { foo: 1, bar: 2, baz: 3 } - */ - merge: function(otherObj) { - for (var i in otherObj) { - obj[i] = otherObj[i]; - } - return this; - }, - - get: function() { - return obj; - }, - - /** - * @example - * wysihtml5.lang.object({ foo: 1 }).clone(); - * // => { foo: 1 } - */ - clone: function() { - var newObj = {}, - i; - for (i in obj) { - newObj[i] = obj[i]; - } - return newObj; - }, - - /** - * @example - * wysihtml5.lang.object([]).isArray(); - * // => true - */ - isArray: function() { - return Object.prototype.toString.call(obj) === "[object Array]"; - } - }; -};(function() { - var WHITE_SPACE_START = /^\s+/, - WHITE_SPACE_END = /\s+$/; - wysihtml5.lang.string = function(str) { - str = String(str); - return { - /** - * @example - * wysihtml5.lang.string(" foo ").trim(); - * // => "foo" - */ - trim: function() { - return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, ""); - }, - - /** - * @example - * wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" }); - * // => "Hello Christopher" - */ - interpolate: function(vars) { - for (var i in vars) { - str = this.replace("#{" + i + "}").by(vars[i]); - } - return str; - }, - - /** - * @example - * wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans"); - * // => "Hello Hans" - */ - replace: function(search) { - return { - by: function(replace) { - return str.split(search).join(replace); - } - } - } - }; - }; -})();/** - * Find urls in descendant text nodes of an element and auto-links them - * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/ - * - * @param {Element} element Container element in which to search for urls - * - * @example - * <div id="text-container">Please click here: www.google.com</div> - * <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script> - */ -(function(wysihtml5) { - var /** - * Don't auto-link urls that are contained in the following elements: - */ - IGNORE_URLS_IN = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]), - /** - * revision 1: - * /(\S+\.{1}[^\s\,\.\!]+)/g - * - * revision 2: - * /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim - * - * put this in the beginning if you don't wan't to match within a word - * (^|[\>\(\{\[\s\>]) - */ - URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi, - TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i, - MAX_DISPLAY_LENGTH = 100, - BRACKETS = { ")": "(", "]": "[", "}": "{" }; - - function autoLink(element) { - if (_hasParentThatShouldBeIgnored(element)) { - return element; - } - - if (element === element.ownerDocument.documentElement) { - element = element.ownerDocument.body; - } - - return _parseNode(element); - } - - /** - * This is basically a rebuild of - * the rails auto_link_urls text helper - */ - function _convertUrlsToLinks(str) { - return str.replace(URL_REG_EXP, function(match, url) { - var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "", - opening = BRACKETS[punctuation]; - url = url.replace(TRAILING_CHAR_REG_EXP, ""); - - if (url.split(opening).length > url.split(punctuation).length) { - url = url + punctuation; - punctuation = ""; - } - var realUrl = url, - displayUrl = url; - if (url.length > MAX_DISPLAY_LENGTH) { - displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "..."; - } - // Add http prefix if necessary - if (realUrl.substr(0, 4) === "www.") { - realUrl = "http://" + realUrl; - } - - return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation; - }); - } - - /** - * Creates or (if already cached) returns a temp element - * for the given document object - */ - function _getTempElement(context) { - var tempElement = context._wysihtml5_tempElement; - if (!tempElement) { - tempElement = context._wysihtml5_tempElement = context.createElement("div"); - } - return tempElement; - } - - /** - * Replaces the original text nodes with the newly auto-linked dom tree - */ - function _wrapMatchesInNode(textNode) { - var parentNode = textNode.parentNode, - tempElement = _getTempElement(parentNode.ownerDocument); - - // We need to insert an empty/temporary <span /> to fix IE quirks - // Elsewise IE would strip white space in the beginning - tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(textNode.data); - tempElement.removeChild(tempElement.firstChild); - - while (tempElement.firstChild) { - // inserts tempElement.firstChild before textNode - parentNode.insertBefore(tempElement.firstChild, textNode); - } - parentNode.removeChild(textNode); - } - - function _hasParentThatShouldBeIgnored(node) { - var nodeName; - while (node.parentNode) { - node = node.parentNode; - nodeName = node.nodeName; - if (IGNORE_URLS_IN.contains(nodeName)) { - return true; - } else if (nodeName === "body") { - return false; - } - } - return false; - } - - function _parseNode(element) { - if (IGNORE_URLS_IN.contains(element.nodeName)) { - return; - } - - if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) { - _wrapMatchesInNode(element); - return; - } - - var childNodes = wysihtml5.lang.array(element.childNodes).get(), - childNodesLength = childNodes.length, - i = 0; - - for (; i<childNodesLength; i++) { - _parseNode(childNodes[i]); - } - - return element; - } - - wysihtml5.dom.autoLink = autoLink; - - // Reveal url reg exp to the outside - wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP; -})(wysihtml5);(function(wysihtml5) { - var supportsClassList = wysihtml5.browser.supportsClassList(), - api = wysihtml5.dom; - - api.addClass = function(element, className) { - if (supportsClassList) { - return element.classList.add(className); - } - if (api.hasClass(element, className)) { - return; - } - element.className += " " + className; - }; - - api.removeClass = function(element, className) { - if (supportsClassList) { - return element.classList.remove(className); - } - - element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " "); - }; - - api.hasClass = function(element, className) { - if (supportsClassList) { - return element.classList.contains(className); - } - - var elementClassName = element.className; - return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); - }; -})(wysihtml5); -wysihtml5.dom.contains = (function() { - var documentElement = document.documentElement; - if (documentElement.contains) { - return function(container, element) { - if (element.nodeType !== wysihtml5.ELEMENT_NODE) { - element = element.parentNode; - } - return container !== element && container.contains(element); - }; - } else if (documentElement.compareDocumentPosition) { - return function(container, element) { - // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition - return !!(container.compareDocumentPosition(element) & 16); - }; - } -})();/** - * Converts an HTML fragment/element into a unordered/ordered list - * - * @param {Element} element The element which should be turned into a list - * @param {String} listType The list type in which to convert the tree (either "ul" or "ol") - * @return {Element} The created list - * - * @example - * <!-- Assume the following dom: --> - * <span id="pseudo-list"> - * eminem<br> - * dr. dre - * <div>50 Cent</div> - * </span> - * - * <script> - * wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul"); - * </script> - * - * <!-- Will result in: --> - * <ul> - * <li>eminem</li> - * <li>dr. dre</li> - * <li>50 Cent</li> - * </ul> - */ -wysihtml5.dom.convertToList = (function() { - function _createListItem(doc, list) { - var listItem = doc.createElement("li"); - list.appendChild(listItem); - return listItem; - } - - function _createList(doc, type) { - return doc.createElement(type); - } - - function convertToList(element, listType) { - if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") { - // Already a list - return element; - } - - var doc = element.ownerDocument, - list = _createList(doc, listType), - lineBreaks = element.querySelectorAll("br"), - lineBreaksLength = lineBreaks.length, - childNodes, - childNodesLength, - childNode, - lineBreak, - parentNode, - isBlockElement, - isLineBreak, - currentListItem, - i; - - // First find <br> at the end of inline elements and move them behind them - for (i=0; i<lineBreaksLength; i++) { - lineBreak = lineBreaks[i]; - while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) { - if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") { - parentNode.removeChild(lineBreak); - break; - } - wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode); - } - } - - childNodes = wysihtml5.lang.array(element.childNodes).get(); - childNodesLength = childNodes.length; - - for (i=0; i<childNodesLength; i++) { - currentListItem = currentListItem || _createListItem(doc, list); - childNode = childNodes[i]; - isBlockElement = wysihtml5.dom.getStyle("display").from(childNode) === "block"; - isLineBreak = childNode.nodeName === "BR"; - - if (isBlockElement) { - // Append blockElement to current <li> if empty, otherwise create a new one - currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem; - currentListItem.appendChild(childNode); - currentListItem = null; - continue; - } - - if (isLineBreak) { - // Only create a new list item in the next iteration when the current one has already content - currentListItem = currentListItem.firstChild ? null : currentListItem; - continue; - } - - currentListItem.appendChild(childNode); - } - - element.parentNode.replaceChild(list, element); - return list; - } - - return convertToList; -})();/** - * Copy a set of attributes from one element to another - * - * @param {Array} attributesToCopy List of attributes which should be copied - * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to - * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked - * with the element where to copy the attributes to (see example) - * - * @example - * var textarea = document.querySelector("textarea"), - * div = document.querySelector("div[contenteditable=true]"), - * anotherDiv = document.querySelector("div.preview"); - * wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv); - * - */ -wysihtml5.dom.copyAttributes = function(attributesToCopy) { - return { - from: function(elementToCopyFrom) { - return { - to: function(elementToCopyTo) { - var attribute, - i = 0, - length = attributesToCopy.length; - for (; i<length; i++) { - attribute = attributesToCopy[i]; - if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") { - elementToCopyTo[attribute] = elementToCopyFrom[attribute]; - } - } - return { andTo: arguments.callee }; - } - }; - } - }; -};/** - * Copy a set of styles from one element to another - * Please note that this only works properly across browsers when the element from which to copy the styles - * is in the dom - * - * Interesting article on how to copy styles - * - * @param {Array} stylesToCopy List of styles which should be copied - * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to - * copy the styles from., this again returns an object which provides a method named "to" which can be invoked - * with the element where to copy the styles to (see example) - * - * @example - * var textarea = document.querySelector("textarea"), - * div = document.querySelector("div[contenteditable=true]"), - * anotherDiv = document.querySelector("div.preview"); - * wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv); - * - */ -(function(dom) { - - /** - * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set - * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then - * its computed css width will be 198px - */ - var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"]; - - var shouldIgnoreBoxSizingBorderBox = function(element) { - if (hasBoxSizingBorderBox(element)) { - return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth; - } - return false; - }; - - var hasBoxSizingBorderBox = function(element) { - var i = 0, - length = BOX_SIZING_PROPERTIES.length; - for (; i<length; i++) { - if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") { - return BOX_SIZING_PROPERTIES[i]; - } - } - }; - - dom.copyStyles = function(stylesToCopy) { - return { - from: function(element) { - if (shouldIgnoreBoxSizingBorderBox(element)) { - stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES); - } - - var cssText = "", - length = stylesToCopy.length, - i = 0, - property; - for (; i<length; i++) { - property = stylesToCopy[i]; - cssText += property + ":" + dom.getStyle(property).from(element) + ";"; - } - - return { - to: function(element) { - dom.setStyles(cssText).on(element); - return { andTo: arguments.callee }; - } - }; - } - }; - }; -})(wysihtml5.dom);/** - * Event Delegation - * - * @example - * wysihtml5.dom.delegate(document.body, "a", "click", function() { - * // foo - * }); - */ -(function(wysihtml5) { - - wysihtml5.dom.delegate = function(container, selector, eventName, handler) { - return wysihtml5.dom.observe(container, eventName, function(event) { - var target = event.target, - match = wysihtml5.lang.array(container.querySelectorAll(selector)); - - while (target && target !== container) { - if (match.contains(target)) { - handler.call(target, event); - break; - } - target = target.parentNode; - } - }); - }; - -})(wysihtml5);/** - * Returns the given html wrapped in a div element - * - * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly - * when inserted via innerHTML - * - * @param {String} html The html which should be wrapped in a dom element - * @param {Obejct} [context] Document object of the context the html belongs to - * - * @example - * wysihtml5.dom.getAsDom("<article>foo</article>"); - */ -wysihtml5.dom.getAsDom = (function() { - - var _innerHTMLShiv = function(html, context) { - var tempElement = context.createElement("div"); - tempElement.style.display = "none"; - context.body.appendChild(tempElement); - // IE throws an exception when trying to insert <frameset></frameset> via innerHTML - try { tempElement.innerHTML = html; } catch(e) {} - context.body.removeChild(tempElement); - return tempElement; - }; - - /** - * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element - */ - var _ensureHTML5Compatibility = function(context) { - if (context._wysihtml5_supportsHTML5Tags) { - return; - } - for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) { - context.createElement(HTML5_ELEMENTS[i]); - } - context._wysihtml5_supportsHTML5Tags = true; - }; - - - /** - * List of html5 tags - * taken from http://simon.html5.org/html5-elements - */ - var HTML5_ELEMENTS = [ - "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption", - "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress", - "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr" - ]; - - return function(html, context) { - context = context || document; - var tempElement; - if (typeof(html) === "object" && html.nodeType) { - tempElement = context.createElement("div"); - tempElement.appendChild(html); - } else if (wysihtml5.browser.supportsHTML5Tags(context)) { - tempElement = context.createElement("div"); - tempElement.innerHTML = html; - } else { - _ensureHTML5Compatibility(context); - tempElement = _innerHTMLShiv(html, context); - } - return tempElement; - }; -})();/** - * Walks the dom tree from the given node up until it finds a match - * Designed for optimal performance. - * - * @param {Element} node The from which to check the parent nodes - * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp) - * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50) - * @return {null|Element} Returns the first element that matched the desiredNodeName(s) - * @example - * var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] }); - * // ... or ... - * var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" }); - * // ... or ... - * var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g }); - */ -wysihtml5.dom.getParentElement = (function() { - - function _isSameNodeName(nodeName, desiredNodeNames) { - if (!desiredNodeNames || !desiredNodeNames.length) { - return true; - } - - if (typeof(desiredNodeNames) === "string") { - return nodeName === desiredNodeNames; - } else { - return wysihtml5.lang.array(desiredNodeNames).contains(nodeName); - } - } - - function _isElement(node) { - return node.nodeType === wysihtml5.ELEMENT_NODE; - } - - function _hasClassName(element, className, classRegExp) { - var classNames = (element.className || "").match(classRegExp) || []; - if (!className) { - return !!classNames.length; - } - return classNames[classNames.length - 1] === className; - } - - function _getParentElementWithNodeName(node, nodeName, levels) { - while (levels-- && node && node.nodeName !== "BODY") { - if (_isSameNodeName(node.nodeName, nodeName)) { - return node; - } - node = node.parentNode; - } - return null; - } - - function _getParentElementWithNodeNameAndClassName(node, nodeName, className, classRegExp, levels) { - while (levels-- && node && node.nodeName !== "BODY") { - if (_isElement(node) && - _isSameNodeName(node.nodeName, nodeName) && - _hasClassName(node, className, classRegExp)) { - return node; - } - node = node.parentNode; - } - return null; - } - - return function(node, matchingSet, levels) { - levels = levels || 50; // Go max 50 nodes upwards from current node - if (matchingSet.className || matchingSet.classRegExp) { - return _getParentElementWithNodeNameAndClassName( - node, matchingSet.nodeName, matchingSet.className, matchingSet.classRegExp, levels - ); - } else { - return _getParentElementWithNodeName( - node, matchingSet.nodeName, levels - ); - } - }; -})(); -/** - * Get element's style for a specific css property - * - * @param {Element} element The element on which to retrieve the style - * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...) - * - * @example - * wysihtml5.dom.getStyle("display").from(document.body); - * // => "block" - */ -wysihtml5.dom.getStyle = (function() { - var stylePropertyMapping = { - "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat" - }, - REG_EXP_CAMELIZE = /\-[a-z]/g; - - function camelize(str) { - return str.replace(REG_EXP_CAMELIZE, function(match) { - return match.charAt(1).toUpperCase(); - }); - } - - return function(property) { - return { - from: function(element) { - if (element.nodeType !== wysihtml5.ELEMENT_NODE) { - return; - } - - var doc = element.ownerDocument, - camelizedProperty = stylePropertyMapping[property] || camelize(property), - style = element.style, - currentStyle = element.currentStyle, - styleValue = style[camelizedProperty]; - if (styleValue) { - return styleValue; - } - - // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant - // window.getComputedStyle, since it returns css property values in their original unit: - // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle - // gives you the original "50%". - // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio - if (currentStyle) { - try { - return currentStyle[camelizedProperty]; - } catch(e) { - //ie will occasionally fail for unknown reasons. swallowing exception - } - } - - var win = doc.defaultView || doc.parentWindow, - needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA", - originalOverflow, - returnValue; - - if (win.getComputedStyle) { - // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars - // therfore we remove and restore the scrollbar and calculate the value in between - if (needsOverflowReset) { - originalOverflow = style.overflow; - style.overflow = "hidden"; - } - returnValue = win.getComputedStyle(element, null).getPropertyValue(property); - if (needsOverflowReset) { - style.overflow = originalOverflow || ""; - } - return returnValue; - } - } - }; - }; -})();/** - * High performant way to check whether an element with a specific tag name is in the given document - * Optimized for being heavily executed - * Unleashes the power of live node lists - * - * @param {Object} doc The document object of the context where to check - * @param {String} tagName Upper cased tag name - * @example - * wysihtml5.dom.hasElementWithTagName(document, "IMG"); - */ -wysihtml5.dom.hasElementWithTagName = (function() { - var LIVE_CACHE = {}, - DOCUMENT_IDENTIFIER = 1; - - function _getDocumentIdentifier(doc) { - return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); - } - - return function(doc, tagName) { - var key = _getDocumentIdentifier(doc) + ":" + tagName, - cacheEntry = LIVE_CACHE[key]; - if (!cacheEntry) { - cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName); - } - - return cacheEntry.length > 0; - }; -})();/** - * High performant way to check whether an element with a specific class name is in the given document - * Optimized for being heavily executed - * Unleashes the power of live node lists - * - * @param {Object} doc The document object of the context where to check - * @param {String} tagName Upper cased tag name - * @example - * wysihtml5.dom.hasElementWithClassName(document, "foobar"); - */ -(function(wysihtml5) { - var LIVE_CACHE = {}, - DOCUMENT_IDENTIFIER = 1; - - function _getDocumentIdentifier(doc) { - return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); - } - - wysihtml5.dom.hasElementWithClassName = function(doc, className) { - // getElementsByClassName is not supported by IE<9 - // but is sometimes mocked via library code (which then doesn't return live node lists) - if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) { - return !!doc.querySelector("." + className); - } - - var key = _getDocumentIdentifier(doc) + ":" + className, - cacheEntry = LIVE_CACHE[key]; - if (!cacheEntry) { - cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className); - } - - return cacheEntry.length > 0; - }; -})(wysihtml5); -wysihtml5.dom.insert = function(elementToInsert) { - return { - after: function(element) { - element.parentNode.insertBefore(elementToInsert, element.nextSibling); - }, - - before: function(element) { - element.parentNode.insertBefore(elementToInsert, element); - }, - - into: function(element) { - element.appendChild(elementToInsert); - } - }; -};wysihtml5.dom.insertCSS = function(rules) { - rules = rules.join("\n"); - - return { - into: function(doc) { - var head = doc.head || doc.getElementsByTagName("head")[0], - styleElement = doc.createElement("style"); - - styleElement.type = "text/css"; - - if (styleElement.styleSheet) { - styleElement.styleSheet.cssText = rules; - } else { - styleElement.appendChild(doc.createTextNode(rules)); - } - - if (head) { - head.appendChild(styleElement); - } - } - }; -};/** - * Method to set dom events - * - * @example - * wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... }); - */ -wysihtml5.dom.observe = function(element, eventNames, handler) { - eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames; - - var handlerWrapper, - eventName, - i = 0, - length = eventNames.length; - - for (; i<length; i++) { - eventName = eventNames[i]; - if (element.addEventListener) { - element.addEventListener(eventName, handler, false); - } else { - handlerWrapper = function(event) { - if (!("target" in event)) { - event.target = event.srcElement; - } - event.preventDefault = event.preventDefault || function() { - this.returnValue = false; - }; - event.stopPropagation = event.stopPropagation || function() { - this.cancelBubble = true; - }; - handler.call(element, event); - }; - element.attachEvent("on" + eventName, handlerWrapper); - } - } - - return { - stop: function() { - var eventName, - i = 0, - length = eventNames.length; - for (; i<length; i++) { - eventName = eventNames[i]; - if (element.removeEventListener) { - element.removeEventListener(eventName, handler, false); - } else { - element.detachEvent("on" + eventName, handlerWrapper); - } - } - } - }; -}; -/** - * HTML Sanitizer - * Rewrites the HTML based on given rules - * - * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized - * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will - * be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the - * desired substitution. - * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing - * - * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element. - * - * @example - * var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>'; - * wysihtml5.dom.parse(userHTML, { - * tags { - * p: "div", // Rename p tags to div tags - * font: "span" // Rename font tags to span tags - * div: true, // Keep them, also possible (same result when passing: "div" or true) - * script: undefined // Remove script elements - * } - * }); - * // => <div><div><span>foo bar</span></div></div> - * - * var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>'; - * wysihtml5.dom.parse(userHTML); - * // => '<span><span><span><span>I'm a table!</span></span></span></span>' - * - * var userHTML = '<div>foobar<br>foobar</div>'; - * wysihtml5.dom.parse(userHTML, { - * tags: { - * div: undefined, - * br: true - * } - * }); - * // => '' - * - * var userHTML = '<div class="red">foo</div><div class="pink">bar</div>'; - * wysihtml5.dom.parse(userHTML, { - * classes: { - * red: 1, - * green: 1 - * }, - * tags: { - * div: { - * rename_tag: "p" - * } - * } - * }); - * // => '<p class="red">foo</p><p>bar</p>' - */ -wysihtml5.dom.parse = (function() { - - /** - * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML - * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the - * node isn't closed - * - * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML. - */ - var NODE_TYPE_MAPPING = { - "1": _handleElement, - "3": _handleText - }, - // Rename unknown tags to this - DEFAULT_NODE_NAME = "span", - WHITE_SPACE_REG_EXP = /\s+/, - defaultRules = { tags: {}, classes: {} }, - currentRules = {}; - - /** - * Iterates over all childs of the element, recreates them, appends them into a document fragment - * which later replaces the entire body content - */ - function parse(elementOrHtml, rules, context, cleanUp) { - wysihtml5.lang.object(currentRules).merge(defaultRules).merge(rules).get(); - - context = context || elementOrHtml.ownerDocument || document; - var fragment = context.createDocumentFragment(), - isString = typeof(elementOrHtml) === "string", - element, - newNode, - firstChild; - - if (isString) { - element = wysihtml5.dom.getAsDom(elementOrHtml, context); - } else { - element = elementOrHtml; - } - - while (element.firstChild) { - firstChild = element.firstChild; - element.removeChild(firstChild); - newNode = _convert(firstChild, cleanUp); - if (newNode) { - fragment.appendChild(newNode); - } - } - - // Clear element contents - element.innerHTML = ""; - - // Insert new DOM tree - element.appendChild(fragment); - - return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element; - } - - function _convert(oldNode, cleanUp) { - var oldNodeType = oldNode.nodeType, - oldChilds = oldNode.childNodes, - oldChildsLength = oldChilds.length, - newNode, - method = NODE_TYPE_MAPPING[oldNodeType], - i = 0; - - newNode = method && method(oldNode); - - if (!newNode) { - return null; - } - - for (i=0; i<oldChildsLength; i++) { - newChild = _convert(oldChilds[i], cleanUp); - if (newChild) { - newNode.appendChild(newChild); - } - } - - // Cleanup senseless <span> elements - if (cleanUp && - newNode.childNodes.length <= 1 && - newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME && - !newNode.attributes.length) { - return newNode.firstChild; - } - - return newNode; - } - - function _handleElement(oldNode) { - var rule, - newNode, - endTag, - tagRules = currentRules.tags, - nodeName = oldNode.nodeName.toLowerCase(), - scopeName = oldNode.scopeName; - - /** - * We already parsed that element - * ignore it! (yes, this sometimes happens in IE8 when the html is invalid) - */ - if (oldNode._wysihtml5) { - return null; - } - oldNode._wysihtml5 = 1; - - if (oldNode.className === "wysihtml5-temp") { - return null; - } - - /** - * IE is the only browser who doesn't include the namespace in the - * nodeName, that's why we have to prepend it by ourselves - * scopeName is a proprietary IE feature - * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx - */ - if (scopeName && scopeName != "HTML") { - nodeName = scopeName + ":" + nodeName; - } - - /** - * Repair node - * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags - * A <p> doesn't need to be closed according HTML4-5 spec, we simply replace it with a <div> to preserve its content and layout - */ - if ("outerHTML" in oldNode) { - if (!wysihtml5.browser.autoClosesUnclosedTags() && - oldNode.nodeName === "P" && - oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") { - nodeName = "div"; - } - } - - if (nodeName in tagRules) { - rule = tagRules[nodeName]; - if (!rule || rule.remove) { - return null; - } - - rule = typeof(rule) === "string" ? { rename_tag: rule } : rule; - } else if (oldNode.firstChild) { - rule = { rename_tag: DEFAULT_NODE_NAME }; - } else { - // Remove empty unknown elements - return null; - } - - newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName); - _handleAttributes(oldNode, newNode, rule); - - oldNode = null; - return newNode; - } - - function _handleAttributes(oldNode, newNode, rule) { - var attributes = {}, // fresh new set of attributes to set on newNode - setClass = rule.set_class, // classes to set - addClass = rule.add_class, // add classes based on existing attributes - setAttributes = rule.set_attributes, // attributes to set on the current node - checkAttributes = rule.check_attributes, // check/convert values of attributes - allowedClasses = currentRules.classes, - allowAllClasses = currentRules.allowAllClasses, - i = 0, - classes = [], - newClasses = [], - newUniqueClasses = [], - oldClasses = [], - classesLength, - newClassesLength, - currentClass, - newClass, - attributeName, - newAttributeValue, - method; - - if (setAttributes) { - attributes = wysihtml5.lang.object(setAttributes).clone(); - } - - if (checkAttributes) { - for (attributeName in checkAttributes) { - method = attributeCheckMethods[checkAttributes[attributeName]]; - if (!method) { - continue; - } - newAttributeValue = method(_getAttribute(oldNode, attributeName)); - if (typeof(newAttributeValue) === "string") { - attributes[attributeName] = newAttributeValue; - } - } - } - - if (setClass) { - classes.push(setClass); - } - - if (addClass) { - for (attributeName in addClass) { - method = addClassMethods[addClass[attributeName]]; - if (!method) { - continue; - } - newClass = method(_getAttribute(oldNode, attributeName)); - if (typeof(newClass) === "string") { - classes.push(newClass); - } - } - } - - // make sure that wysihtml5 temp class doesn't get stripped out - allowedClasses["_wysihtml5-temp-placeholder"] = 1; - - // add old classes last - oldClasses = oldNode.getAttribute("class"); - if (oldClasses) { - classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); - } - classesLength = classes.length; - for (; i<classesLength; i++) { - currentClass = classes[i]; - if (allowedClasses[currentClass] || allowAllClasses) { - newClasses.push(currentClass); - } - } - - // remove duplicate entries and preserve class specificity - newClassesLength = newClasses.length; - while (newClassesLength--) { - currentClass = newClasses[newClassesLength]; - if (!wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) { - newUniqueClasses.unshift(currentClass); - } - } - - if (newUniqueClasses.length) { - attributes["class"] = newUniqueClasses.join(" "); - } - - // set attributes on newNode - for (attributeName in attributes) { - // Setting attributes can cause a js error in IE under certain circumstances - // eg. on a <img> under https when it's new attribute value is non-https - // TODO: Investigate this further and check for smarter handling - try { - newNode.setAttribute(attributeName, attributes[attributeName]); - } catch(e) {} - } - - // IE8 sometimes loses the width/height attributes when those are set before the "src" - // so we make sure to set them again - if (attributes.src) { - if (typeof(attributes.width) !== "undefined") { - newNode.setAttribute("width", attributes.width); - } - if (typeof(attributes.height) !== "undefined") { - newNode.setAttribute("height", attributes.height); - } - } - } - - /** - * IE gives wrong results for hasAttribute/getAttribute, for example: - * var td = document.createElement("td"); - * td.getAttribute("rowspan"); // => "1" in IE - * - * Therefore we have to check the element's outerHTML for the attribute - */ - var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(); - function _getAttribute(node, attributeName) { - attributeName = attributeName.toLowerCase(); - var nodeName = node.nodeName; - if (nodeName == "IMG" && attributeName == "src" && _isLoadedImage(node) === true) { - // Get 'src' attribute value via object property since this will always contain the - // full absolute url (http://...) - // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host - // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url) - return node.src; - } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) { - // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML - var outerHTML = node.outerHTML.toLowerCase(), - // TODO: This might not work for attributes without value: <input disabled> - hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1; - - return hasAttribute ? node.getAttribute(attributeName) : null; - } else{ - return node.getAttribute(attributeName); - } - } - - /** - * Check whether the given node is a proper loaded image - * FIXME: Returns undefined when unknown (Chrome, Safari) - */ - function _isLoadedImage(node) { - try { - return node.complete && !node.mozMatchesSelector(":-moz-broken"); - } catch(e) { - if (node.complete && node.readyState === "complete") { - return true; - } - } - } - - function _handleText(oldNode) { - return oldNode.ownerDocument.createTextNode(oldNode.data); - } - - - // ------------ attribute checks ------------ \\ - var attributeCheckMethods = { - url: (function() { - var REG_EXP = /^https?:\/\//i; - return function(attributeValue) { - if (!attributeValue || !attributeValue.match(REG_EXP)) { - return null; - } - return attributeValue.replace(REG_EXP, function(match) { - return match.toLowerCase(); - }); - }; - })(), - - alt: (function() { - var REG_EXP = /[^ a-z0-9_\-]/gi; - return function(attributeValue) { - if (!attributeValue) { - return ""; - } - return attributeValue.replace(REG_EXP, ""); - }; - })(), - - numbers: (function() { - var REG_EXP = /\D/g; - return function(attributeValue) { - attributeValue = (attributeValue || "").replace(REG_EXP, ""); - return attributeValue || null; - }; - })() - }; - - // ------------ class converter (converts an html attribute to a class name) ------------ \\ - var addClassMethods = { - align_img: (function() { - var mapping = { - left: "wysiwyg-float-left", - right: "wysiwyg-float-right" - }; - return function(attributeValue) { - return mapping[String(attributeValue).toLowerCase()]; - }; - })(), - - align_text: (function() { - var mapping = { - left: "wysiwyg-text-align-left", - right: "wysiwyg-text-align-right", - center: "wysiwyg-text-align-center", - justify: "wysiwyg-text-align-justify" - }; - return function(attributeValue) { - return mapping[String(attributeValue).toLowerCase()]; - }; - })(), - - clear_br: (function() { - var mapping = { - left: "wysiwyg-clear-left", - right: "wysiwyg-clear-right", - both: "wysiwyg-clear-both", - all: "wysiwyg-clear-both" - }; - return function(attributeValue) { - return mapping[String(attributeValue).toLowerCase()]; - }; - })(), - - size_font: (function() { - var mapping = { - "1": "wysiwyg-font-size-xx-small", - "2": "wysiwyg-font-size-small", - "3": "wysiwyg-font-size-medium", - "4": "wysiwyg-font-size-large", - "5": "wysiwyg-font-size-x-large", - "6": "wysiwyg-font-size-xx-large", - "7": "wysiwyg-font-size-xx-large", - "-": "wysiwyg-font-size-smaller", - "+": "wysiwyg-font-size-larger" - }; - return function(attributeValue) { - return mapping[String(attributeValue).charAt(0)]; - }; - })() - }; - - return parse; -})();/** - * Checks for empty text node childs and removes them - * - * @param {Element} node The element in which to cleanup - * @example - * wysihtml5.dom.removeEmptyTextNodes(element); - */ -wysihtml5.dom.removeEmptyTextNodes = function(node) { - var childNode, - childNodes = wysihtml5.lang.array(node.childNodes).get(), - childNodesLength = childNodes.length, - i = 0; - for (; i<childNodesLength; i++) { - childNode = childNodes[i]; - if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") { - childNode.parentNode.removeChild(childNode); - } - } -}; -/** - * Renames an element (eg. a <div> to a <p>) and keeps its childs - * - * @param {Element} element The list element which should be renamed - * @param {Element} newNodeName The desired tag name - * - * @example - * <!-- Assume the following dom: --> - * <ul id="list"> - * <li>eminem</li> - * <li>dr. dre</li> - * <li>50 Cent</li> - * </ul> - * - * <script> - * wysihtml5.dom.renameElement(document.getElementById("list"), "ol"); - * </script> - * - * <!-- Will result in: --> - * <ol> - * <li>eminem</li> - * <li>dr. dre</li> - * <li>50 Cent</li> - * </ol> - */ -wysihtml5.dom.renameElement = function(element, newNodeName) { - var newElement = element.ownerDocument.createElement(newNodeName), - firstChild; - while (firstChild = element.firstChild) { - newElement.appendChild(firstChild); - } - wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement); - element.parentNode.replaceChild(newElement, element); - return newElement; -};/** - * Takes an element, removes it and replaces it with it's childs - * - * @param {Object} node The node which to replace with it's child nodes - * @example - * <div id="foo"> - * <span>hello</span> - * </div> - * <script> - * // Remove #foo and replace with it's children - * wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo")); - * </script> - */ -wysihtml5.dom.replaceWithChildNodes = function(node) { - if (!node.parentNode) { - return; - } - - if (!node.firstChild) { - node.parentNode.removeChild(node); - return; - } - - var fragment = node.ownerDocument.createDocumentFragment(); - while (node.firstChild) { - fragment.appendChild(node.firstChild); - } - node.parentNode.replaceChild(fragment, node); - node = fragment = null; -}; -/** - * Unwraps an unordered/ordered list - * - * @param {Element} element The list element which should be unwrapped - * - * @example - * <!-- Assume the following dom: --> - * <ul id="list"> - * <li>eminem</li> - * <li>dr. dre</li> - * <li>50 Cent</li> - * </ul> - * - * <script> - * wysihtml5.dom.resolveList(document.getElementById("list")); - * </script> - * - * <!-- Will result in: --> - * eminem<br> - * dr. dre<br> - * 50 Cent<br> - */ -(function(dom) { - function _isBlockElement(node) { - return dom.getStyle("display").from(node) === "block"; - } - - function _isLineBreak(node) { - return node.nodeName === "BR"; - } - - function _appendLineBreak(element) { - var lineBreak = element.ownerDocument.createElement("br"); - element.appendChild(lineBreak); - } - - function resolveList(list) { - if (list.nodeName !== "MENU" && list.nodeName !== "UL" && list.nodeName !== "OL") { - return; - } - - var doc = list.ownerDocument, - fragment = doc.createDocumentFragment(), - previousSibling = list.previousElementSibling || list.previousSibling, - firstChild, - lastChild, - isLastChild, - shouldAppendLineBreak, - listItem; - - if (previousSibling && !_isBlockElement(previousSibling)) { - _appendLineBreak(fragment); - } - - while (listItem = list.firstChild) { - lastChild = listItem.lastChild; - while (firstChild = listItem.firstChild) { - isLastChild = firstChild === lastChild; - // This needs to be done before appending it to the fragment, as it otherwise will loose style information - shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild); - fragment.appendChild(firstChild); - if (shouldAppendLineBreak) { - _appendLineBreak(fragment); - } - } - - listItem.parentNode.removeChild(listItem); - } - list.parentNode.replaceChild(fragment, list); - } - - dom.resolveList = resolveList; -})(wysihtml5.dom);/** - * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way - * - * Browser Compatibility: - * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted" - * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...) - * - * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons: - * - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'") - * - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...) - * - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire - * - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe - * can do anything as if the sandbox attribute wasn't set - * - * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready - * @param {Object} [config] Optional parameters - * - * @example - * new wysihtml5.dom.Sandbox(function(sandbox) { - * sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">'; - * }); - */ -(function(wysihtml5) { - var /** - * Default configuration - */ - doc = document, - /** - * Properties to unset/protect on the window object - */ - windowProperties = [ - "parent", "top", "opener", "frameElement", "frames", - "localStorage", "globalStorage", "sessionStorage", "indexedDB" - ], - /** - * Properties on the window object which are set to an empty function - */ - windowProperties2 = [ - "open", "close", "openDialog", "showModalDialog", - "alert", "confirm", "prompt", - "openDatabase", "postMessage", - "XMLHttpRequest", "XDomainRequest" - ], - /** - * Properties to unset/protect on the document object - */ - documentProperties = [ - "referrer", - "write", "open", "close" - ]; - - wysihtml5.dom.Sandbox = Base.extend( - /** @scope wysihtml5.dom.Sandbox.prototype */ { - - constructor: function(readyCallback, config) { - this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION; - this.config = wysihtml5.lang.object({}).merge(config).get(); - this.iframe = this._createIframe(); - }, - - insertInto: function(element) { - if (typeof(element) === "string") { - element = doc.getElementById(element); - } - - element.appendChild(this.iframe); - }, - - getIframe: function() { - return this.iframe; - }, - - getWindow: function() { - this._readyError(); - }, - - getDocument: function() { - this._readyError(); - }, - - destroy: function() { - var iframe = this.getIframe(); - iframe.parentNode.removeChild(iframe); - }, - - _readyError: function() { - throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet"); - }, - - /** - * Creates the sandbox iframe - * - * Some important notes: - * - We can't use HTML5 sandbox for now: - * setting it causes that the iframe's dom can't be accessed from the outside - * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom - * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired. - * In order to make this happen we need to set the "allow-scripts" flag. - * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all. - * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document) - * - IE needs to have the security="restricted" attribute set before the iframe is - * inserted into the dom tree - * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even - * though it supports it - * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore - * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely - * on the onreadystatechange event - */ - _createIframe: function() { - var that = this, - iframe = doc.createElement("iframe"); - iframe.className = "wysihtml5-sandbox"; - wysihtml5.dom.setAttributes({ - "security": "restricted", - "allowtransparency": "true", - "frameborder": 0, - "width": 0, - "height": 0, - "marginwidth": 0, - "marginheight": 0 - }).on(iframe); - - // Setting the src like this prevents ssl warnings in IE6 - if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) { - iframe.src = "javascript:'<html></html>'"; - } - - iframe.onload = function() { - iframe.onreadystatechange = iframe.onload = null; - that._onLoadIframe(iframe); - }; - - iframe.onreadystatechange = function() { - if (/loaded|complete/.test(iframe.readyState)) { - iframe.onreadystatechange = iframe.onload = null; - that._onLoadIframe(iframe); - } - }; - - return iframe; - }, - - /** - * Callback for when the iframe has finished loading - */ - _onLoadIframe: function(iframe) { - // don't resume when the iframe got unloaded (eg. by removing it from the dom) - if (!wysihtml5.dom.contains(doc.documentElement, iframe)) { - return; - } - - var that = this, - iframeWindow = iframe.contentWindow, - iframeDocument = iframe.contentWindow.document, - charset = doc.characterSet || doc.charset || "utf-8", - sandboxHtml = this._getHtml({ - charset: charset, - stylesheets: this.config.stylesheets - }); - - // Create the basic dom tree including proper DOCTYPE and charset - iframeDocument.open("text/html", "replace"); - iframeDocument.write(sandboxHtml); - iframeDocument.close(); - - this.getWindow = function() { return iframe.contentWindow; }; - this.getDocument = function() { return iframe.contentWindow.document; }; - - // Catch js errors and pass them to the parent's onerror event - // addEventListener("error") doesn't work properly in some browsers - // TODO: apparently this doesn't work in IE9! - iframeWindow.onerror = function(errorMessage, fileName, lineNumber) { - throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber); - }; - - if (!wysihtml5.browser.supportsSandboxedIframes()) { - // Unset a bunch of sensitive variables - // Please note: This isn't hack safe! - // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information - // IE is secure though, which is the most important thing, since IE is the only browser, who - // takes over scripts & styles into contentEditable elements when copied from external websites - // or applications (Microsoft Word, ...) - var i, length; - for (i=0, length=windowProperties.length; i<length; i++) { - this._unset(iframeWindow, windowProperties[i]); - } - for (i=0, length=windowProperties2.length; i<length; i++) { - this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION); - } - for (i=0, length=documentProperties.length; i<length; i++) { - this._unset(iframeDocument, documentProperties[i]); - } - // This doesn't work in Safari 5 - // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit - this._unset(iframeDocument, "cookie", "", true); - } - - this.loaded = true; - - // Trigger the callback - setTimeout(function() { that.callback(that); }, 0); - }, - - _getHtml: function(templateVars) { - var stylesheets = templateVars.stylesheets, - html = "", - i = 0, - length; - stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets; - if (stylesheets) { - length = stylesheets.length; - for (; i<length; i++) { - html += '<link rel="stylesheet" href="' + stylesheets[i] + '">'; - } - } - templateVars.stylesheets = html; - - return wysihtml5.lang.string( - '<!DOCTYPE html><html><head>' - + '<meta charset="#{charset}">#{stylesheets}</head>' - + '<body></body></html>' - ).interpolate(templateVars); - }, - - /** - * Method to unset/override existing variables - * @example - * // Make cookie unreadable and unwritable - * this._unset(document, "cookie", "", true); - */ - _unset: function(object, property, value, setter) { - try { object[property] = value; } catch(e) {} - - try { object.__defineGetter__(property, function() { return value; }); } catch(e) {} - if (setter) { - try { object.__defineSetter__(property, function() {}); } catch(e) {} - } - - if (!wysihtml5.browser.crashesWhenDefineProperty(property)) { - try { - var config = { - get: function() { return value; } - }; - if (setter) { - config.set = function() {}; - } - Object.defineProperty(object, property, config); - } catch(e) {} - } - } - }); -})(wysihtml5); -(function() { - var mapping = { - "className": "class" - }; - wysihtml5.dom.setAttributes = function(attributes) { - return { - on: function(element) { - for (var i in attributes) { - element.setAttribute(mapping[i] || i, attributes[i]); - } - } - } - }; -})();wysihtml5.dom.setStyles = function(styles) { - return { - on: function(element) { - var style = element.style; - if (typeof(styles) === "string") { - style.cssText += ";" + styles; - return; - } - for (var i in styles) { - if (i === "float") { - style.cssFloat = styles[i]; - style.styleFloat = styles[i]; - } else { - style[i] = styles[i]; - } - } - } - }; -};/** - * Simulate HTML5 placeholder attribute - * - * Needed since - * - div[contentEditable] elements don't support it - * - older browsers (such as IE8 and Firefox 3.6) don't support it at all - * - * @param {Object} parent Instance of main wysihtml5.Editor class - * @param {Element} view Instance of wysihtml5.views.* class - * @param {String} placeholderText - * - * @example - * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar"); - */ -(function(dom) { - dom.simulatePlaceholder = function(editor, view, placeholderText) { - var CLASS_NAME = "placeholder", - unset = function() { - if (view.hasPlaceholderSet()) { - view.clear(); - } - dom.removeClass(view.element, CLASS_NAME); - }, - set = function() { - if (view.isEmpty()) { - view.setValue(placeholderText); - dom.addClass(view.element, CLASS_NAME); - } - }; - - editor - .observe("set_placeholder", set) - .observe("unset_placeholder", unset) - .observe("focus:composer", unset) - .observe("paste:composer", unset) - .observe("blur:composer", set); - - set(); - }; -})(wysihtml5.dom); -(function(dom) { - var documentElement = document.documentElement; - if ("textContent" in documentElement) { - dom.setTextContent = function(element, text) { - element.textContent = text; - }; - - dom.getTextContent = function(element) { - return element.textContent; - }; - } else if ("innerText" in documentElement) { - dom.setTextContent = function(element, text) { - element.innerText = text; - }; - - dom.getTextContent = function(element) { - return element.innerText; - }; - } else { - dom.setTextContent = function(element, text) { - element.nodeValue = text; - }; - - dom.getTextContent = function(element) { - return element.nodeValue; - }; - } -})(wysihtml5.dom); - -/** - * Fix most common html formatting misbehaviors of browsers implementation when inserting - * content via copy & paste contentEditable - * - * @author Christopher Blum - */ -wysihtml5.quirks.cleanPastedHTML = (function() { - // TODO: We probably need more rules here - var defaultRules = { - // When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling - "a u": wysihtml5.dom.replaceWithChildNodes - }; - - function cleanPastedHTML(elementOrHtml, rules, context) { - rules = rules || defaultRules; - context = context || elementOrHtml.ownerDocument || document; - - var element, - isString = typeof(elementOrHtml) === "string", - method, - matches, - matchesLength, - i, - j = 0; - if (isString) { - element = wysihtml5.dom.getAsDom(elementOrHtml, context); - } else { - element = elementOrHtml; - } - - for (i in rules) { - matches = element.querySelectorAll(i); - method = rules[i]; - matchesLength = matches.length; - for (; j<matchesLength; j++) { - method(matches[j]); - } - } - - matches = elementOrHtml = rules = null; - - return isString ? element.innerHTML : element; - } - - return cleanPastedHTML; -})();/** - * IE and Opera leave an empty paragraph in the contentEditable element after clearing it - * - * @param {Object} contentEditableElement The contentEditable element to observe for clearing events - * @exaple - * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); - */ -(function(wysihtml5) { - var dom = wysihtml5.dom; - - wysihtml5.quirks.ensureProperClearing = (function() { - var clearIfNecessary = function(event) { - var element = this; - setTimeout(function() { - var innerHTML = element.innerHTML.toLowerCase(); - if (innerHTML == "<p>&nbsp;</p>" || - innerHTML == "<p>&nbsp;</p><p>&nbsp;</p>") { - element.innerHTML = ""; - } - }, 0); - }; - - return function(composer) { - dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary); - }; - })(); - - - - /** - * In Opera when the caret is in the first and only item of a list (<ul><li>|</li></ul>) and the list is the first child of the contentEditable element, it's impossible to delete the list by hitting backspace - * - * @param {Object} contentEditableElement The contentEditable element to observe for clearing events - * @exaple - * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); - */ - wysihtml5.quirks.ensureProperClearingOfLists = (function() { - var ELEMENTS_THAT_CONTAIN_LI = ["OL", "UL", "MENU"]; - - var clearIfNecessary = function(element, contentEditableElement) { - if (!contentEditableElement.firstChild || !wysihtml5.lang.array(ELEMENTS_THAT_CONTAIN_LI).contains(contentEditableElement.firstChild.nodeName)) { - return; - } - - var list = dom.getParentElement(element, { nodeName: ELEMENTS_THAT_CONTAIN_LI }); - if (!list) { - return; - } - - var listIsFirstChildOfContentEditable = list == contentEditableElement.firstChild; - if (!listIsFirstChildOfContentEditable) { - return; - } - - var hasOnlyOneListItem = list.childNodes.length <= 1; - if (!hasOnlyOneListItem) { - return; - } - - var onlyListItemIsEmpty = list.firstChild ? list.firstChild.innerHTML === "" : true; - if (!onlyListItemIsEmpty) { - return; - } - - list.parentNode.removeChild(list); - }; - - return function(composer) { - dom.observe(composer.element, "keydown", function(event) { - if (event.keyCode !== wysihtml5.BACKSPACE_KEY) { - return; - } - - var element = composer.selection.getSelectedNode(); - clearIfNecessary(element, composer.element); - }); - }; - })(); - -})(wysihtml5); -// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398 -// -// In Firefox this: -// var d = document.createElement("div"); -// d.innerHTML ='<a href="~"></a>'; -// d.innerHTML; -// will result in: -// <a href="%7E"></a> -// which is wrong -(function(wysihtml5) { - var TILDE_ESCAPED = "%7E"; - wysihtml5.quirks.getCorrectInnerHTML = function(element) { - var innerHTML = element.innerHTML; - if (innerHTML.indexOf(TILDE_ESCAPED) === -1) { - return innerHTML; - } - - var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"), - url, - urlToSearch, - length, - i; - for (i=0, length=elementsWithTilde.length; i<length; i++) { - url = elementsWithTilde[i].href || elementsWithTilde[i].src; - urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED); - innerHTML = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url); - } - return innerHTML; - }; -})(wysihtml5);/** - * Some browsers don't insert line breaks when hitting return in a contentEditable element - * - Opera & IE insert new <p> on return - * - Chrome & Safari insert new <div> on return - * - Firefox inserts <br> on return (yippie!) - * - * @param {Element} element - * - * @example - * wysihtml5.quirks.insertLineBreakOnReturn(element); - */ -(function(wysihtml5) { - var dom = wysihtml5.dom, - USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"], - LIST_TAGS = ["UL", "OL", "MENU"]; - - wysihtml5.quirks.insertLineBreakOnReturn = function(composer) { - function unwrap(selectedNode) { - var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2); - if (!parentElement) { - return; - } - - var invisibleSpace = document.createTextNode(wysihtml5.INVISIBLE_SPACE); - dom.insert(invisibleSpace).before(parentElement); - dom.replaceWithChildNodes(parentElement); - composer.selection.selectNode(invisibleSpace); - } - - function keyDown(event) { - var keyCode = event.keyCode; - if (event.shiftKey || (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY)) { - return; - } - - var element = event.target, - selectedNode = composer.selection.getSelectedNode(), - blockElement = dom.getParentElement(selectedNode, { nodeName: USE_NATIVE_LINE_BREAK_WHEN_CARET_INSIDE_TAGS }, 4); - if (blockElement) { - // Some browsers create <p> elements after leaving a list - // check after keydown of backspace and return whether a <p> got inserted and unwrap it - if (blockElement.nodeName === "LI" && (keyCode === wysihtml5.ENTER_KEY || keyCode === wysihtml5.BACKSPACE_KEY)) { - setTimeout(function() { - var selectedNode = composer.selection.getSelectedNode(), - list, - div; - if (!selectedNode) { - return; - } - - list = dom.getParentElement(selectedNode, { - nodeName: LIST_TAGS - }, 2); - - if (list) { - return; - } - - unwrap(selectedNode); - }, 0); - } else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === wysihtml5.ENTER_KEY) { - setTimeout(function() { - unwrap(composer.selection.getSelectedNode()); - }, 0); - } - return; - } - - if (keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) { - composer.commands.exec("insertLineBreak"); - event.preventDefault(); - } - } - - // keypress doesn't fire when you hit backspace - dom.observe(composer.element.ownerDocument, "keydown", keyDown); - }; -})(wysihtml5);/** - * Force rerendering of a given element - * Needed to fix display misbehaviors of IE - * - * @param {Element} element The element object which needs to be rerendered - * @example - * wysihtml5.quirks.redraw(document.body); - */ -(function(wysihtml5) { - var CLASS_NAME = "wysihtml5-quirks-redraw"; - - wysihtml5.quirks.redraw = function(element) { - wysihtml5.dom.addClass(element, CLASS_NAME); - wysihtml5.dom.removeClass(element, CLASS_NAME); - - // Following hack is needed for firefox to make sure that image resize handles are properly removed - try { - var doc = element.ownerDocument; - doc.execCommand("italic", false, null); - doc.execCommand("italic", false, null); - } catch(e) {} - }; -})(wysihtml5);/** - * Selection API - * - * @example - * var selection = new wysihtml5.Selection(editor); - */ -(function(wysihtml5) { - var dom = wysihtml5.dom; - - function _getCumulativeOffsetTop(element) { - var top = 0; - if (element.parentNode) { - do { - top += element.offsetTop || 0; - element = element.offsetParent; - } while (element); - } - return top; - } - - wysihtml5.Selection = Base.extend( - /** @scope wysihtml5.Selection.prototype */ { - constructor: function(editor) { - // Make sure that our external range library is initialized - window.rangy.init(); - - this.editor = editor; - this.composer = editor.composer; - this.doc = this.composer.doc; - }, - - /** - * Get the current selection as a bookmark to be able to later restore it - * - * @return {Object} An object that represents the current selection - */ - getBookmark: function() { - var range = this.getRange(); - return range && range.cloneRange(); - }, - - /** - * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark - * - * @param {Object} bookmark An object that represents the current selection - */ - setBookmark: function(bookmark) { - if (!bookmark) { - return; - } - - this.setSelection(bookmark); - }, - - /** - * Set the caret in front of the given node - * - * @param {Object} node The element or text node where to position the caret in front of - * @example - * selection.setBefore(myElement); - */ - setBefore: function(node) { - var range = rangy.createRange(this.doc); - range.setStartBefore(node); - range.setEndBefore(node); - return this.setSelection(range); - }, - - /** - * Set the caret after the given node - * - * @param {Object} node The element or text node where to position the caret in front of - * @example - * selection.setBefore(myElement); - */ - setAfter: function(node) { - var range = rangy.createRange(this.doc); - range.setStartAfter(node); - range.setEndAfter(node); - return this.setSelection(range); - }, - - /** - * Ability to select/mark nodes - * - * @param {Element} node The node/element to select - * @example - * selection.selectNode(document.getElementById("my-image")); - */ - selectNode: function(node) { - var range = rangy.createRange(this.doc), - isElement = node.nodeType === wysihtml5.ELEMENT_NODE, - canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"), - content = isElement ? node.innerHTML : node.data, - isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE), - displayStyle = dom.getStyle("display").from(node), - isBlockElement = (displayStyle === "block" || displayStyle === "list-item"); - - if (isEmpty && isElement && canHaveHTML) { - // Make sure that caret is visible in node by inserting a zero width no breaking space - try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} - } - - if (canHaveHTML) { - range.selectNodeContents(node); - } else { - range.selectNode(node); - } - - if (canHaveHTML && isEmpty && isElement) { - range.collapse(isBlockElement); - } else if (canHaveHTML && isEmpty) { - range.setStartAfter(node); - range.setEndAfter(node); - } - - this.setSelection(range); - }, - - /** - * Get the node which contains the selection - * - * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange" - * @return {Object} The node that contains the caret - * @example - * var nodeThatContainsCaret = selection.getSelectedNode(); - */ - getSelectedNode: function(controlRange) { - var selection, - range; - - if (controlRange && this.doc.selection && this.doc.selection.type === "Control") { - range = this.doc.selection.createRange(); - if (range && range.length) { - return range.item(0); - } - } - - selection = this.getSelection(this.doc); - if (selection.focusNode === selection.anchorNode) { - return selection.focusNode; - } else { - range = this.getRange(this.doc); - return range ? range.commonAncestorContainer : this.doc.body; - } - }, - - executeAndRestore: function(method, restoreScrollPosition) { - var body = this.doc.body, - oldScrollTop = restoreScrollPosition && body.scrollTop, - oldScrollLeft = restoreScrollPosition && body.scrollLeft, - className = "_wysihtml5-temp-placeholder", - placeholderHTML = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>', - range = this.getRange(this.doc), - newRange; - - // Nothing selected, execute and say goodbye - if (!range) { - method(body, body); - return; - } - - var node = range.createContextualFragment(placeholderHTML); - range.insertNode(node); - - // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder - try { - method(range.startContainer, range.endContainer); - } catch(e3) { - setTimeout(function() { throw e3; }, 0); - } - - caretPlaceholder = this.doc.querySelector("." + className); - if (caretPlaceholder) { - newRange = rangy.createRange(this.doc); - newRange.selectNode(caretPlaceholder); - newRange.deleteContents(); - this.setSelection(newRange); - } else { - // fallback for when all hell breaks loose - body.focus(); - } - - if (restoreScrollPosition) { - body.scrollTop = oldScrollTop; - body.scrollLeft = oldScrollLeft; - } - - // Remove it again, just to make sure that the placeholder is definitely out of the dom tree - try { - caretPlaceholder.parentNode.removeChild(caretPlaceholder); - } catch(e4) {} - }, - - /** - * Different approach of preserving the selection (doesn't modify the dom) - * Takes all text nodes in the selection and saves the selection position in the first and last one - */ - executeAndRestoreSimple: function(method) { - var range = this.getRange(), - body = this.doc.body, - newRange, - firstNode, - lastNode, - textNodes, - rangeBackup; - - // Nothing selected, execute and say goodbye - if (!range) { - method(body, body); - return; - } - - textNodes = range.getNodes([3]); - firstNode = textNodes[0] || range.startContainer; - lastNode = textNodes[textNodes.length - 1] || range.endContainer; - - rangeBackup = { - collapsed: range.collapsed, - startContainer: firstNode, - startOffset: firstNode === range.startContainer ? range.startOffset : 0, - endContainer: lastNode, - endOffset: lastNode === range.endContainer ? range.endOffset : lastNode.length - }; - - try { - method(range.startContainer, range.endContainer); - } catch(e) { - setTimeout(function() { throw e; }, 0); - } - - newRange = rangy.createRange(this.doc); - try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {} - try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {} - try { this.setSelection(newRange); } catch(e3) {} - }, - - /** - * Insert html at the caret position and move the cursor after the inserted html - * - * @param {String} html HTML string to insert - * @example - * selection.insertHTML("<p>foobar</p>"); - */ - insertHTML: function(html) { - var range = rangy.createRange(this.doc), - node = range.createContextualFragment(html), - lastChild = node.lastChild; - this.insertNode(node); - if (lastChild) { - this.setAfter(lastChild); - } - }, - - /** - * Insert a node at the caret position and move the cursor behind it - * - * @param {Object} node HTML string to insert - * @example - * selection.insertNode(document.createTextNode("foobar")); - */ - insertNode: function(node) { - var range = this.getRange(); - if (range) { - range.insertNode(node); - } - }, - - /** - * Wraps current selection with the given node - * - * @param {Object} node The node to surround the selected elements with - */ - surround: function(node) { - var range = this.getRange(); - if (!range) { - return; - } - - try { - // This only works when the range boundaries are not overlapping other elements - range.surroundContents(node); - this.selectNode(node); - } catch(e) { - // fallback - node.appendChild(range.extractContents()); - range.insertNode(node); - } - }, - - /** - * Scroll the current caret position into the view - * FIXME: This is a bit hacky, there might be a smarter way of doing this - * - * @example - * selection.scrollIntoView(); - */ - scrollIntoView: function() { - var doc = this.doc, - hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight, - tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() { - var element = doc.createElement("span"); - // The element needs content in order to be able to calculate it's position properly - element.innerHTML = wysihtml5.INVISIBLE_SPACE; - return element; - })(), - offsetTop; - - if (hasScrollBars) { - this.insertNode(tempElement); - offsetTop = _getCumulativeOffsetTop(tempElement); - tempElement.parentNode.removeChild(tempElement); - if (offsetTop > doc.body.scrollTop) { - doc.body.scrollTop = offsetTop; - } - } - }, - - /** - * Select line where the caret is in - */ - selectLine: function() { - if (wysihtml5.browser.supportsSelectionModify()) { - this._selectLine_W3C(); - } else if (this.doc.selection) { - this._selectLine_MSIE(); - } - }, - - /** - * See https://developer.mozilla.org/en/DOM/Selection/modify - */ - _selectLine_W3C: function() { - var win = this.doc.defaultView, - selection = win.getSelection(); - selection.modify("extend", "left", "lineboundary"); - selection.modify("extend", "right", "lineboundary"); - }, - - _selectLine_MSIE: function() { - var range = this.doc.selection.createRange(), - rangeTop = range.boundingTop, - rangeHeight = range.boundingHeight, - scrollWidth = this.doc.body.scrollWidth, - rangeBottom, - rangeEnd, - measureNode, - i, - j; - - if (!range.moveToPoint) { - return; - } - - if (rangeTop === 0) { - // Don't know why, but when the selection ends at the end of a line - // range.boundingTop is 0 - measureNode = this.doc.createElement("span"); - this.insertNode(measureNode); - rangeTop = measureNode.offsetTop; - measureNode.parentNode.removeChild(measureNode); - } - - rangeTop += 1; - - for (i=-10; i<scrollWidth; i+=2) { - try { - range.moveToPoint(i, rangeTop); - break; - } catch(e1) {} - } - - // Investigate the following in order to handle multi line selections - // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0); - rangeBottom = rangeTop; - rangeEnd = this.doc.selection.createRange(); - for (j=scrollWidth; j>=0; j--) { - try { - rangeEnd.moveToPoint(j, rangeBottom); - break; - } catch(e2) {} - } - - range.setEndPoint("EndToEnd", rangeEnd); - range.select(); - }, - - getText: function() { - var selection = this.getSelection(); - return selection ? selection.toString() : ""; - }, - - getNodes: function(nodeType, filter) { - var range = this.getRange(); - if (range) { - return range.getNodes([nodeType], filter); - } else { - return []; - } - }, - - getRange: function() { - var selection = this.getSelection(); - return selection && selection.rangeCount && selection.getRangeAt(0); - }, - - getSelection: function() { - return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow); - }, - - setSelection: function(range) { - var win = this.doc.defaultView || this.doc.parentWindow, - selection = rangy.getSelection(win); - return selection.setSingleRange(range); - } - }); - -})(wysihtml5); -/** - * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license. - * http://code.google.com/p/rangy/ - * - * changed in order to be able ... - * - to use custom tags - * - to detect and replace similar css classes via reg exp - */ -(function(wysihtml5, rangy) { - var defaultTagName = "span"; - - var REG_EXP_WHITE_SPACE = /\s+/g; - - function hasClass(el, cssClass, regExp) { - if (!el.className) { - return false; - } - - var matchingClassNames = el.className.match(regExp) || []; - return matchingClassNames[matchingClassNames.length - 1] === cssClass; - } - - function addClass(el, cssClass, regExp) { - if (el.className) { - removeClass(el, regExp); - el.className += " " + cssClass; - } else { - el.className = cssClass; - } - } - - function removeClass(el, regExp) { - if (el.className) { - el.className = el.className.replace(regExp, ""); - } - } - - function hasSameClasses(el1, el2) { - return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " "); - } - - function replaceWithOwnChildren(el) { - var parent = el.parentNode; - while (el.firstChild) { - parent.insertBefore(el.firstChild, el); - } - parent.removeChild(el); - } - - function elementsHaveSameNonClassAttributes(el1, el2) { - if (el1.attributes.length != el2.attributes.length) { - return false; - } - for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { - attr1 = el1.attributes[i]; - name = attr1.name; - if (name != "class") { - attr2 = el2.attributes.getNamedItem(name); - if (attr1.specified != attr2.specified) { - return false; - } - if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) { - return false; - } - } - } - return true; - } - - function isSplitPoint(node, offset) { - if (rangy.dom.isCharacterDataNode(node)) { - if (offset == 0) { - return !!node.previousSibling; - } else if (offset == node.length) { - return !!node.nextSibling; - } else { - return true; - } - } - - return offset > 0 && offset < node.childNodes.length; - } - - function splitNodeAt(node, descendantNode, descendantOffset) { - var newNode; - if (rangy.dom.isCharacterDataNode(descendantNode)) { - if (descendantOffset == 0) { - descendantOffset = rangy.dom.getNodeIndex(descendantNode); - descendantNode = descendantNode.parentNode; - } else if (descendantOffset == descendantNode.length) { - descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1; - descendantNode = descendantNode.parentNode; - } else { - newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset); - } - } - if (!newNode) { - newNode = descendantNode.cloneNode(false); - if (newNode.id) { - newNode.removeAttribute("id"); - } - var child; - while ((child = descendantNode.childNodes[descendantOffset])) { - newNode.appendChild(child); - } - rangy.dom.insertAfter(newNode, descendantNode); - } - return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode)); - } - - function Merge(firstNode) { - this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE); - this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; - this.textNodes = [this.firstTextNode]; - } - - Merge.prototype = { - doMerge: function() { - var textBits = [], textNode, parent, text; - for (var i = 0, len = this.textNodes.length; i < len; ++i) { - textNode = this.textNodes[i]; - parent = textNode.parentNode; - textBits[i] = textNode.data; - if (i) { - parent.removeChild(textNode); - if (!parent.hasChildNodes()) { - parent.parentNode.removeChild(parent); - } - } - } - this.firstTextNode.data = text = textBits.join(""); - return text; - }, - - getLength: function() { - var i = this.textNodes.length, len = 0; - while (i--) { - len += this.textNodes[i].length; - } - return len; - }, - - toString: function() { - var textBits = []; - for (var i = 0, len = this.textNodes.length; i < len; ++i) { - textBits[i] = "'" + this.textNodes[i].data + "'"; - } - return "[Merge(" + textBits.join(",") + ")]"; - } - }; - - function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize) { - this.tagNames = tagNames || [defaultTagName]; - this.cssClass = cssClass || ""; - this.similarClassRegExp = similarClassRegExp; - this.normalize = normalize; - this.applyToAnyTagName = false; - } - - HTMLApplier.prototype = { - getAncestorWithClass: function(node) { - var cssClassMatch; - while (node) { - cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : true; - if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) { - return node; - } - node = node.parentNode; - } - return false; - }, - - // Normalizes nodes after applying a CSS class to a Range. - postApply: function(textNodes, range) { - var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; - - var merges = [], currentMerge; - - var rangeStartNode = firstNode, rangeEndNode = lastNode; - var rangeStartOffset = 0, rangeEndOffset = lastNode.length; - - var textNode, precedingTextNode; - - for (var i = 0, len = textNodes.length; i < len; ++i) { - textNode = textNodes[i]; - precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false); - if (precedingTextNode) { - if (!currentMerge) { - currentMerge = new Merge(precedingTextNode); - merges.push(currentMerge); - } - currentMerge.textNodes.push(textNode); - if (textNode === firstNode) { - rangeStartNode = currentMerge.firstTextNode; - rangeStartOffset = rangeStartNode.length; - } - if (textNode === lastNode) { - rangeEndNode = currentMerge.firstTextNode; - rangeEndOffset = currentMerge.getLength(); - } - } else { - currentMerge = null; - } - } - - // Test whether the first node after the range needs merging - var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true); - if (nextTextNode) { - if (!currentMerge) { - currentMerge = new Merge(lastNode); - merges.push(currentMerge); - } - currentMerge.textNodes.push(nextTextNode); - } - - // Do the merges - if (merges.length) { - for (i = 0, len = merges.length; i < len; ++i) { - merges[i].doMerge(); - } - // Set the range boundaries - range.setStart(rangeStartNode, rangeStartOffset); - range.setEnd(rangeEndNode, rangeEndOffset); - } - }, - - getAdjacentMergeableTextNode: function(node, forward) { - var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE); - var el = isTextNode ? node.parentNode : node; - var adjacentNode; - var propName = forward ? "nextSibling" : "previousSibling"; - if (isTextNode) { - // Can merge if the node's previous/next sibling is a text node - adjacentNode = node[propName]; - if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) { - return adjacentNode; - } - } else { - // Compare element with its sibling - adjacentNode = el[propName]; - if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) { - return adjacentNode[forward ? "firstChild" : "lastChild"]; - } - } - return null; - }, - - areElementsMergeable: function(el1, el2) { - return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase()) - && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase()) - && hasSameClasses(el1, el2) - && elementsHaveSameNonClassAttributes(el1, el2); - }, - - createContainer: function(doc) { - var el = doc.createElement(this.tagNames[0]); - if (this.cssClass) { - el.className = this.cssClass; - } - return el; - }, - - applyToTextNode: function(textNode) { - var parent = textNode.parentNode; - if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) { - if (this.cssClass) { - addClass(parent, this.cssClass, this.similarClassRegExp); - } - } else { - var el = this.createContainer(rangy.dom.getDocument(textNode)); - textNode.parentNode.insertBefore(el, textNode); - el.appendChild(textNode); - } - }, - - isRemovable: function(el) { - return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && wysihtml5.lang.string(el.className).trim() == this.cssClass; - }, - - undoToTextNode: function(textNode, range, ancestorWithClass) { - if (!range.containsNode(ancestorWithClass)) { - // Split out the portion of the ancestor from which we can remove the CSS class - var ancestorRange = range.cloneRange(); - ancestorRange.selectNode(ancestorWithClass); - - if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) { - splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset); - range.setEndAfter(ancestorWithClass); - } - if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) { - ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset); - } - } - - if (this.similarClassRegExp) { - removeClass(ancestorWithClass, this.similarClassRegExp); - } - if (this.isRemovable(ancestorWithClass)) { - replaceWithOwnChildren(ancestorWithClass); - } - }, - - applyToRange: function(range) { - var textNodes = range.getNodes([wysihtml5.TEXT_NODE]); - if (!textNodes.length) { - try { - var node = this.createContainer(range.endContainer.ownerDocument); - range.surroundContents(node); - this.selectNode(range, node); - return; - } catch(e) {} - } - - range.splitBoundaries(); - textNodes = range.getNodes([wysihtml5.TEXT_NODE]); - - if (textNodes.length) { - var textNode; - - for (var i = 0, len = textNodes.length; i < len; ++i) { - textNode = textNodes[i]; - if (!this.getAncestorWithClass(textNode)) { - this.applyToTextNode(textNode); - } - } - - range.setStart(textNodes[0], 0); - textNode = textNodes[textNodes.length - 1]; - range.setEnd(textNode, textNode.length); - - if (this.normalize) { - this.postApply(textNodes, range); - } - } - }, - - undoToRange: function(range) { - var textNodes = range.getNodes([wysihtml5.TEXT_NODE]), textNode, ancestorWithClass; - if (textNodes.length) { - range.splitBoundaries(); - textNodes = range.getNodes([wysihtml5.TEXT_NODE]); - } else { - var doc = range.endContainer.ownerDocument, - node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); - range.insertNode(node); - range.selectNode(node); - textNodes = [node]; - } - - for (var i = 0, len = textNodes.length; i < len; ++i) { - textNode = textNodes[i]; - ancestorWithClass = this.getAncestorWithClass(textNode); - if (ancestorWithClass) { - this.undoToTextNode(textNode, range, ancestorWithClass); - } - } - - if (len == 1) { - this.selectNode(range, textNodes[0]); - } else { - range.setStart(textNodes[0], 0); - textNode = textNodes[textNodes.length - 1]; - range.setEnd(textNode, textNode.length); - - if (this.normalize) { - this.postApply(textNodes, range); - } - } - }, - - selectNode: function(range, node) { - var isElement = node.nodeType === wysihtml5.ELEMENT_NODE, - canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true, - content = isElement ? node.innerHTML : node.data, - isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE); - - if (isEmpty && isElement && canHaveHTML) { - // Make sure that caret is visible in node by inserting a zero width no breaking space - try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} - } - range.selectNodeContents(node); - if (isEmpty && isElement) { - range.collapse(false); - } else if (isEmpty) { - range.setStartAfter(node); - range.setEndAfter(node); - } - }, - - getTextSelectedByRange: function(textNode, range) { - var textRange = range.cloneRange(); - textRange.selectNodeContents(textNode); - - var intersectionRange = textRange.intersection(range); - var text = intersectionRange ? intersectionRange.toString() : ""; - textRange.detach(); - - return text; - }, - - isAppliedToRange: function(range) { - var ancestors = [], - ancestor, - textNodes = range.getNodes([wysihtml5.TEXT_NODE]); - if (!textNodes.length) { - ancestor = this.getAncestorWithClass(range.startContainer); - return ancestor ? [ancestor] : false; - } - - for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) { - selectedText = this.getTextSelectedByRange(textNodes[i], range); - ancestor = this.getAncestorWithClass(textNodes[i]); - if (selectedText != "" && !ancestor) { - return false; - } else { - ancestors.push(ancestor); - } - } - return ancestors; - }, - - toggleRange: function(range) { - if (this.isAppliedToRange(range)) { - this.undoToRange(range); - } else { - this.applyToRange(range); - } - } - }; - - wysihtml5.selection.HTMLApplier = HTMLApplier; - -})(wysihtml5, rangy);/** - * Rich Text Query/Formatting Commands - * - * @example - * var commands = new wysihtml5.Commands(editor); - */ -wysihtml5.Commands = Base.extend( - /** @scope wysihtml5.Commands.prototype */ { - constructor: function(editor) { - this.editor = editor; - this.composer = editor.composer; - this.doc = this.composer.doc; - }, - - /** - * Check whether the browser supports the given command - * - * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") - * @example - * commands.supports("createLink"); - */ - support: function(command) { - return wysihtml5.browser.supportsCommand(this.doc, command); - }, - - /** - * Check whether the browser supports the given command - * - * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList") - * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...) - * @example - * commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg"); - */ - exec: function(command, value) { - var obj = wysihtml5.commands[command], - args = wysihtml5.lang.array(arguments).get(), - method = obj && obj.exec, - result = null; - - this.editor.fire("beforecommand:composer"); - - if (method) { - args.unshift(this.composer); - result = method.apply(obj, args); - } else { - try { - // try/catch for buggy firefox - result = this.doc.execCommand(command, false, value); - } catch(e) {} - } - - this.editor.fire("aftercommand:composer"); - return result; - }, - - /** - * Check whether the current command is active - * If the caret is within a bold text, then calling this with command "bold" should return true - * - * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") - * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src) - * @return {Boolean} Whether the command is active - * @example - * var isCurrentSelectionBold = commands.state("bold"); - */ - state: function(command, commandValue) { - var obj = wysihtml5.commands[command], - args = wysihtml5.lang.array(arguments).get(), - method = obj && obj.state; - if (method) { - args.unshift(this.composer); - return method.apply(obj, args); - } else { - try { - // try/catch for buggy firefox - return this.doc.queryCommandState(command); - } catch(e) { - return false; - } - } - }, - - /** - * Get the current command's value - * - * @param {String} command The command string which to check (eg. "formatBlock") - * @return {String} The command value - * @example - * var currentBlockElement = commands.value("formatBlock"); - */ - value: function(command) { - var obj = wysihtml5.commands[command], - method = obj && obj.value; - if (method) { - return method.call(obj, this.composer, command); - } else { - try { - // try/catch for buggy firefox - return this.doc.queryCommandValue(command); - } catch(e) { - return null; - } - } - } -}); -(function(wysihtml5) { - var undef; - - wysihtml5.commands.bold = { - exec: function(composer, command) { - return wysihtml5.commands.formatInline.exec(composer, command, "b"); - }, - - state: function(composer, command, color) { - // element.ownerDocument.queryCommandState("bold") results: - // firefox: only <b> - // chrome: <b>, <strong>, <h1>, <h2>, ... - // ie: <b>, <strong> - // opera: <b>, <strong> - return wysihtml5.commands.formatInline.state(composer, command, "b"); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5); - -(function(wysihtml5) { - var undef, - NODE_NAME = "A", - dom = wysihtml5.dom; - - function _removeFormat(composer, anchors) { - var length = anchors.length, - i = 0, - anchor, - codeElement, - textContent; - for (; i<length; i++) { - anchor = anchors[i]; - codeElement = dom.getParentElement(anchor, { nodeName: "code" }); - textContent = dom.getTextContent(anchor); - - // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking - // else replace <a> with its childNodes - if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) { - // <code> element is used to prevent later auto-linking of the content - codeElement = dom.renameElement(anchor, "code"); - } else { - dom.replaceWithChildNodes(anchor); - } - } - } - - function _format(composer, attributes) { - var doc = composer.doc, - tempClass = "_wysihtml5-temp-" + (+new Date()), - tempClassRegExp = /non-matching-class/g, - i = 0, - length, - anchors, - anchor, - hasElementChild, - isEmpty, - elementToSetCaretAfter, - textContent, - whiteSpace, - j; - wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp); - anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass); - length = anchors.length; - for (; i<length; i++) { - anchor = anchors[i]; - anchor.removeAttribute("class"); - for (j in attributes) { - anchor.setAttribute(j, attributes[j]); - } - } - - elementToSetCaretAfter = anchor; - if (length === 1) { - textContent = dom.getTextContent(anchor); - hasElementChild = !!anchor.querySelector("*"); - isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE; - if (!hasElementChild && isEmpty) { - dom.setTextContent(anchor, attributes.text || anchor.href); - whiteSpace = doc.createTextNode(" "); - composer.selection.setAfter(anchor); - composer.selection.insertNode(whiteSpace); - elementToSetCaretAfter = whiteSpace; - } - } - composer.selection.setAfter(elementToSetCaretAfter); - } - - wysihtml5.commands.createLink = { - /** - * TODO: Use HTMLApplier or formatInline here - * - * Turns selection into a link - * If selection is already a link, it removes the link and wraps it with a <code> element - * The <code> element is needed to avoid auto linking - * - * @example - * // either ... - * wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de"); - * // ... or ... - * wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" }); - */ - exec: function(composer, command, value) { - var anchors = this.state(composer, command); - if (anchors) { - // Selection contains links - composer.selection.executeAndRestore(function() { - _removeFormat(composer, anchors); - }); - } else { - // Create links - value = typeof(value) === "object" ? value : { href: value }; - _format(composer, value); - } - }, - - state: function(composer, command) { - return wysihtml5.commands.formatInline.state(composer, command, "A"); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);/** - * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags - * which we don't want - * Instead we set a css class - */ -(function(wysihtml5) { - var undef, - REG_EXP = /wysiwyg-font-size-[a-z\-]+/g; - - wysihtml5.commands.fontSize = { - exec: function(composer, command, size) { - return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP); - }, - - state: function(composer, command, size) { - return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5); -/** - * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags - * which we don't want - * Instead we set a css class - */ -(function(wysihtml5) { - var undef, - REG_EXP = /wysiwyg-color-[a-z]+/g; - - wysihtml5.commands.foreColor = { - exec: function(composer, command, color) { - return wysihtml5.commands.formatInline.exec(composer, command, "span", "wysiwyg-color-" + color, REG_EXP); - }, - - state: function(composer, command, color) { - return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);(function(wysihtml5) { - var undef, - dom = wysihtml5.dom, - DEFAULT_NODE_NAME = "DIV", - // Following elements are grouped - // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4 - // instead of creating a H4 within a H1 which would result in semantically invalid html - BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", DEFAULT_NODE_NAME]; - - /** - * Remove similiar classes (based on classRegExp) - * and add the desired class name - */ - function _addClass(element, className, classRegExp) { - if (element.className) { - _removeClass(element, classRegExp); - element.className += " " + className; - } else { - element.className = className; - } - } - - function _removeClass(element, classRegExp) { - element.className = element.className.replace(classRegExp, ""); - } - - /** - * Check whether given node is a text node and whether it's empty - */ - function _isBlankTextNode(node) { - return node.nodeType === wysihtml5.TEXT_NODE && !wysihtml5.lang.string(node.data).trim(); - } - - /** - * Returns previous sibling node that is not a blank text node - */ - function _getPreviousSiblingThatIsNotBlank(node) { - var previousSibling = node.previousSibling; - while (previousSibling && _isBlankTextNode(previousSibling)) { - previousSibling = previousSibling.previousSibling; - } - return previousSibling; - } - - /** - * Returns next sibling node that is not a blank text node - */ - function _getNextSiblingThatIsNotBlank(node) { - var nextSibling = node.nextSibling; - while (nextSibling && _isBlankTextNode(nextSibling)) { - nextSibling = nextSibling.nextSibling; - } - return nextSibling; - } - - /** - * Adds line breaks before and after the given node if the previous and next siblings - * aren't already causing a visual line break (block element or <br>) - */ - function _addLineBreakBeforeAndAfter(node) { - var doc = node.ownerDocument, - nextSibling = _getNextSiblingThatIsNotBlank(node), - previousSibling = _getPreviousSiblingThatIsNotBlank(node); - - if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) { - node.parentNode.insertBefore(doc.createElement("br"), nextSibling); - } - if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) { - node.parentNode.insertBefore(doc.createElement("br"), node); - } - } - - /** - * Removes line breaks before and after the given node - */ - function _removeLineBreakBeforeAndAfter(node) { - var nextSibling = _getNextSiblingThatIsNotBlank(node), - previousSibling = _getPreviousSiblingThatIsNotBlank(node); - - if (nextSibling && _isLineBreak(nextSibling)) { - nextSibling.parentNode.removeChild(nextSibling); - } - if (previousSibling && _isLineBreak(previousSibling)) { - previousSibling.parentNode.removeChild(previousSibling); - } - } - - function _removeLastChildIfLineBreak(node) { - var lastChild = node.lastChild; - if (lastChild && _isLineBreak(lastChild)) { - lastChild.parentNode.removeChild(lastChild); - } - } - - function _isLineBreak(node) { - return node.nodeName === "BR"; - } - - /** - * Checks whether the elment causes a visual line break - * (<br> or block elements) - */ - function _isLineBreakOrBlockElement(element) { - if (_isLineBreak(element)) { - return true; - } - - if (dom.getStyle("display").from(element) === "block") { - return true; - } - - return false; - } - - /** - * Execute native query command - * and if necessary modify the inserted node's className - */ - function _execCommand(doc, command, nodeName, className) { - if (className) { - var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) { - var target = event.target, - displayStyle; - if (target.nodeType !== wysihtml5.ELEMENT_NODE) { - return; - } - displayStyle = dom.getStyle("display").from(target); - if (displayStyle.substr(0, 6) !== "inline") { - // Make sure that only block elements receive the given class - target.className += " " + className; - } - }); - } - doc.execCommand(command, false, nodeName); - if (eventListener) { - eventListener.stop(); - } - } - - function _selectLineAndWrap(composer, element) { - composer.selection.selectLine(); - composer.selection.surround(element); - _removeLineBreakBeforeAndAfter(element); - _removeLastChildIfLineBreak(element); - composer.selection.selectNode(element); - } - - function _hasClasses(element) { - return !!wysihtml5.lang.string(element.className).trim(); - } - - wysihtml5.commands.formatBlock = { - exec: function(composer, command, nodeName, className, classRegExp) { - var doc = composer.doc, - blockElement = this.state(composer, command, nodeName, className, classRegExp), - selectedNode; - - nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; - - if (blockElement) { - composer.selection.executeAndRestoreSimple(function() { - if (classRegExp) { - _removeClass(blockElement, classRegExp); - } - var hasClasses = _hasClasses(blockElement); - if (!hasClasses && blockElement.nodeName === (nodeName || DEFAULT_NODE_NAME)) { - // Insert a line break afterwards and beforewards when there are siblings - // that are not of type line break or block element - _addLineBreakBeforeAndAfter(blockElement); - dom.replaceWithChildNodes(blockElement); - } else if (hasClasses) { - // Make sure that styling is kept by renaming the element to <div> and copying over the class name - dom.renameElement(blockElement, DEFAULT_NODE_NAME); - } - }); - return; - } - - // Find similiar block element and rename it (<h2 class="foo"></h2> => <h1 class="foo"></h1>) - if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) { - selectedNode = composer.selection.getSelectedNode(); - blockElement = dom.getParentElement(selectedNode, { - nodeName: BLOCK_ELEMENTS_GROUP - }); - - if (blockElement) { - composer.selection.executeAndRestoreSimple(function() { - // Rename current block element to new block element and add class - if (nodeName) { - blockElement = dom.renameElement(blockElement, nodeName); - } - if (className) { - _addClass(blockElement, className, classRegExp); - } - }); - return; - } - } - - if (composer.commands.support(command)) { - _execCommand(doc, command, nodeName || DEFAULT_NODE_NAME, className); - return; - } - - blockElement = doc.createElement(nodeName || DEFAULT_NODE_NAME); - if (className) { - blockElement.className = className; - } - _selectLineAndWrap(composer, blockElement); - }, - - state: function(composer, command, nodeName, className, classRegExp) { - nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; - var selectedNode = composer.selection.getSelectedNode(); - return dom.getParentElement(selectedNode, { - nodeName: nodeName, - className: className, - classRegExp: classRegExp - }); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);/** - * formatInline scenarios for tag "B" (| = caret, |foo| = selected text) - * - * #1 caret in unformatted text: - * abcdefg| - * output: - * abcdefg<b>|</b> - * - * #2 unformatted text selected: - * abc|deg|h - * output: - * abc<b>|deg|</b>h - * - * #3 unformatted text selected across boundaries: - * ab|c <span>defg|h</span> - * output: - * ab<b>|c </b><span><b>defg</b>|h</span> - * - * #4 formatted text entirely selected - * <b>|abc|</b> - * output: - * |abc| - * - * #5 formatted text partially selected - * <b>ab|c|</b> - * output: - * <b>ab</b>|c| - * - * #6 formatted text selected across boundaries - * <span>ab|c</span> <b>de|fgh</b> - * output: - * <span>ab|c</span> de|<b>fgh</b> - */ -(function(wysihtml5) { - var undef, - // Treat <b> as <strong> and vice versa - ALIAS_MAPPING = { - "strong": "b", - "em": "i", - "b": "strong", - "i": "em" - }, - htmlApplier = {}; - - function _getTagNames(tagName) { - var alias = ALIAS_MAPPING[tagName]; - return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()]; - } - - function _getApplier(tagName, className, classRegExp) { - var identifier = tagName + ":" + className; - if (!htmlApplier[identifier]) { - htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true); - } - return htmlApplier[identifier]; - } - - wysihtml5.commands.formatInline = { - exec: function(composer, command, tagName, className, classRegExp) { - var range = composer.selection.getRange(); - if (!range) { - return false; - } - _getApplier(tagName, className, classRegExp).toggleRange(range); - composer.selection.setSelection(range); - }, - - state: function(composer, command, tagName, className, classRegExp) { - var doc = composer.doc, - aliasTagName = ALIAS_MAPPING[tagName] || tagName, - range; - - // Check whether the document contains a node with the desired tagName - if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) && - !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) { - return false; - } - - // Check whether the document contains a node with the desired className - if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) { - return false; - } - - range = composer.selection.getRange(); - if (!range) { - return false; - } - - return _getApplier(tagName, className, classRegExp).isAppliedToRange(range); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);(function(wysihtml5) { - var undef; - - wysihtml5.commands.insertHTML = { - exec: function(composer, command, html) { - if (composer.commands.support(command)) { - composer.doc.execCommand(command, false, html); - } else { - composer.selection.insertHTML(html); - } - }, - - state: function() { - return false; - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);(function(wysihtml5) { - var NODE_NAME = "IMG"; - - wysihtml5.commands.insertImage = { - /** - * Inserts an <img> - * If selection is already an image link, it removes it - * - * @example - * // either ... - * wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg"); - * // ... or ... - * wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" }); - */ - exec: function(composer, command, value) { - value = typeof(value) === "object" ? value : { src: value }; - - var doc = composer.doc, - image = this.state(composer), - textNode, - i, - parent; - - if (image) { - // Image already selected, set the caret before it and delete it - composer.selection.setBefore(image); - parent = image.parentNode; - parent.removeChild(image); - - // and it's parent <a> too if it hasn't got any other relevant child nodes - wysihtml5.dom.removeEmptyTextNodes(parent); - if (parent.nodeName === "A" && !parent.firstChild) { - composer.selection.setAfter(parent); - parent.parentNode.removeChild(parent); - } - - // firefox and ie sometimes don't remove the image handles, even though the image got removed - wysihtml5.quirks.redraw(composer.element); - return; - } - - image = doc.createElement(NODE_NAME); - - for (i in value) { - image[i] = value[i]; - } - - composer.selection.insertNode(image); - if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) { - textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); - composer.selection.insertNode(textNode); - composer.selection.setAfter(textNode); - } else { - composer.selection.setAfter(image); - } - }, - - state: function(composer) { - var doc = composer.doc, - selectedNode, - text, - imagesInSelection; - - if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) { - return false; - } - - selectedNode = composer.selection.getSelectedNode(); - if (!selectedNode) { - return false; - } - - if (selectedNode.nodeName === NODE_NAME) { - // This works perfectly in IE - return selectedNode; - } - - if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) { - return false; - } - - text = composer.selection.getText(); - text = wysihtml5.lang.string(text).trim(); - if (text) { - return false; - } - - imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) { - return node.nodeName === "IMG"; - }); - - if (imagesInSelection.length !== 1) { - return false; - } - - return imagesInSelection[0]; - }, - - value: function(composer) { - var image = this.state(composer); - return image && image.src; - } - }; -})(wysihtml5);(function(wysihtml5) { - var undef, - LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : ""); - - wysihtml5.commands.insertLineBreak = { - exec: function(composer, command) { - if (composer.commands.support(command)) { - composer.doc.execCommand(command, false, null); - if (!wysihtml5.browser.autoScrollsToCaret()) { - composer.selection.scrollIntoView(); - } - } else { - composer.commands.exec("insertHTML", LINE_BREAK); - } - }, - - state: function() { - return false; - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);(function(wysihtml5) { - var undef; - - wysihtml5.commands.insertOrderedList = { - exec: function(composer, command) { - var doc = composer.doc, - selectedNode = composer.selection.getSelectedNode(), - list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), - otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), - tempClassName = "_wysihtml5-temp-" + new Date().getTime(), - isEmpty, - tempElement; - - if (composer.commands.support(command)) { - doc.execCommand(command, false, null); - return; - } - - if (list) { - // Unwrap list - // <ol><li>foo</li><li>bar</li></ol> - // becomes: - // foo<br>bar<br> - composer.selection.executeAndRestoreSimple(function() { - wysihtml5.dom.resolveList(list); - }); - } else if (otherList) { - // Turn an unordered list into an ordered list - // <ul><li>foo</li><li>bar</li></ul> - // becomes: - // <ol><li>foo</li><li>bar</li></ol> - composer.selection.executeAndRestoreSimple(function() { - wysihtml5.dom.renameElement(otherList, "ol"); - }); - } else { - // Create list - composer.commands.exec("formatBlock", "div", tempClassName); - tempElement = doc.querySelector("." + tempClassName); - isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; - composer.selection.executeAndRestoreSimple(function() { - list = wysihtml5.dom.convertToList(tempElement, "ol"); - }); - if (isEmpty) { - composer.selection.selectNode(list.querySelector("li")); - } - } - }, - - state: function(composer) { - var selectedNode = composer.selection.getSelectedNode(); - return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);(function(wysihtml5) { - var undef; - - wysihtml5.commands.insertUnorderedList = { - exec: function(composer, command) { - var doc = composer.doc, - selectedNode = composer.selection.getSelectedNode(), - list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), - otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), - tempClassName = "_wysihtml5-temp-" + new Date().getTime(), - isEmpty, - tempElement; - - if (composer.commands.support(command)) { - doc.execCommand(command, false, null); - return; - } - - if (list) { - // Unwrap list - // <ul><li>foo</li><li>bar</li></ul> - // becomes: - // foo<br>bar<br> - composer.selection.executeAndRestoreSimple(function() { - wysihtml5.dom.resolveList(list); - }); - } else if (otherList) { - // Turn an ordered list into an unordered list - // <ol><li>foo</li><li>bar</li></ol> - // becomes: - // <ul><li>foo</li><li>bar</li></ul> - composer.selection.executeAndRestoreSimple(function() { - wysihtml5.dom.renameElement(otherList, "ul"); - }); - } else { - // Create list - composer.commands.exec("formatBlock", "div", tempClassName); - tempElement = doc.querySelector("." + tempClassName); - isEmpty = tempElement.innerHTML === "" || tempElement.innerHTML === wysihtml5.INVISIBLE_SPACE; - composer.selection.executeAndRestoreSimple(function() { - list = wysihtml5.dom.convertToList(tempElement, "ul"); - }); - if (isEmpty) { - composer.selection.selectNode(list.querySelector("li")); - } - } - }, - - state: function(composer) { - var selectedNode = composer.selection.getSelectedNode(); - return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);(function(wysihtml5) { - var undef; - - wysihtml5.commands.italic = { - exec: function(composer, command) { - return wysihtml5.commands.formatInline.exec(composer, command, "i"); - }, - - state: function(composer, command, color) { - // element.ownerDocument.queryCommandState("italic") results: - // firefox: only <i> - // chrome: <i>, <em>, <blockquote>, ... - // ie: <i>, <em> - // opera: only <i> - return wysihtml5.commands.formatInline.state(composer, command, "i"); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);(function(wysihtml5) { - var undef, - CLASS_NAME = "wysiwyg-text-align-center", - REG_EXP = /wysiwyg-text-align-[a-z]+/g; - - wysihtml5.commands.justifyCenter = { - exec: function(composer, command) { - return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); - }, - - state: function(composer, command) { - return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);(function(wysihtml5) { - var undef, - CLASS_NAME = "wysiwyg-text-align-left", - REG_EXP = /wysiwyg-text-align-[a-z]+/g; - - wysihtml5.commands.justifyLeft = { - exec: function(composer, command) { - return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); - }, - - state: function(composer, command) { - return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);(function(wysihtml5) { - var undef, - CLASS_NAME = "wysiwyg-text-align-right", - REG_EXP = /wysiwyg-text-align-[a-z]+/g; - - wysihtml5.commands.justifyRight = { - exec: function(composer, command) { - return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); - }, - - state: function(composer, command) { - return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);(function(wysihtml5) { - var undef; - wysihtml5.commands.underline = { - exec: function(composer, command) { - return wysihtml5.commands.formatInline.exec(composer, command, "u"); - }, - - state: function(composer, command) { - return wysihtml5.commands.formatInline.state(composer, command, "u"); - }, - - value: function() { - return undef; - } - }; -})(wysihtml5);/** - * Undo Manager for wysihtml5 - * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface - */ -(function(wysihtml5) { - var Z_KEY = 90, - Y_KEY = 89, - BACKSPACE_KEY = 8, - DELETE_KEY = 46, - MAX_HISTORY_ENTRIES = 40, - UNDO_HTML = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>', - REDO_HTML = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>', - dom = wysihtml5.dom; - - function cleanTempElements(doc) { - var tempElement; - while (tempElement = doc.querySelector("._wysihtml5-temp")) { - tempElement.parentNode.removeChild(tempElement); - } - } - - wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend( - /** @scope wysihtml5.UndoManager.prototype */ { - constructor: function(editor) { - this.editor = editor; - this.composer = editor.composer; - this.element = this.composer.element; - this.history = [this.composer.getValue()]; - this.position = 1; - - // Undo manager currently only supported in browsers who have the insertHTML command (not IE) - if (this.composer.commands.support("insertHTML")) { - this._observe(); - } - }, - - _observe: function() { - var that = this, - doc = this.composer.sandbox.getDocument(), - lastKey; - - // Catch CTRL+Z and CTRL+Y - dom.observe(this.element, "keydown", function(event) { - if (event.altKey || (!event.ctrlKey && !event.metaKey)) { - return; - } - - var keyCode = event.keyCode, - isUndo = keyCode === Z_KEY && !event.shiftKey, - isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY); - - if (isUndo) { - that.undo(); - event.preventDefault(); - } else if (isRedo) { - that.redo(); - event.preventDefault(); - } - }); - - // Catch delete and backspace - dom.observe(this.element, "keydown", function(event) { - var keyCode = event.keyCode; - if (keyCode === lastKey) { - return; - } - - lastKey = keyCode; - - if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) { - that.transact(); - } - }); - - // Now this is very hacky: - // These days browsers don't offer a undo/redo event which we could hook into - // to be notified when the user hits undo/redo in the contextmenu. - // Therefore we simply insert two elements as soon as the contextmenu gets opened. - // The last element being inserted will be immediately be removed again by a exexCommand("undo") - // => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu - // => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu - if (wysihtml5.browser.hasUndoInContextMenu()) { - var interval, observed, cleanUp = function() { - cleanTempElements(doc); - clearInterval(interval); - }; - - dom.observe(this.element, "contextmenu", function() { - cleanUp(); - that.composer.selection.executeAndRestoreSimple(function() { - if (that.element.lastChild) { - that.composer.selection.setAfter(that.element.lastChild); - } - - // enable undo button in context menu - doc.execCommand("insertHTML", false, UNDO_HTML); - // enable redo button in context menu - doc.execCommand("insertHTML", false, REDO_HTML); - doc.execCommand("undo", false, null); - }); - - interval = setInterval(function() { - if (doc.getElementById("_wysihtml5-redo")) { - cleanUp(); - that.redo(); - } else if (!doc.getElementById("_wysihtml5-undo")) { - cleanUp(); - that.undo(); - } - }, 400); - - if (!observed) { - observed = true; - dom.observe(document, "mousedown", cleanUp); - dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp); - } - }); - } - - this.editor - .observe("newword:composer", function() { - that.transact(); - }) - - .observe("beforecommand:composer", function() { - that.transact(); - }); - }, - - transact: function() { - var previousHtml = this.history[this.position - 1], - currentHtml = this.composer.getValue(); - - if (currentHtml == previousHtml) { - return; - } - - var length = this.history.length = this.position; - if (length > MAX_HISTORY_ENTRIES) { - this.history.shift(); - this.position--; - } - - this.position++; - this.history.push(currentHtml); - }, - - undo: function() { - this.transact(); - - if (this.position <= 1) { - return; - } - - this.set(this.history[--this.position - 1]); - this.editor.fire("undo:composer"); - }, - - redo: function() { - if (this.position >= this.history.length) { - return; - } - - this.set(this.history[++this.position - 1]); - this.editor.fire("redo:composer"); - }, - - set: function(html) { - this.composer.setValue(html); - this.editor.focus(true); - } - }); -})(wysihtml5); -/** - * TODO: the following methods still need unit test coverage - */ -wysihtml5.views.View = Base.extend( - /** @scope wysihtml5.views.View.prototype */ { - constructor: function(parent, textareaElement, config) { - this.parent = parent; - this.element = textareaElement; - this.config = config; - - this._observeViewChange(); - }, - - _observeViewChange: function() { - var that = this; - this.parent.observe("beforeload", function() { - that.parent.observe("change_view", function(view) { - if (view === that.name) { - that.parent.currentView = that; - that.show(); - // Using tiny delay here to make sure that the placeholder is set before focusing - setTimeout(function() { that.focus(); }, 0); - } else { - that.hide(); - } - }); - }); - }, - - focus: function() { - if (this.element.ownerDocument.querySelector(":focus") === this.element) { - return; - } - - try { this.element.focus(); } catch(e) {} - }, - - hide: function() { - this.element.style.display = "none"; - }, - - show: function() { - this.element.style.display = ""; - }, - - disable: function() { - this.element.setAttribute("disabled", "disabled"); - }, - - enable: function() { - this.element.removeAttribute("disabled"); - } -});(function(wysihtml5) { - var dom = wysihtml5.dom, - browser = wysihtml5.browser; - - wysihtml5.views.Composer = wysihtml5.views.View.extend( - /** @scope wysihtml5.views.Composer.prototype */ { - name: "composer", - - // Needed for firefox in order to display a proper caret in an empty contentEditable - CARET_HACK: "<br>", - - constructor: function(parent, textareaElement, config) { - this.base(parent, textareaElement, config); - this.textarea = this.parent.textarea; - this._initSandbox(); - }, - - clear: function() { - this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK; - }, - - getValue: function(parse) { - var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element); - - if (parse) { - value = this.parent.parse(value); - } - - // Replace all "zero width no breaking space" chars - // which are used as hacks to enable some functionalities - // Also remove all CARET hacks that somehow got left - value = wysihtml5.lang.string(value).replace(wysihtml5.INVISIBLE_SPACE).by(""); - - return value; - }, - - setValue: function(html, parse) { - if (parse) { - html = this.parent.parse(html); - } - this.element.innerHTML = html; - }, - - show: function() { - this.iframe.style.display = this._displayStyle || ""; - - // Firefox needs this, otherwise contentEditable becomes uneditable - this.disable(); - this.enable(); - }, - - hide: function() { - this._displayStyle = dom.getStyle("display").from(this.iframe); - if (this._displayStyle === "none") { - this._displayStyle = null; - } - this.iframe.style.display = "none"; - }, - - disable: function() { - this.element.removeAttribute("contentEditable"); - this.base(); - }, - - enable: function() { - this.element.setAttribute("contentEditable", "true"); - this.base(); - }, - - focus: function(setToEnd) { - // IE 8 fires the focus event after .focus() - // This is needed by our simulate_placeholder.js to work - // therefore we clear it ourselves this time - if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) { - this.clear(); - } - - this.base(); - - var lastChild = this.element.lastChild; - if (setToEnd && lastChild) { - if (lastChild.nodeName === "BR") { - this.selection.setBefore(this.element.lastChild); - } else { - this.selection.setAfter(this.element.lastChild); - } - } - }, - - getTextContent: function() { - return dom.getTextContent(this.element); - }, - - hasPlaceholderSet: function() { - return this.getTextContent() == this.textarea.element.getAttribute("placeholder"); - }, - - isEmpty: function() { - var innerHTML = this.element.innerHTML, - elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea"; - return innerHTML === "" || - innerHTML === this.CARET_HACK || - this.hasPlaceholderSet() || - (this.getTextContent() === "" && !this.element.querySelector(elementsWithVisualValue)); - }, - - _initSandbox: function() { - var that = this; - - this.sandbox = new dom.Sandbox(function() { - that._create(); - }, { - stylesheets: this.config.stylesheets - }); - this.iframe = this.sandbox.getIframe(); - - // Create hidden field which tells the server after submit, that the user used an wysiwyg editor - var hiddenField = document.createElement("input"); - hiddenField.type = "hidden"; - hiddenField.name = "_wysihtml5_mode"; - hiddenField.value = 1; - - // Store reference to current wysihtml5 instance on the textarea element - var textareaElement = this.textarea.element; - dom.insert(this.iframe).after(textareaElement); - dom.insert(hiddenField).after(textareaElement); - }, - - _create: function() { - var that = this; - - this.doc = this.sandbox.getDocument(); - this.element = this.doc.body; - this.textarea = this.parent.textarea; - this.element.innerHTML = this.textarea.getValue(true); - this.enable(); - - // Make sure our selection handler is ready - this.selection = new wysihtml5.Selection(this.parent); - - // Make sure commands dispatcher is ready - this.commands = new wysihtml5.Commands(this.parent); - - dom.copyAttributes([ - "className", "spellcheck", "title", "lang", "dir", "accessKey" - ]).from(this.textarea.element).to(this.element); - - dom.addClass(this.element, this.config.composerClassName); - - // Make the editor look like the original textarea, by syncing styles - if (this.config.style) { - this.style(); - } - - this.observe(); - - var name = this.config.name; - if (name) { - dom.addClass(this.element, name); - dom.addClass(this.iframe, name); - } - - // Simulate html5 placeholder attribute on contentEditable element - var placeholderText = typeof(this.config.placeholder) === "string" - ? this.config.placeholder - : this.textarea.element.getAttribute("placeholder"); - if (placeholderText) { - dom.simulatePlaceholder(this.parent, this, placeholderText); - } - - // Make sure that the browser avoids using inline styles whenever possible - this.commands.exec("styleWithCSS", false); - - this._initAutoLinking(); - this._initObjectResizing(); - this._initUndoManager(); - - // Simulate html5 autofocus on contentEditable element - if (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) { - setTimeout(function() { that.focus(); }, 100); - } - - wysihtml5.quirks.insertLineBreakOnReturn(this); - - // IE sometimes leaves a single paragraph, which can't be removed by the user - if (!browser.clearsContentEditableCorrectly()) { - wysihtml5.quirks.ensureProperClearing(this); - } - - if (!browser.clearsListsInContentEditableCorrectly()) { - wysihtml5.quirks.ensureProperClearingOfLists(this); - } - - // Set up a sync that makes sure that textarea and editor have the same content - if (this.initSync && this.config.sync) { - this.initSync(); - } - - // Okay hide the textarea, we are ready to go - this.textarea.hide(); - - // Fire global (before-)load event - this.parent.fire("beforeload").fire("load"); - }, - - _initAutoLinking: function() { - var that = this, - supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(), - supportsAutoLinking = browser.doesAutoLinkingInContentEditable(); - if (supportsDisablingOfAutoLinking) { - this.commands.exec("autoUrlDetect", false); - } - - if (!this.config.autoLink) { - return; - } - - // Only do the auto linking by ourselves when the browser doesn't support auto linking - // OR when he supports auto linking but we were able to turn it off (IE9+) - if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) { - this.parent.observe("newword:composer", function() { - that.selection.executeAndRestore(function(startContainer, endContainer) { - dom.autoLink(endContainer.parentNode); - }); - }); - } - - // Assuming we have the following: - // <a href="http://www.google.de">http://www.google.de</a> - // If a user now changes the url in the innerHTML we want to make sure that - // it's synchronized with the href attribute (as long as the innerHTML is still a url) - var // Use a live NodeList to check whether there are any links in the document - links = this.sandbox.getDocument().getElementsByTagName("a"), - // The autoLink helper method reveals a reg exp to detect correct urls - urlRegExp = dom.autoLink.URL_REG_EXP, - getTextContent = function(element) { - var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim(); - if (textContent.substr(0, 4) === "www.") { - textContent = "http://" + textContent; - } - return textContent; - }; - - dom.observe(this.element, "keydown", function(event) { - if (!links.length) { - return; - } - - var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument), - link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4), - textContent; - - if (!link) { - return; - } - - textContent = getTextContent(link); - // keydown is fired before the actual content is changed - // therefore we set a timeout to change the href - setTimeout(function() { - var newTextContent = getTextContent(link); - if (newTextContent === textContent) { - return; - } - - // Only set href when new href looks like a valid url - if (newTextContent.match(urlRegExp)) { - link.setAttribute("href", newTextContent); - } - }, 0); - }); - }, - - _initObjectResizing: function() { - var properties = ["width", "height"], - propertiesLength = properties.length, - element = this.element; - - this.commands.exec("enableObjectResizing", this.config.allowObjectResizing); - - if (this.config.allowObjectResizing) { - // IE sets inline styles after resizing objects - // The following lines make sure that the width/height css properties - // are copied over to the width/height attributes - if (browser.supportsEvent("resizeend")) { - dom.observe(element, "resizeend", function(event) { - var target = event.target || event.srcElement, - style = target.style, - i = 0, - property; - for(; i<propertiesLength; i++) { - property = properties[i]; - if (style[property]) { - target.setAttribute(property, parseInt(style[property], 10)); - style[property] = ""; - } - } - // After resizing IE sometimes forgets to remove the old resize handles - wysihtml5.quirks.redraw(element); - }); - } - } else { - if (browser.supportsEvent("resizestart")) { - dom.observe(element, "resizestart", function(event) { event.preventDefault(); }); - } - } - }, - - _initUndoManager: function() { - new wysihtml5.UndoManager(this.parent); - } - }); -})(wysihtml5);(function(wysihtml5) { - var dom = wysihtml5.dom, - doc = document, - win = window, - HOST_TEMPLATE = doc.createElement("div"), - /** - * Styles to copy from textarea to the composer element - */ - TEXT_FORMATTING = [ - "background-color", - "color", "cursor", - "font-family", "font-size", "font-style", "font-variant", "font-weight", - "line-height", "letter-spacing", - "text-align", "text-decoration", "text-indent", "text-rendering", - "word-break", "word-wrap", "word-spacing" - ], - /** - * Styles to copy from textarea to the iframe - */ - BOX_FORMATTING = [ - "background-color", - "border-collapse", - "border-bottom-color", "border-bottom-style", "border-bottom-width", - "border-left-color", "border-left-style", "border-left-width", - "border-right-color", "border-right-style", "border-right-width", - "border-top-color", "border-top-style", "border-top-width", - "clear", "display", "float", - "margin-bottom", "margin-left", "margin-right", "margin-top", - "outline-color", "outline-offset", "outline-width", "outline-style", - "padding-left", "padding-right", "padding-top", "padding-bottom", - "position", "top", "left", "right", "bottom", "z-index", - "vertical-align", "text-align", - "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing", - "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow", - "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius", - "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius", - "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius", - "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius", - "width", "height" - ], - /** - * Styles to sync while the window gets resized - */ - RESIZE_STYLE = [ - "width", "height", - "top", "left", "right", "bottom" - ], - ADDITIONAL_CSS_RULES = [ - "html { height: 100%; }", - "body { min-height: 100%; padding: 0; margin: 0; margin-top: -1px; padding-top: 1px; }", - "._wysihtml5-temp { display: none; }", - wysihtml5.browser.isGecko ? - "body.placeholder { color: graytext !important; }" : - "body.placeholder { color: #a9a9a9 !important; }", - "body[disabled] { background-color: #eee !important; color: #999 !important; cursor: default !important; }", - // Ensure that user see's broken images and can delete them - "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }" - ]; - - /** - * With "setActive" IE offers a smart way of focusing elements without scrolling them into view: - * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx - * - * Other browsers need a more hacky way: (pssst don't tell my mama) - * In order to prevent the element being scrolled into view when focusing it, we simply - * move it out of the scrollable area, focus it, and reset it's position - */ - var focusWithoutScrolling = function(element) { - if (element.setActive) { - // Following line could cause a js error when the textarea is invisible - // See https://github.com/xing/wysihtml5/issues/9 - try { element.setActive(); } catch(e) {} - } else { - var elementStyle = element.style, - originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop, - originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft, - originalStyles = { - position: elementStyle.position, - top: elementStyle.top, - left: elementStyle.left, - WebkitUserSelect: elementStyle.WebkitUserSelect - }; - - dom.setStyles({ - position: "absolute", - top: "-99999px", - left: "-99999px", - // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother - WebkitUserSelect: "none" - }).on(element); - - element.focus(); - - dom.setStyles(originalStyles).on(element); - - if (win.scrollTo) { - // Some browser extensions unset this method to prevent annoyances - // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100 - // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1 - win.scrollTo(originalScrollLeft, originalScrollTop); - } - } - }; - - - wysihtml5.views.Composer.prototype.style = function() { - var that = this, - originalActiveElement = doc.querySelector(":focus"), - textareaElement = this.textarea.element, - hasPlaceholder = textareaElement.hasAttribute("placeholder"), - originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"); - this.focusStylesHost = this.focusStylesHost || HOST_TEMPLATE.cloneNode(false); - this.blurStylesHost = this.blurStylesHost || HOST_TEMPLATE.cloneNode(false); - - // Remove placeholder before copying (as the placeholder has an affect on the computed style) - if (hasPlaceholder) { - textareaElement.removeAttribute("placeholder"); - } - - if (textareaElement === originalActiveElement) { - textareaElement.blur(); - } - - // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) --------- - dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.iframe).andTo(this.blurStylesHost); - - // --------- editor styles --------- - dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost); - - // --------- apply standard rules --------- - dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument); - - // --------- :focus styles --------- - focusWithoutScrolling(textareaElement); - dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost); - dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost); - - // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus - // this is needed for when the change_view event is fired where the iframe is hidden and then - // the blur event fires and re-displays it - var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]); - - // --------- restore focus --------- - if (originalActiveElement) { - originalActiveElement.focus(); - } else { - textareaElement.blur(); - } - - // --------- restore placeholder --------- - if (hasPlaceholder) { - textareaElement.setAttribute("placeholder", originalPlaceholder); - } - - // When copying styles, we only get the computed style which is never returned in percent unit - // Therefore we've to recalculate style onresize - if (!wysihtml5.browser.hasCurrentStyleProperty()) { - var winObserver = dom.observe(win, "resize", function() { - // Remove event listener if composer doesn't exist anymore - if (!dom.contains(document.documentElement, that.iframe)) { - winObserver.stop(); - return; - } - var originalTextareaDisplayStyle = dom.getStyle("display").from(textareaElement), - originalComposerDisplayStyle = dom.getStyle("display").from(that.iframe); - textareaElement.style.display = ""; - that.iframe.style.display = "none"; - dom.copyStyles(RESIZE_STYLE) - .from(textareaElement) - .to(that.iframe) - .andTo(that.focusStylesHost) - .andTo(that.blurStylesHost); - that.iframe.style.display = originalComposerDisplayStyle; - textareaElement.style.display = originalTextareaDisplayStyle; - }); - } - - // --------- Sync focus/blur styles --------- - this.parent.observe("focus:composer", function() { - dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.iframe); - dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element); - }); - - this.parent.observe("blur:composer", function() { - dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.iframe); - dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); - }); - - return this; - }; -})(wysihtml5);/** - * Taking care of events - * - Simulating 'change' event on contentEditable element - * - Handling drag & drop logic - * - Catch paste events - * - Dispatch proprietary newword:composer event - * - Keyboard shortcuts - */ -(function(wysihtml5) { - var dom = wysihtml5.dom, - browser = wysihtml5.browser, - /** - * Map keyCodes to query commands - */ - shortcuts = { - "66": "bold", // B - "73": "italic", // I - "85": "underline" // U - }; - - wysihtml5.views.Composer.prototype.observe = function() { - var that = this, - state = this.getValue(), - iframe = this.sandbox.getIframe(), - element = this.element, - focusBlurElement = browser.supportsEventsInIframeCorrectly() ? element : this.sandbox.getWindow(), - // Firefox < 3.5 doesn't support the drop event, instead it supports a so called "dragdrop" event which behaves almost the same - pasteEvents = browser.supportsEvent("drop") ? ["drop", "paste"] : ["dragdrop", "paste"]; - - // --------- destroy:composer event --------- - dom.observe(iframe, "DOMNodeRemoved", function() { - clearInterval(domNodeRemovedInterval); - that.parent.fire("destroy:composer"); - }); - - // DOMNodeRemoved event is not supported in IE 8 - var domNodeRemovedInterval = setInterval(function() { - if (!dom.contains(document.documentElement, iframe)) { - clearInterval(domNodeRemovedInterval); - that.parent.fire("destroy:composer"); - } - }, 250); - - - // --------- Focus & blur logic --------- - dom.observe(focusBlurElement, "focus", function() { - that.parent.fire("focus").fire("focus:composer"); - - // Delay storing of state until all focus handler are fired - // especially the one which resets the placeholder - setTimeout(function() { state = that.getValue(); }, 0); - }); - - dom.observe(focusBlurElement, "blur", function() { - if (state !== that.getValue()) { - that.parent.fire("change").fire("change:composer"); - } - that.parent.fire("blur").fire("blur:composer"); - }); - - if (wysihtml5.browser.isIos()) { - // When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus - // but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible) - // We prevent that by focusing a temporary input element which immediately loses focus - dom.observe(element, "blur", function() { - var input = element.ownerDocument.createElement("input"), - originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop, - originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; - try { - that.selection.insertNode(input); - } catch(e) { - element.appendChild(input); - } - input.focus(); - input.parentNode.removeChild(input); - - window.scrollTo(originalScrollLeft, originalScrollTop); - }); - } - - // --------- Drag & Drop logic --------- - dom.observe(element, "dragenter", function() { - that.parent.fire("unset_placeholder"); - }); - - if (browser.firesOnDropOnlyWhenOnDragOverIsCancelled()) { - dom.observe(element, ["dragover", "dragenter"], function(event) { - event.preventDefault(); - }); - } - - dom.observe(element, pasteEvents, function(event) { - var dataTransfer = event.dataTransfer, - data; - - if (dataTransfer && browser.supportsDataTransfer()) { - data = dataTransfer.getData("text/html") || dataTransfer.getData("text/plain"); - } - if (data) { - element.focus(); - that.commands.exec("insertHTML", data); - that.parent.fire("paste").fire("paste:composer"); - event.stopPropagation(); - event.preventDefault(); - } else { - setTimeout(function() { - that.parent.fire("paste").fire("paste:composer"); - }, 0); - } - }); - - // --------- neword event --------- - dom.observe(element, "keyup", function(event) { - var keyCode = event.keyCode; - if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) { - that.parent.fire("newword:composer"); - } - }); - - this.parent.observe("paste:composer", function() { - setTimeout(function() { that.parent.fire("newword:composer"); }, 0); - }); - - // --------- Make sure that images are selected when clicking on them --------- - if (!browser.canSelectImagesInContentEditable()) { - dom.observe(element, "mousedown", function(event) { - var target = event.target; - if (target.nodeName === "IMG") { - that.selection.selectNode(target); - event.preventDefault(); - } - }); - } - - // --------- Shortcut logic --------- - dom.observe(element, "keydown", function(event) { - var keyCode = event.keyCode, - command = shortcuts[keyCode]; - if ((event.ctrlKey || event.metaKey) && !event.altKey && command) { - that.commands.exec(command); - event.preventDefault(); - } - }); - - // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor --------- - dom.observe(element, "keydown", function(event) { - var target = that.selection.getSelectedNode(true), - keyCode = event.keyCode, - parent; - if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete - parent = target.parentNode; - // delete the <img> - parent.removeChild(target); - // and it's parent <a> too if it hasn't got any other child nodes - if (parent.nodeName === "A" && !parent.firstChild) { - parent.parentNode.removeChild(parent); - } - - setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0); - event.preventDefault(); - } - }); - - // --------- Show url in tooltip when hovering links or images --------- - var titlePrefixes = { - IMG: "Image: ", - A: "Link: " - }; - - dom.observe(element, "mouseover", function(event) { - var target = event.target, - nodeName = target.nodeName, - title; - if (nodeName !== "A" && nodeName !== "IMG") { - return; - } - var hasTitle = target.hasAttribute("title"); - if(!hasTitle){ - title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src")); - target.setAttribute("title", title); - } - }); - }; -})(wysihtml5);/** - * Class that takes care that the value of the composer and the textarea is always in sync - */ -(function(wysihtml5) { - var INTERVAL = 400; - - wysihtml5.views.Synchronizer = Base.extend( - /** @scope wysihtml5.views.Synchronizer.prototype */ { - - constructor: function(editor, textarea, composer) { - this.editor = editor; - this.textarea = textarea; - this.composer = composer; - - this._observe(); - }, - - /** - * Sync html from composer to textarea - * Takes care of placeholders - * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea - */ - fromComposerToTextarea: function(shouldParseHtml) { - this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue()).trim(), shouldParseHtml); - }, - - /** - * Sync value of textarea to composer - * Takes care of placeholders - * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer - */ - fromTextareaToComposer: function(shouldParseHtml) { - var textareaValue = this.textarea.getValue(); - if (textareaValue) { - this.composer.setValue(textareaValue, shouldParseHtml); - } else { - this.composer.clear(); - this.editor.fire("set_placeholder"); - } - }, - - /** - * Invoke syncing based on view state - * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea - */ - sync: function(shouldParseHtml) { - if (this.editor.currentView.name === "textarea") { - this.fromTextareaToComposer(shouldParseHtml); - } else { - this.fromComposerToTextarea(shouldParseHtml); - } - }, - - /** - * Initializes interval-based syncing - * also makes sure that on-submit the composer's content is synced with the textarea - * immediately when the form gets submitted - */ - _observe: function() { - var interval, - that = this, - form = this.textarea.element.form, - startInterval = function() { - interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL); - }, - stopInterval = function() { - clearInterval(interval); - interval = null; - }; - - startInterval(); - - if (form) { - // If the textarea is in a form make sure that after onreset and onsubmit the composer - // has the correct state - wysihtml5.dom.observe(form, "submit", function() { - that.sync(true); - }); - wysihtml5.dom.observe(form, "reset", function() { - setTimeout(function() { that.fromTextareaToComposer(); }, 0); - }); - } - - this.editor.observe("change_view", function(view) { - if (view === "composer" && !interval) { - that.fromTextareaToComposer(true); - startInterval(); - } else if (view === "textarea") { - that.fromComposerToTextarea(true); - stopInterval(); - } - }); - - this.editor.observe("destroy:composer", stopInterval); - } - }); -})(wysihtml5); -wysihtml5.views.Textarea = wysihtml5.views.View.extend( - /** @scope wysihtml5.views.Textarea.prototype */ { - name: "textarea", - - constructor: function(parent, textareaElement, config) { - this.base(parent, textareaElement, config); - - this._observe(); - }, - - clear: function() { - this.element.value = ""; - }, - - getValue: function(parse) { - var value = this.isEmpty() ? "" : this.element.value; - if (parse) { - value = this.parent.parse(value); - } - return value; - }, - - setValue: function(html, parse) { - if (parse) { - html = this.parent.parse(html); - } - this.element.value = html; - }, - - hasPlaceholderSet: function() { - var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element), - placeholderText = this.element.getAttribute("placeholder") || null, - value = this.element.value, - isEmpty = !value; - return (supportsPlaceholder && isEmpty) || (value === placeholderText); - }, - - isEmpty: function() { - return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet(); - }, - - _observe: function() { - var element = this.element, - parent = this.parent, - eventMapping = { - focusin: "focus", - focusout: "blur" - }, - /** - * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events - * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai - */ - events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"]; - - parent.observe("beforeload", function() { - wysihtml5.dom.observe(element, events, function(event) { - var eventName = eventMapping[event.type] || event.type; - parent.fire(eventName).fire(eventName + ":textarea"); - }); - - wysihtml5.dom.observe(element, ["paste", "drop"], function() { - setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0); - }); - }); - } -});/** - * Toolbar Dialog - * - * @param {Element} link The toolbar link which causes the dialog to show up - * @param {Element} container The dialog container - * - * @example - * <!-- Toolbar link --> - * <a data-wysihtml5-command="insertImage">insert an image</a> - * - * <!-- Dialog --> - * <div data-wysihtml5-dialog="insertImage" style="display: none;"> - * <label> - * URL: <input data-wysihtml5-dialog-field="src" value="http://"> - * </label> - * <label> - * Alternative text: <input data-wysihtml5-dialog-field="alt" value=""> - * </label> - * </div> - * - * <script> - * var dialog = new wysihtml5.toolbar.Dialog( - * document.querySelector("[data-wysihtml5-command='insertImage']"), - * document.querySelector("[data-wysihtml5-dialog='insertImage']") - * ); - * dialog.observe("save", function(attributes) { - * // do something - * }); - * </script> - */ -(function(wysihtml5) { - var dom = wysihtml5.dom, - CLASS_NAME_OPENED = "wysihtml5-command-dialog-opened", - SELECTOR_FORM_ELEMENTS = "input, select, textarea", - SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]", - ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field"; - - - wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend( - /** @scope wysihtml5.toolbar.Dialog.prototype */ { - constructor: function(link, container) { - this.link = link; - this.container = container; - }, - - _observe: function() { - if (this._observed) { - return; - } - - var that = this, - callbackWrapper = function(event) { - var attributes = that._serialize(); - if (attributes == that.elementToChange) { - that.fire("edit", attributes); - } else { - that.fire("save", attributes); - } - that.hide(); - event.preventDefault(); - event.stopPropagation(); - }; - - dom.observe(that.link, "click", function(event) { - if (dom.hasClass(that.link, CLASS_NAME_OPENED)) { - setTimeout(function() { that.hide(); }, 0); - } - }); - - dom.observe(this.container, "keydown", function(event) { - var keyCode = event.keyCode; - if (keyCode === wysihtml5.ENTER_KEY) { - callbackWrapper(event); - } - if (keyCode === wysihtml5.ESCAPE_KEY) { - that.hide(); - } - }); - - dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper); - - dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) { - that.fire("cancel"); - that.hide(); - event.preventDefault(); - event.stopPropagation(); - }); - - var formElements = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS), - i = 0, - length = formElements.length, - _clearInterval = function() { clearInterval(that.interval); }; - for (; i<length; i++) { - dom.observe(formElements[i], "change", _clearInterval); - } - - this._observed = true; - }, - - /** - * Grabs all fields in the dialog and puts them in key=>value style in an object which - * then gets returned - */ - _serialize: function() { - var data = this.elementToChange || {}, - fields = this.container.querySelectorAll(SELECTOR_FIELDS), - length = fields.length, - i = 0; - for (; i<length; i++) { - data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value; - } - return data; - }, - - /** - * Takes the attributes of the "elementToChange" - * and inserts them in their corresponding dialog input fields - * - * Assume the "elementToChange" looks like this: - * <a href="http://www.google.com" target="_blank">foo</a> - * - * and we have the following dialog: - * <input type="text" data-wysihtml5-dialog-field="href" value=""> - * <input type="text" data-wysihtml5-dialog-field="target" value=""> - * - * after calling _interpolate() the dialog will look like this - * <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com"> - * <input type="text" data-wysihtml5-dialog-field="target" value="_blank"> - * - * Basically it adopted the attribute values into the corresponding input fields - * - */ - _interpolate: function(avoidHiddenFields) { - var field, - fieldName, - newValue, - focusedElement = document.querySelector(":focus"), - fields = this.container.querySelectorAll(SELECTOR_FIELDS), - length = fields.length, - i = 0; - for (; i<length; i++) { - field = fields[i]; - - // Never change elements where the user is currently typing in - if (field === focusedElement) { - continue; - } - - // Don't update hidden fields - // See https://github.com/xing/wysihtml5/pull/14 - if (avoidHiddenFields && field.type === "hidden") { - continue; - } - - fieldName = field.getAttribute(ATTRIBUTE_FIELDS); - newValue = this.elementToChange ? (this.elementToChange[fieldName] || "") : field.defaultValue; - field.value = newValue; - } - }, - - /** - * Show the dialog element - */ - show: function(elementToChange) { - var that = this, - firstField = this.container.querySelector(SELECTOR_FORM_ELEMENTS); - this.elementToChange = elementToChange; - this._observe(); - this._interpolate(); - if (elementToChange) { - this.interval = setInterval(function() { that._interpolate(true); }, 500); - } - dom.addClass(this.link, CLASS_NAME_OPENED); - this.container.style.display = ""; - this.fire("show"); - if (firstField && !elementToChange) { - try { - firstField.focus(); - } catch(e) {} - } - }, - - /** - * Hide the dialog element - */ - hide: function() { - clearInterval(this.interval); - this.elementToChange = null; - dom.removeClass(this.link, CLASS_NAME_OPENED); - this.container.style.display = "none"; - this.fire("hide"); - } - }); -})(wysihtml5); -/** - * Converts speech-to-text and inserts this into the editor - * As of now (2011/03/25) this only is supported in Chrome >= 11 - * - * Note that it sends the recorded audio to the google speech recognition api: - * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec - * - * Current HTML5 draft can be found here - * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html - * - * "Accessing Google Speech API Chrome 11" - * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/ - */ -(function(wysihtml5) { - var dom = wysihtml5.dom; - - var linkStyles = { - position: "relative" - }; - - var wrapperStyles = { - left: 0, - margin: 0, - opacity: 0, - overflow: "hidden", - padding: 0, - position: "absolute", - top: 0, - zIndex: 1 - }; - - var inputStyles = { - cursor: "inherit", - fontSize: "50px", - height: "50px", - marginTop: "-25px", - outline: 0, - padding: 0, - position: "absolute", - right: "-4px", - top: "50%" - }; - - var inputAttributes = { - "x-webkit-speech": "", - "speech": "" - }; - - wysihtml5.toolbar.Speech = function(parent, link) { - var input = document.createElement("input"); - if (!wysihtml5.browser.supportsSpeechApiOn(input)) { - link.style.display = "none"; - return; - } - - var wrapper = document.createElement("div"); - - wysihtml5.lang.object(wrapperStyles).merge({ - width: link.offsetWidth + "px", - height: link.offsetHeight + "px" - }); - - dom.insert(input).into(wrapper); - dom.insert(wrapper).into(link); - - dom.setStyles(inputStyles).on(input); - dom.setAttributes(inputAttributes).on(input) - - dom.setStyles(wrapperStyles).on(wrapper); - dom.setStyles(linkStyles).on(link); - - var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange"; - dom.observe(input, eventName, function() { - parent.execCommand("insertText", input.value); - input.value = ""; - }); - - dom.observe(input, "click", function(event) { - if (dom.hasClass(link, "wysihtml5-command-disabled")) { - event.preventDefault(); - } - - event.stopPropagation(); - }); - }; -})(wysihtml5);/** - * Toolbar - * - * @param {Object} parent Reference to instance of Editor instance - * @param {Element} container Reference to the toolbar container element - * - * @example - * <div id="toolbar"> - * <a data-wysihtml5-command="createLink">insert link</a> - * <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a> - * </div> - * - * <script> - * var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar")); - * </script> - */ -(function(wysihtml5) { - var CLASS_NAME_COMMAND_DISABLED = "wysihtml5-command-disabled", - CLASS_NAME_COMMANDS_DISABLED = "wysihtml5-commands-disabled", - CLASS_NAME_COMMAND_ACTIVE = "wysihtml5-command-active", - CLASS_NAME_ACTION_ACTIVE = "wysihtml5-action-active", - dom = wysihtml5.dom; - - wysihtml5.toolbar.Toolbar = Base.extend( - /** @scope wysihtml5.toolbar.Toolbar.prototype */ { - constructor: function(editor, container) { - this.editor = editor; - this.container = typeof(container) === "string" ? document.getElementById(container) : container; - this.composer = editor.composer; - - this._getLinks("command"); - this._getLinks("action"); - - this._observe(); - this.show(); - - var speechInputLinks = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"), - length = speechInputLinks.length, - i = 0; - for (; i<length; i++) { - new wysihtml5.toolbar.Speech(this, speechInputLinks[i]); - } - }, - - _getLinks: function(type) { - var links = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("[data-wysihtml5-" + type + "]")).get(), - length = links.length, - i = 0, - mapping = this[type + "Mapping"] = {}, - link, - group, - name, - value, - dialog; - for (; i<length; i++) { - link = links[i]; - name = link.getAttribute("data-wysihtml5-" + type); - value = link.getAttribute("data-wysihtml5-" + type + "-value"); - group = this.container.querySelector("[data-wysihtml5-" + type + "-group='" + name + "']"); - dialog = this._getDialog(link, name); - - mapping[name + ":" + value] = { - link: link, - group: group, - name: name, - value: value, - dialog: dialog, - state: false - }; - } - }, - - _getDialog: function(link, command) { - var that = this, - dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"), - dialog, - caretBookmark; - - if (dialogElement) { - dialog = new wysihtml5.toolbar.Dialog(link, dialogElement); - - dialog.observe("show", function() { - caretBookmark = that.composer.selection.getBookmark(); - - that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); - }); - - dialog.observe("save", function(attributes) { - if (caretBookmark) { - that.composer.selection.setBookmark(caretBookmark); - } - that._execCommand(command, attributes); - - that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); - }); - - dialog.observe("cancel", function() { - that.editor.focus(false); - that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link }); - }); - } - return dialog; - }, - - /** - * @example - * var toolbar = new wysihtml5.Toolbar(); - * // Insert a <blockquote> element or wrap current selection in <blockquote> - * toolbar.execCommand("formatBlock", "blockquote"); - */ - execCommand: function(command, commandValue) { - if (this.commandsDisabled) { - return; - } - - var commandObj = this.commandMapping[command + ":" + commandValue]; - - // Show dialog when available - if (commandObj && commandObj.dialog && !commandObj.state) { - commandObj.dialog.show(); - } else { - this._execCommand(command, commandValue); - } - }, - - _execCommand: function(command, commandValue) { - // Make sure that composer is focussed (false => don't move caret to the end) - this.editor.focus(false); - - this.composer.commands.exec(command, commandValue); - this._updateLinkStates(); - }, - - execAction: function(action) { - var editor = this.editor; - switch(action) { - case "change_view": - if (editor.currentView === editor.textarea) { - editor.fire("change_view", "composer"); - } else { - editor.fire("change_view", "textarea"); - } - break; - } - }, - - _observe: function() { - var that = this, - editor = this.editor, - container = this.container, - links = this.commandLinks.concat(this.actionLinks), - length = links.length, - i = 0; - - for (; i<length; i++) { - // 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied - // (you know, a:link { ... } doesn't match anchors with missing href attribute) - dom.setAttributes({ - href: "javascript:;", - unselectable: "on" - }).on(links[i]); - } - - // Needed for opera - dom.delegate(container, "[data-wysihtml5-command]", "mousedown", function(event) { event.preventDefault(); }); - - dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) { - var link = this, - command = link.getAttribute("data-wysihtml5-command"), - commandValue = link.getAttribute("data-wysihtml5-command-value"); - that.execCommand(command, commandValue); - event.preventDefault(); - }); - - dom.delegate(container, "[data-wysihtml5-action]", "click", function(event) { - var action = this.getAttribute("data-wysihtml5-action"); - that.execAction(action); - event.preventDefault(); - }); - - editor.observe("focus:composer", function() { - that.bookmark = null; - clearInterval(that.interval); - that.interval = setInterval(function() { that._updateLinkStates(); }, 500); - }); - - editor.observe("blur:composer", function() { - clearInterval(that.interval); - }); - - editor.observe("destroy:composer", function() { - clearInterval(that.interval); - }); - - editor.observe("change_view", function(currentView) { - // Set timeout needed in order to let the blur event fire first - setTimeout(function() { - that.commandsDisabled = (currentView !== "composer"); - that._updateLinkStates(); - if (that.commandsDisabled) { - dom.addClass(container, CLASS_NAME_COMMANDS_DISABLED); - } else { - dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED); - } - }, 0); - }); - }, - - _updateLinkStates: function() { - var element = this.composer.element, - commandMapping = this.commandMapping, - actionMapping = this.actionMapping, - i, - state, - action, - command; - // every millisecond counts... this is executed quite often - for (i in commandMapping) { - command = commandMapping[i]; - if (this.commandsDisabled) { - state = false; - dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE); - if (command.group) { - dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE); - } - if (command.dialog) { - command.dialog.hide(); - } - } else { - state = this.composer.commands.state(command.name, command.value); - if (wysihtml5.lang.object(state).isArray()) { - // Grab first and only object/element in state array, otherwise convert state into boolean - // to avoid showing a dialog for multiple selected elements which may have different attributes - // eg. when two links with different href are selected, the state will be an array consisting of both link elements - // but the dialog interface can only update one - state = state.length === 1 ? state[0] : true; - } - dom.removeClass(command.link, CLASS_NAME_COMMAND_DISABLED); - if (command.group) { - dom.removeClass(command.group, CLASS_NAME_COMMAND_DISABLED); - } - } - - if (command.state === state) { - continue; - } - - command.state = state; - if (state) { - dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE); - if (command.group) { - dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE); - } - if (command.dialog) { - if (typeof(state) === "object") { - command.dialog.show(state); - } else { - command.dialog.hide(); - } - } - } else { - dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE); - if (command.group) { - dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE); - } - if (command.dialog) { - command.dialog.hide(); - } - } - } - - for (i in actionMapping) { - action = actionMapping[i]; - - if (action.name === "change_view") { - action.state = this.editor.currentView === this.editor.textarea; - if (action.state) { - dom.addClass(action.link, CLASS_NAME_ACTION_ACTIVE); - } else { - dom.removeClass(action.link, CLASS_NAME_ACTION_ACTIVE); - } - } - } - }, - - show: function() { - this.container.style.display = ""; - }, - - hide: function() { - this.container.style.display = "none"; - } - }); - -})(wysihtml5); -/** - * WYSIHTML5 Editor - * - * @param {Element} textareaElement Reference to the textarea which should be turned into a rich text interface - * @param {Object} [config] See defaultConfig object below for explanation of each individual config option - * - * @events - * load - * beforeload (for internal use only) - * focus - * focus:composer - * focus:textarea - * blur - * blur:composer - * blur:textarea - * change - * change:composer - * change:textarea - * paste - * paste:composer - * paste:textarea - * newword:composer - * destroy:composer - * undo:composer - * redo:composer - * beforecommand:composer - * aftercommand:composer - * change_view - */ -(function(wysihtml5) { - var undef; - - var defaultConfig = { - // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body - name: undef, - // Whether the editor should look like the textarea (by adopting styles) - style: true, - // Id of the toolbar element, pass falsey value if you don't want any toolbar logic - toolbar: undef, - // Whether urls, entered by the user should automatically become clickable-links - autoLink: true, - // Object which includes parser rules to apply when html gets inserted via copy & paste - // See parser_rules/*.js for examples - parserRules: { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} }, - // Parser method to use when the user inserts content via copy & paste - parser: wysihtml5.dom.parse, - // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option - composerClassName: "wysihtml5-editor", - // Class name to add to the body when the wysihtml5 editor is supported - bodyClassName: "wysihtml5-supported", - // Array (or single string) of stylesheet urls to be loaded in the editor's iframe - stylesheets: [], - // Placeholder text to use, defaults to the placeholder attribute on the textarea element - placeholderText: undef, - // Whether the composer should allow the user to manually resize images, tables etc. - allowObjectResizing: true, - // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5) - supportTouchDevices: true - }; - - wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend( - /** @scope wysihtml5.Editor.prototype */ { - constructor: function(textareaElement, config) { - this.textareaElement = typeof(textareaElement) === "string" ? document.getElementById(textareaElement) : textareaElement; - this.config = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get(); - this.textarea = new wysihtml5.views.Textarea(this, this.textareaElement, this.config); - this.currentView = this.textarea; - this._isCompatible = wysihtml5.browser.supported(); - - // Sort out unsupported/unwanted browsers here - if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) { - var that = this; - setTimeout(function() { that.fire("beforeload").fire("load"); }, 0); - return; - } - - // Add class name to body, to indicate that the editor is supported - wysihtml5.dom.addClass(document.body, this.config.bodyClassName); - - this.composer = new wysihtml5.views.Composer(this, this.textareaElement, this.config); - this.currentView = this.composer; - - if (typeof(this.config.parser) === "function") { - this._initParser(); - } - - this.observe("beforeload", function() { - this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer); - if (this.config.toolbar) { - this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar); - } - }); - - try { - // console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5"); - } catch(e) {} - }, - - isCompatible: function() { - return this._isCompatible; - }, - - clear: function() { - this.currentView.clear(); - return this; - }, - - getValue: function(parse) { - return this.currentView.getValue(parse); - }, - - setValue: function(html, parse) { - if (!html) { - return this.clear(); - } - this.currentView.setValue(html, parse); - return this; - }, - - focus: function(setToEnd) { - this.currentView.focus(setToEnd); - return this; - }, - - /** - * Deactivate editor (make it readonly) - */ - disable: function() { - this.currentView.disable(); - return this; - }, - - /** - * Activate editor - */ - enable: function() { - this.currentView.enable(); - return this; - }, - - isEmpty: function() { - return this.currentView.isEmpty(); - }, - - hasPlaceholderSet: function() { - return this.currentView.hasPlaceholderSet(); - }, - - parse: function(htmlOrElement) { - var returnValue = this.config.parser(htmlOrElement, this.config.parserRules, this.composer.sandbox.getDocument(), true); - if (typeof(htmlOrElement) === "object") { - wysihtml5.quirks.redraw(htmlOrElement); - } - return returnValue; - }, - - /** - * Prepare html parser logic - * - Observes for paste and drop - */ - _initParser: function() { - this.observe("paste:composer", function() { - var keepScrollPosition = true, - that = this; - that.composer.selection.executeAndRestore(function() { - wysihtml5.quirks.cleanPastedHTML(that.composer.element); - that.parse(that.composer.element); - }, keepScrollPosition); - }); - - this.observe("paste:textarea", function() { - var value = this.textarea.getValue(), - newValue; - newValue = this.parse(value); - this.textarea.setValue(newValue); - }); - } - }); -})(wysihtml5); +window.Storytime || (window.Storytime = {}) +window.Storytime.Dashboard = {} +; /*! * jQuery JavaScript Library v1.11.1 * http://jquery.com/ * * Includes Sizzle.js @@ -19865,11 +10345,11 @@ /** * Unobtrusive scripting adapter for jQuery * https://github.com/rails/jquery-ujs * - * Requires jQuery 1.7.0 or later. + * Requires jQuery 1.8.0 or later. * * Released under the MIT license * */ @@ -19886,20 +10366,20 @@ $.rails = rails = { // Link elements bound by jquery-ujs linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]', // Button elements bound by jquery-ujs - buttonClickSelector: 'button[data-remote], button[data-confirm]', + buttonClickSelector: 'button[data-remote]:not(form button), button[data-confirm]:not(form button)', // Select elements bound by jquery-ujs inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]', // Form elements bound by jquery-ujs formSubmitSelector: 'form', // Form input elements bound by jquery-ujs - formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])', + formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])', // Form input elements disabled during form submission disableSelector: 'input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled', // Form input elements re-enabled after form submission @@ -20217,10 +10697,11 @@ } }); $document.delegate(rails.buttonClickSelector, 'click.rails', function(e) { var button = $(this); + if (!rails.allowAction(button)) return rails.stopEverything(e); if (button.is(rails.buttonDisableSelector)) rails.disableFormElement(button); var handleRemote = rails.handleRemote(button); @@ -22412,566 +12893,2943 @@ return this } }(jQuery); -!function($, wysi) { - "use strict"; +/*! + * jQuery UI Core 1.11.0 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/category/ui-core/ + */ - var tpl = { - "font-styles": function(locale, options) { - var size = (options && options.size) ? ' btn-'+options.size : ''; - return "<li class='dropdown'>" + - "<a class='btn dropdown-toggle btn-" + size + " btn-default' data-toggle='dropdown' href='#'>" + - "<i class='glyphicon glyphicon-font'></i>&nbsp;<span class='current-font'>" + locale.font_styles.normal + "</span>&nbsp;<b class='caret'></b>" + - "</a>" + - "<ul class='dropdown-menu'>" + - "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='div' tabindex='-1'>" + locale.font_styles.normal + "</a></li>" + - "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h1' tabindex='-1'>" + locale.font_styles.h1 + "</a></li>" + - "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h2' tabindex='-1'>" + locale.font_styles.h2 + "</a></li>" + - "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h3' tabindex='-1'>" + locale.font_styles.h3 + "</a></li>" + - "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h4'>" + locale.font_styles.h4 + "</a></li>" + - "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h5'>" + locale.font_styles.h5 + "</a></li>" + - "<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h6'>" + locale.font_styles.h6 + "</a></li>" + - "</ul>" + - "</li>"; - }, +(function( factory ) { + if ( typeof define === "function" && define.amd ) { - "emphasis": function(locale, options) { - var size = (options && options.size) ? ' btn-'+options.size : ''; - return "<li>" + - "<div class='btn-group'>" + - "<a class='btn btn-" + size + " btn-default' data-wysihtml5-command='bold' title='CTRL+B' tabindex='-1'>" + locale.emphasis.bold + "</a>" + - "<a class='btn btn-" + size + " btn-default' data-wysihtml5-command='italic' title='CTRL+I' tabindex='-1'>" + locale.emphasis.italic + "</a>" + - "<a class='btn btn-" + size + " btn-default' data-wysihtml5-command='underline' title='CTRL+U' tabindex='-1'>" + locale.emphasis.underline + "</a>" + - "</div>" + - "</li>"; - }, + // AMD. Register as an anonymous module. + define( [ "jquery" ], factory ); + } else { - "lists": function(locale, options) { - var size = (options && options.size) ? ' btn-'+options.size : ''; - return "<li>" + - "<div class='btn-group'>" + - "<a class='btn btn-" + size + " btn-default' data-wysihtml5-command='insertUnorderedList' title='" + locale.lists.unordered + "' tabindex='-1'><i class='glyphicon glyphicon-list'></i></a>" + - "<a class='btn btn-" + size + " btn-default' data-wysihtml5-command='insertOrderedList' title='" + locale.lists.ordered + "' tabindex='-1'><i class='glyphicon glyphicon-th-list'></i></a>" + - "<a class='btn btn-" + size + " btn-default' data-wysihtml5-command='Outdent' title='" + locale.lists.outdent + "' tabindex='-1'><i class='glyphicon glyphicon-indent-right'></i></a>" + - "<a class='btn btn-" + size + " btn-default' data-wysihtml5-command='Indent' title='" + locale.lists.indent + "' tabindex='-1'><i class='glyphicon glyphicon-indent-left'></i></a>" + - "</div>" + - "</li>"; - }, + // Browser globals + factory( jQuery ); + } +}(function( $ ) { - "link": function(locale, options) { - var size = (options && options.size) ? ' btn-'+options.size : ''; - return "<li>" + - ""+ - "<div class='bootstrap-wysihtml5-insert-link-modal modal fade'>" + - "<div class='modal-dialog'>"+ - "<div class='modal-content'>"+ - "<div class='modal-header'>" + - "<a class='close' data-dismiss='modal'>&times;</a>" + - "<h4>" + locale.link.insert + "</h4>" + - "</div>" + - "<div class='modal-body'>" + - "<input value='http://' class='bootstrap-wysihtml5-insert-link-url form-control'>" + - "<label class='checkbox'> <input type='checkbox' class='bootstrap-wysihtml5-insert-link-target' checked>" + locale.link.target + "</label>" + - "</div>" + - "<div class='modal-footer'>" + - "<button class='btn btn-default' data-dismiss='modal'>" + locale.link.cancel + "</button>" + - "<button href='#' class='btn btn-primary' data-dismiss='modal'>" + locale.link.insert + "</button>" + - "</div>" + - "</div>" + - "</div>" + - "</div>" + - "<a class='btn btn-" + size + " btn-default' data-wysihtml5-command='createLink' title='" + locale.link.insert + "' tabindex='-1'><i class='glyphicon glyphicon-share'></i></a>" + - "</li>"; - }, +// $.ui might exist from components with no dependencies, e.g., $.ui.position +$.ui = $.ui || {}; - "image": function(locale, options) { - var size = (options && options.size) ? ' btn-'+options.size : ''; - return "<li>" + - "<div class='bootstrap-wysihtml5-insert-image-modal modal fade'>" + - "<div class='modal-dialog'>"+ - "<div class='modal-content'>"+ - "<div class='modal-header'>" + - "<a class='close' data-dismiss='modal'>&times;</a>" + - "<h4>" + locale.image.insert + "</h4>" + - "</div>" + - "<div class='modal-body'>" + - "<input value='http://' class='bootstrap-wysihtml5-insert-image-url form-control'>" + - "</div>" + - "<div class='modal-footer'>" + - "<button class='btn btn-default' data-dismiss='modal'>" + locale.image.cancel + "</button>" + - "<button class='btn btn-primary' data-dismiss='modal'>" + locale.image.insert + "</button>" + - "</div>" + - "</div>" + - "</div>" + - "</div>" + - "<a class='btn btn-" + size + " btn-default' data-wysihtml5-command='insertImage' title='" + locale.image.insert + "' tabindex='-1'><i class='glyphicon glyphicon-picture'></i></a>" + - "</li>"; - }, +$.extend( $.ui, { + version: "1.11.0", - "html": function(locale, options) { - var size = (options && options.size) ? ' btn-'+options.size : ''; - return "<li>" + - "<div class='btn-group'>" + - "<a class='btn btn-" + size + " btn-default' data-wysihtml5-action='change_view' title='" + locale.html.edit + "' tabindex='-1'><i class='glyphicon glyphicon-pencil'></i></a>" + - "</div>" + - "</li>"; - }, + keyCode: { + BACKSPACE: 8, + COMMA: 188, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + LEFT: 37, + PAGE_DOWN: 34, + PAGE_UP: 33, + PERIOD: 190, + RIGHT: 39, + SPACE: 32, + TAB: 9, + UP: 38 + } +}); - "color": function(locale, options) { - var size = (options && options.size) ? ' btn-'+options.size : ''; - return "<li class='dropdown'>" + - "<a class='btn dropdown-toggle btn-" + size + " btn-default' data-toggle='dropdown' href='#' tabindex='-1'>" + - "<span class='current-color'>" + locale.colours.black + "</span>&nbsp;<b class='caret'></b>" + - "</a>" + - "<ul class='dropdown-menu'>" + - "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='black'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='black'>" + locale.colours.black + "</a></li>" + - "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='silver'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='silver'>" + locale.colours.silver + "</a></li>" + - "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='gray'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='gray'>" + locale.colours.gray + "</a></li>" + - "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='maroon'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='maroon'>" + locale.colours.maroon + "</a></li>" + - "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='red'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='red'>" + locale.colours.red + "</a></li>" + - "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='purple'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='purple'>" + locale.colours.purple + "</a></li>" + - "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='green'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='green'>" + locale.colours.green + "</a></li>" + - "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='olive'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='olive'>" + locale.colours.olive + "</a></li>" + - "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='navy'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='navy'>" + locale.colours.navy + "</a></li>" + - "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='blue'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='blue'>" + locale.colours.blue + "</a></li>" + - "<li><div class='wysihtml5-colors' data-wysihtml5-command-value='orange'></div><a class='wysihtml5-colors-title' data-wysihtml5-command='foreColor' data-wysihtml5-command-value='orange'>" + locale.colours.orange + "</a></li>" + - "</ul>" + - "</li>"; - } - }; +// plugins +$.fn.extend({ + scrollParent: function() { + var position = this.css( "position" ), + excludeStaticParent = position === "absolute", + scrollParent = this.parents().filter( function() { + var parent = $( this ); + if ( excludeStaticParent && parent.css( "position" ) === "static" ) { + return false; + } + return (/(auto|scroll)/).test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + parent.css( "overflow-x" ) ); + }).eq( 0 ); - var templates = function(key, locale, options) { - return tpl[key](locale, options); - }; + return position === "fixed" || !scrollParent.length ? $( this[ 0 ].ownerDocument || document ) : scrollParent; + }, + uniqueId: (function() { + var uuid = 0; - var Wysihtml5 = function(el, options) { - this.el = el; - var toolbarOpts = options || defaultOptions; - for(var t in toolbarOpts.customTemplates) { - tpl[t] = toolbarOpts.customTemplates[t]; - } - this.toolbar = this.createToolbar(el, toolbarOpts); - this.editor = this.createEditor(options); + return function() { + return this.each(function() { + if ( !this.id ) { + this.id = "ui-id-" + ( ++uuid ); + } + }); + }; + })(), - window.editor = this.editor; + removeUniqueId: function() { + return this.each(function() { + if ( /^ui-id-\d+$/.test( this.id ) ) { + $( this ).removeAttr( "id" ); + } + }); + } +}); - $('iframe.wysihtml5-sandbox').each(function(i, el){ - $(el.contentWindow).off('focus.wysihtml5').on({ - 'focus.wysihtml5' : function(){ - $('li.dropdown').removeClass('open'); - } - }); - }); - }; +// selectors +function focusable( element, isTabIndexNotNaN ) { + var map, mapName, img, + nodeName = element.nodeName.toLowerCase(); + if ( "area" === nodeName ) { + map = element.parentNode; + mapName = map.name; + if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { + return false; + } + img = $( "img[usemap=#" + mapName + "]" )[0]; + return !!img && visible( img ); + } + return ( /input|select|textarea|button|object/.test( nodeName ) ? + !element.disabled : + "a" === nodeName ? + element.href || isTabIndexNotNaN : + isTabIndexNotNaN) && + // the element and all of its ancestors must be visible + visible( element ); +} - Wysihtml5.prototype = { +function visible( element ) { + return $.expr.filters.visible( element ) && + !$( element ).parents().addBack().filter(function() { + return $.css( this, "visibility" ) === "hidden"; + }).length; +} - constructor: Wysihtml5, +$.extend( $.expr[ ":" ], { + data: $.expr.createPseudo ? + $.expr.createPseudo(function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + }) : + // support: jQuery <1.8 + function( elem, i, match ) { + return !!$.data( elem, match[ 3 ] ); + }, - createEditor: function(options) { - options = options || {}; + focusable: function( element ) { + return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); + }, - // Add the toolbar to a clone of the options object so multiple instances - // of the WYISYWG don't break because "toolbar" is already defined - options = $.extend(true, {}, options); - options.toolbar = this.toolbar[0]; + tabbable: function( element ) { + var tabIndex = $.attr( element, "tabindex" ), + isTabIndexNaN = isNaN( tabIndex ); + return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); + } +}); - var editor = new wysi.Editor(this.el[0], options); +// support: jQuery <1.8 +if ( !$( "<a>" ).outerWidth( 1 ).jquery ) { + $.each( [ "Width", "Height" ], function( i, name ) { + var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], + type = name.toLowerCase(), + orig = { + innerWidth: $.fn.innerWidth, + innerHeight: $.fn.innerHeight, + outerWidth: $.fn.outerWidth, + outerHeight: $.fn.outerHeight + }; - if(options && options.events) { - for(var eventName in options.events) { - editor.on(eventName, options.events[eventName]); - } - } - return editor; - }, + function reduce( elem, size, border, margin ) { + $.each( side, function() { + size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; + if ( border ) { + size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; + } + if ( margin ) { + size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; + } + }); + return size; + } - createToolbar: function(el, options) { - var self = this; - var toolbar = $("<ul/>", { - 'class' : "wysihtml5-toolbar", - 'style': "display:none" - }); - var culture = options.locale || defaultOptions.locale || "en"; - for(var key in defaultOptions) { - var value = false; + $.fn[ "inner" + name ] = function( size ) { + if ( size === undefined ) { + return orig[ "inner" + name ].call( this ); + } - if(options[key] !== undefined) { - if(options[key] === true) { - value = true; - } - } else { - value = defaultOptions[key]; - } + return this.each(function() { + $( this ).css( type, reduce( this, size ) + "px" ); + }); + }; - if(value === true) { - toolbar.append(templates(key, locale[culture], options)); + $.fn[ "outer" + name] = function( size, margin ) { + if ( typeof size !== "number" ) { + return orig[ "outer" + name ].call( this, size ); + } - if(key === "html") { - this.initHtml(toolbar); - } + return this.each(function() { + $( this).css( type, reduce( this, size, true, margin ) + "px" ); + }); + }; + }); +} - if(key === "link") { - this.initInsertLink(toolbar); - } +// support: jQuery <1.8 +if ( !$.fn.addBack ) { + $.fn.addBack = function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + }; +} - if(key === "image") { - this.initInsertImage(toolbar); - } - } - } +// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) +if ( $( "<a>" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { + $.fn.removeData = (function( removeData ) { + return function( key ) { + if ( arguments.length ) { + return removeData.call( this, $.camelCase( key ) ); + } else { + return removeData.call( this ); + } + }; + })( $.fn.removeData ); +} - if(options.toolbar) { - for(key in options.toolbar) { - toolbar.append(options.toolbar[key]); - } - } +// deprecated +$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); - toolbar.find("a[data-wysihtml5-command='formatBlock']").click(function(e) { - var target = e.target || e.srcElement; - var el = $(target); - self.toolbar.find('.current-font').text(el.html()); - }); +$.fn.extend({ + focus: (function( orig ) { + return function( delay, fn ) { + return typeof delay === "number" ? + this.each(function() { + var elem = this; + setTimeout(function() { + $( elem ).focus(); + if ( fn ) { + fn.call( elem ); + } + }, delay ); + }) : + orig.apply( this, arguments ); + }; + })( $.fn.focus ), - toolbar.find("a[data-wysihtml5-command='foreColor']").click(function(e) { - var target = e.target || e.srcElement; - var el = $(target); - self.toolbar.find('.current-color').text(el.html()); - }); + disableSelection: (function() { + var eventType = "onselectstart" in document.createElement( "div" ) ? + "selectstart" : + "mousedown"; - this.el.before(toolbar); + return function() { + return this.bind( eventType + ".ui-disableSelection", function( event ) { + event.preventDefault(); + }); + }; + })(), - return toolbar; - }, + enableSelection: function() { + return this.unbind( ".ui-disableSelection" ); + }, - initHtml: function(toolbar) { - var changeViewSelector = "a[data-wysihtml5-action='change_view']"; - toolbar.find(changeViewSelector).click(function(e) { - toolbar.find('a.btn').not(changeViewSelector).toggleClass('disabled'); - }); - }, + zIndex: function( zIndex ) { + if ( zIndex !== undefined ) { + return this.css( "zIndex", zIndex ); + } - initInsertImage: function(toolbar) { - var self = this; - var insertImageModal = toolbar.find('.bootstrap-wysihtml5-insert-image-modal'); - var urlInput = insertImageModal.find('.bootstrap-wysihtml5-insert-image-url'); - var insertButton = insertImageModal.find('.btn-primary'); - var initialValue = urlInput.val(); - var caretBookmark; + if ( this.length ) { + var elem = $( this[ 0 ] ), position, value; + while ( elem.length && elem[ 0 ] !== document ) { + // Ignore z-index if position is set to a value where z-index is ignored by the browser + // This makes behavior of this function consistent across browsers + // WebKit always returns auto if the element is positioned + position = elem.css( "position" ); + if ( position === "absolute" || position === "relative" || position === "fixed" ) { + // IE returns 0 when zIndex is not specified + // other browsers return a string + // we ignore the case of nested elements with an explicit value of 0 + // <div style="z-index: -10;"><div style="z-index: 0;"></div></div> + value = parseInt( elem.css( "zIndex" ), 10 ); + if ( !isNaN( value ) && value !== 0 ) { + return value; + } + } + elem = elem.parent(); + } + } - var insertImage = function() { - var url = urlInput.val(); - urlInput.val(initialValue); - self.editor.currentView.element.focus(); - if (caretBookmark) { - self.editor.composer.selection.setBookmark(caretBookmark); - caretBookmark = null; - } - self.editor.composer.commands.exec("insertImage", url); - }; + return 0; + } +}); - urlInput.keypress(function(e) { - if(e.which == 13) { - insertImage(); - insertImageModal.modal('hide'); - } - }); +// $.ui.plugin is deprecated. Use $.widget() extensions instead. +$.ui.plugin = { + add: function( module, option, set ) { + var i, + proto = $.ui[ module ].prototype; + for ( i in set ) { + proto.plugins[ i ] = proto.plugins[ i ] || []; + proto.plugins[ i ].push( [ option, set[ i ] ] ); + } + }, + call: function( instance, name, args, allowDisconnected ) { + var i, + set = instance.plugins[ name ]; - insertButton.click(insertImage); + if ( !set ) { + return; + } - insertImageModal.on('shown', function() { - urlInput.focus(); - }); + if ( !allowDisconnected && ( !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) ) { + return; + } - insertImageModal.on('hide', function() { - self.editor.currentView.element.focus(); - }); + for ( i = 0; i < set.length; i++ ) { + if ( instance.options[ set[ i ][ 0 ] ] ) { + set[ i ][ 1 ].apply( instance.element, args ); + } + } + } +}; - toolbar.find('a[data-wysihtml5-command=insertImage]').click(function() { - var activeButton = $(this).hasClass("wysihtml5-command-active"); +})); - if (!activeButton) { - self.editor.currentView.element.focus(false); - caretBookmark = self.editor.composer.selection.getBookmark(); - insertImageModal.appendTo('body').modal('show'); - insertImageModal.on('click.dismiss.modal', '[data-dismiss="modal"]', function(e) { - e.stopPropagation(); - }); - return false; - } - else { - return true; - } - }); - }, - initInsertLink: function(toolbar) { - var self = this; - var insertLinkModal = toolbar.find('.bootstrap-wysihtml5-insert-link-modal'); - var urlInput = insertLinkModal.find('.bootstrap-wysihtml5-insert-link-url'); - var targetInput = insertLinkModal.find('.bootstrap-wysihtml5-insert-link-target'); - var insertButton = insertLinkModal.find('.btn-primary'); - var initialValue = urlInput.val(); - var caretBookmark; +/*! + * jQuery UI Datepicker 1.11.0 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/datepicker/ + */ - var insertLink = function() { - var url = urlInput.val(); - urlInput.val(initialValue); - self.editor.currentView.element.focus(); - if (caretBookmark) { - self.editor.composer.selection.setBookmark(caretBookmark); - caretBookmark = null; - } +(function( factory ) { + if ( typeof define === "function" && define.amd ) { - var newWindow = targetInput.prop("checked"); - self.editor.composer.commands.exec("createLink", { - 'href' : url, - 'target' : (newWindow ? '_blank' : '_self'), - 'rel' : (newWindow ? 'nofollow' : '') - }); - }; - var pressedEnter = false; + // AMD. Register as an anonymous module. + define([ + "jquery", + "./core" + ], factory ); + } else { - urlInput.keypress(function(e) { - if(e.which == 13) { - insertLink(); - insertLinkModal.modal('hide'); - } - }); + // Browser globals + factory( jQuery ); + } +}(function( $ ) { - insertButton.click(insertLink); +$.extend($.ui, { datepicker: { version: "1.11.0" } }); - insertLinkModal.on('shown', function() { - urlInput.focus(); - }); +var datepicker_instActive; - insertLinkModal.on('hide', function() { - self.editor.currentView.element.focus(); - }); +function datepicker_getZindex( elem ) { + var position, value; + while ( elem.length && elem[ 0 ] !== document ) { + // Ignore z-index if position is set to a value where z-index is ignored by the browser + // This makes behavior of this function consistent across browsers + // WebKit always returns auto if the element is positioned + position = elem.css( "position" ); + if ( position === "absolute" || position === "relative" || position === "fixed" ) { + // IE returns 0 when zIndex is not specified + // other browsers return a string + // we ignore the case of nested elements with an explicit value of 0 + // <div style="z-index: -10;"><div style="z-index: 0;"></div></div> + value = parseInt( elem.css( "zIndex" ), 10 ); + if ( !isNaN( value ) && value !== 0 ) { + return value; + } + } + elem = elem.parent(); + } - toolbar.find('a[data-wysihtml5-command=createLink]').click(function() { - var activeButton = $(this).hasClass("wysihtml5-command-active"); + return 0; +} +/* Date picker manager. + Use the singleton instance of this class, $.datepicker, to interact with the date picker. + Settings for (groups of) date pickers are maintained in an instance object, + allowing multiple different settings on the same page. */ - if (!activeButton) { - self.editor.currentView.element.focus(false); - caretBookmark = self.editor.composer.selection.getBookmark(); - insertLinkModal.appendTo('body').modal('show'); - insertLinkModal.on('click.dismiss.modal', '[data-dismiss="modal"]', function(e) { - e.stopPropagation(); - }); - return false; - } - else { - return true; - } - }); - } - }; +function Datepicker() { + this._curInst = null; // The current instance in use + this._keyEvent = false; // If the last event was a key event + this._disabledInputs = []; // List of date picker inputs that have been disabled + this._datepickerShowing = false; // True if the popup picker is showing , false if not + this._inDialog = false; // True if showing within a "dialog", false if not + this._mainDivId = "ui-datepicker-div"; // The ID of the main datepicker division + this._inlineClass = "ui-datepicker-inline"; // The name of the inline marker class + this._appendClass = "ui-datepicker-append"; // The name of the append marker class + this._triggerClass = "ui-datepicker-trigger"; // The name of the trigger marker class + this._dialogClass = "ui-datepicker-dialog"; // The name of the dialog marker class + this._disableClass = "ui-datepicker-disabled"; // The name of the disabled covering marker class + this._unselectableClass = "ui-datepicker-unselectable"; // The name of the unselectable cell marker class + this._currentClass = "ui-datepicker-current-day"; // The name of the current day marker class + this._dayOverClass = "ui-datepicker-days-cell-over"; // The name of the day hover marker class + this.regional = []; // Available regional settings, indexed by language code + this.regional[""] = { // Default regional settings + closeText: "Done", // Display text for close link + prevText: "Prev", // Display text for previous month link + nextText: "Next", // Display text for next month link + currentText: "Today", // Display text for current month link + monthNames: ["January","February","March","April","May","June", + "July","August","September","October","November","December"], // Names of months for drop-down and formatting + monthNamesShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], // For formatting + dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], // For formatting + dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], // For formatting + dayNamesMin: ["Su","Mo","Tu","We","Th","Fr","Sa"], // Column headings for days starting at Sunday + weekHeader: "Wk", // Column header for week of the year + dateFormat: "mm/dd/yy", // See format options on parseDate + firstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ... + isRTL: false, // True if right-to-left language, false if left-to-right + showMonthAfterYear: false, // True if the year select precedes month, false for month then year + yearSuffix: "" // Additional text to append to the year in the month headers + }; + this._defaults = { // Global defaults for all the date picker instances + showOn: "focus", // "focus" for popup on focus, + // "button" for trigger button, or "both" for either + showAnim: "fadeIn", // Name of jQuery animation for popup + showOptions: {}, // Options for enhanced animations + defaultDate: null, // Used when field is blank: actual date, + // +/-number for offset from today, null for today + appendText: "", // Display text following the input box, e.g. showing the format + buttonText: "...", // Text for trigger button + buttonImage: "", // URL for trigger button image + buttonImageOnly: false, // True if the image appears alone, false if it appears on a button + hideIfNoPrevNext: false, // True to hide next/previous month links + // if not applicable, false to just disable them + navigationAsDateFormat: false, // True if date formatting applied to prev/today/next links + gotoCurrent: false, // True if today link goes back to current selection instead + changeMonth: false, // True if month can be selected directly, false if only prev/next + changeYear: false, // True if year can be selected directly, false if only prev/next + yearRange: "c-10:c+10", // Range of years to display in drop-down, + // either relative to today's year (-nn:+nn), relative to currently displayed year + // (c-nn:c+nn), absolute (nnnn:nnnn), or a combination of the above (nnnn:-n) + showOtherMonths: false, // True to show dates in other months, false to leave blank + selectOtherMonths: false, // True to allow selection of dates in other months, false for unselectable + showWeek: false, // True to show week of the year, false to not show it + calculateWeek: this.iso8601Week, // How to calculate the week of the year, + // takes a Date and returns the number of the week for it + shortYearCutoff: "+10", // Short year values < this are in the current century, + // > this are in the previous century, + // string value starting with "+" for current year + value + minDate: null, // The earliest selectable date, or null for no limit + maxDate: null, // The latest selectable date, or null for no limit + duration: "fast", // Duration of display/closure + beforeShowDay: null, // Function that takes a date and returns an array with + // [0] = true if selectable, false if not, [1] = custom CSS class name(s) or "", + // [2] = cell title (optional), e.g. $.datepicker.noWeekends + beforeShow: null, // Function that takes an input field and + // returns a set of custom settings for the date picker + onSelect: null, // Define a callback function when a date is selected + onChangeMonthYear: null, // Define a callback function when the month or year is changed + onClose: null, // Define a callback function when the datepicker is closed + numberOfMonths: 1, // Number of months to show at a time + showCurrentAtPos: 0, // The position in multipe months at which to show the current month (starting at 0) + stepMonths: 1, // Number of months to step back/forward + stepBigMonths: 12, // Number of months to step back/forward for the big links + altField: "", // Selector for an alternate field to store selected dates into + altFormat: "", // The date format to use for the alternate field + constrainInput: true, // The input is constrained by the current date format + showButtonPanel: false, // True to show button panel, false to not show it + autoSize: false, // True to size the input for the date format, false to leave as is + disabled: false // The initial disabled state + }; + $.extend(this._defaults, this.regional[""]); + this.regional.en = $.extend( true, {}, this.regional[ "" ]); + this.regional[ "en-US" ] = $.extend( true, {}, this.regional.en ); + this.dpDiv = datepicker_bindHover($("<div id='" + this._mainDivId + "' class='ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")); +} - // these define our public api - var methods = { - resetDefaults: function() { - $.fn.wysihtml5.defaultOptions = $.extend(true, {}, $.fn.wysihtml5.defaultOptionsCache); - }, - bypassDefaults: function(options) { - return this.each(function () { - var $this = $(this); - $this.data('wysihtml5', new Wysihtml5($this, options)); - }); - }, - shallowExtend: function (options) { - var settings = $.extend({}, $.fn.wysihtml5.defaultOptions, options || {}, $(this).data()); - var that = this; - return methods.bypassDefaults.apply(that, [settings]); - }, - deepExtend: function(options) { - var settings = $.extend(true, {}, $.fn.wysihtml5.defaultOptions, options || {}); - var that = this; - return methods.bypassDefaults.apply(that, [settings]); - }, - init: function(options) { - var that = this; - return methods.shallowExtend.apply(that, [options]); - } - }; +$.extend(Datepicker.prototype, { + /* Class name added to elements to indicate already configured with a date picker. */ + markerClassName: "hasDatepicker", - $.fn.wysihtml5 = function ( method ) { - if ( methods[method] ) { - return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 )); - } else if ( typeof method === 'object' || ! method ) { - return methods.init.apply( this, arguments ); - } else { - $.error( 'Method ' + method + ' does not exist on jQuery.wysihtml5' ); - } - }; + //Keep track of the maximum number of rows displayed (see #7043) + maxRows: 4, - $.fn.wysihtml5.Constructor = Wysihtml5; + // TODO rename to "widget" when switching to widget factory + _widgetDatepicker: function() { + return this.dpDiv; + }, - var defaultOptions = $.fn.wysihtml5.defaultOptions = { - "font-styles": true, - "color": false, - "emphasis": true, - "lists": true, - "html": false, - "link": true, - "image": true, - "size": 'sm', - events: {}, - parserRules: { - classes: { - // (path_to_project/lib/css/bootstrap3-wysiwyg5-color.css) - "wysiwyg-color-silver" : 1, - "wysiwyg-color-gray" : 1, - "wysiwyg-color-white" : 1, - "wysiwyg-color-maroon" : 1, - "wysiwyg-color-red" : 1, - "wysiwyg-color-purple" : 1, - "wysiwyg-color-fuchsia" : 1, - "wysiwyg-color-green" : 1, - "wysiwyg-color-lime" : 1, - "wysiwyg-color-olive" : 1, - "wysiwyg-color-yellow" : 1, - "wysiwyg-color-navy" : 1, - "wysiwyg-color-blue" : 1, - "wysiwyg-color-teal" : 1, - "wysiwyg-color-aqua" : 1, - "wysiwyg-color-orange" : 1 - }, - tags: { - "b": {}, - "i": {}, - "br": {}, - "ol": {}, - "ul": {}, - "li": {}, - "h1": {}, - "h2": {}, - "h3": {}, - "h4": {}, - "h5": {}, - "h6": {}, - "blockquote": {}, - "u": 1, - "img": { - "check_attributes": { - "width": "numbers", - "alt": "alt", - "src": "url", - "height": "numbers" - } - }, - "a": { - check_attributes: { - 'href': "url", // important to avoid XSS - 'target': 'alt', - 'rel': 'alt' - } - }, - "span": 1, - "div": 1, - // to allow save and edit files with code tag hacks - "code": 1, - "pre": 1 - } - }, - stylesheets: ["/assets/bootstrap3-wysiwyg5-color.css"], // (path_to_project/lib/css/bootstrap3-wysiwyg5-color.css) - locale: "en" - }; + /* Override the default settings for all instances of the date picker. + * @param settings object - the new settings to use as defaults (anonymous object) + * @return the manager object + */ + setDefaults: function(settings) { + datepicker_extendRemove(this._defaults, settings || {}); + return this; + }, - if (typeof $.fn.wysihtml5.defaultOptionsCache === 'undefined') { - $.fn.wysihtml5.defaultOptionsCache = $.extend(true, {}, $.fn.wysihtml5.defaultOptions); - } + /* Attach the date picker to a jQuery selection. + * @param target element - the target input field or division or span + * @param settings object - the new settings to use for this date picker instance (anonymous) + */ + _attachDatepicker: function(target, settings) { + var nodeName, inline, inst; + nodeName = target.nodeName.toLowerCase(); + inline = (nodeName === "div" || nodeName === "span"); + if (!target.id) { + this.uuid += 1; + target.id = "dp" + this.uuid; + } + inst = this._newInst($(target), inline); + inst.settings = $.extend({}, settings || {}); + if (nodeName === "input") { + this._connectDatepicker(target, inst); + } else if (inline) { + this._inlineDatepicker(target, inst); + } + }, - var locale = $.fn.wysihtml5.locale = { - en: { - font_styles: { - normal: "Normal text", - h1: "Heading 1", - h2: "Heading 2", - h3: "Heading 3", - h4: "Heading 4", - h5: "Heading 5", - h6: "Heading 6" - }, - emphasis: { - bold: "Bold", - italic: "Italic", - underline: "Underline" - }, - lists: { - unordered: "Unordered list", - ordered: "Ordered list", - outdent: "Outdent", - indent: "Indent" - }, - link: { - insert: "Insert link", - cancel: "Cancel", - target: "Open link in new window" - }, - image: { - insert: "Insert image", - cancel: "Cancel" - }, - html: { - edit: "Edit HTML" - }, - colours: { - black: "Black", - silver: "Silver", - gray: "Grey", - maroon: "Maroon", - red: "Red", - purple: "Purple", - green: "Green", - olive: "Olive", - navy: "Navy", - blue: "Blue", - orange: "Orange" - } - } - }; + /* Create a new instance object. */ + _newInst: function(target, inline) { + var id = target[0].id.replace(/([^A-Za-z0-9_\-])/g, "\\\\$1"); // escape jQuery meta chars + return {id: id, input: target, // associated target + selectedDay: 0, selectedMonth: 0, selectedYear: 0, // current selection + drawMonth: 0, drawYear: 0, // month being drawn + inline: inline, // is datepicker inline or not + dpDiv: (!inline ? this.dpDiv : // presentation div + datepicker_bindHover($("<div class='" + this._inlineClass + " ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all'></div>")))}; + }, -}(window.jQuery, window.wysihtml5); + /* Attach the date picker to an input field. */ + _connectDatepicker: function(target, inst) { + var input = $(target); + inst.append = $([]); + inst.trigger = $([]); + if (input.hasClass(this.markerClassName)) { + return; + } + this._attachments(input, inst); + input.addClass(this.markerClassName).keydown(this._doKeyDown). + keypress(this._doKeyPress).keyup(this._doKeyUp); + this._autoSize(inst); + $.data(target, "datepicker", inst); + //If disabled option is true, disable the datepicker once it has been attached to the input (see ticket #5665) + if( inst.settings.disabled ) { + this._disableDatepicker( target ); + } + }, + + /* Make attachments based on settings. */ + _attachments: function(input, inst) { + var showOn, buttonText, buttonImage, + appendText = this._get(inst, "appendText"), + isRTL = this._get(inst, "isRTL"); + + if (inst.append) { + inst.append.remove(); + } + if (appendText) { + inst.append = $("<span class='" + this._appendClass + "'>" + appendText + "</span>"); + input[isRTL ? "before" : "after"](inst.append); + } + + input.unbind("focus", this._showDatepicker); + + if (inst.trigger) { + inst.trigger.remove(); + } + + showOn = this._get(inst, "showOn"); + if (showOn === "focus" || showOn === "both") { // pop-up date picker when in the marked field + input.focus(this._showDatepicker); + } + if (showOn === "button" || showOn === "both") { // pop-up date picker when button clicked + buttonText = this._get(inst, "buttonText"); + buttonImage = this._get(inst, "buttonImage"); + inst.trigger = $(this._get(inst, "buttonImageOnly") ? + $("<img/>").addClass(this._triggerClass). + attr({ src: buttonImage, alt: buttonText, title: buttonText }) : + $("<button type='button'></button>").addClass(this._triggerClass). + html(!buttonImage ? buttonText : $("<img/>").attr( + { src:buttonImage, alt:buttonText, title:buttonText }))); + input[isRTL ? "before" : "after"](inst.trigger); + inst.trigger.click(function() { + if ($.datepicker._datepickerShowing && $.datepicker._lastInput === input[0]) { + $.datepicker._hideDatepicker(); + } else if ($.datepicker._datepickerShowing && $.datepicker._lastInput !== input[0]) { + $.datepicker._hideDatepicker(); + $.datepicker._showDatepicker(input[0]); + } else { + $.datepicker._showDatepicker(input[0]); + } + return false; + }); + } + }, + + /* Apply the maximum length for the date format. */ + _autoSize: function(inst) { + if (this._get(inst, "autoSize") && !inst.inline) { + var findMax, max, maxI, i, + date = new Date(2009, 12 - 1, 20), // Ensure double digits + dateFormat = this._get(inst, "dateFormat"); + + if (dateFormat.match(/[DM]/)) { + findMax = function(names) { + max = 0; + maxI = 0; + for (i = 0; i < names.length; i++) { + if (names[i].length > max) { + max = names[i].length; + maxI = i; + } + } + return maxI; + }; + date.setMonth(findMax(this._get(inst, (dateFormat.match(/MM/) ? + "monthNames" : "monthNamesShort")))); + date.setDate(findMax(this._get(inst, (dateFormat.match(/DD/) ? + "dayNames" : "dayNamesShort"))) + 20 - date.getDay()); + } + inst.input.attr("size", this._formatDate(inst, date).length); + } + }, + + /* Attach an inline date picker to a div. */ + _inlineDatepicker: function(target, inst) { + var divSpan = $(target); + if (divSpan.hasClass(this.markerClassName)) { + return; + } + divSpan.addClass(this.markerClassName).append(inst.dpDiv); + $.data(target, "datepicker", inst); + this._setDate(inst, this._getDefaultDate(inst), true); + this._updateDatepicker(inst); + this._updateAlternate(inst); + //If disabled option is true, disable the datepicker before showing it (see ticket #5665) + if( inst.settings.disabled ) { + this._disableDatepicker( target ); + } + // Set display:block in place of inst.dpDiv.show() which won't work on disconnected elements + // http://bugs.jqueryui.com/ticket/7552 - A Datepicker created on a detached div has zero height + inst.dpDiv.css( "display", "block" ); + }, + + /* Pop-up the date picker in a "dialog" box. + * @param input element - ignored + * @param date string or Date - the initial date to display + * @param onSelect function - the function to call when a date is selected + * @param settings object - update the dialog date picker instance's settings (anonymous object) + * @param pos int[2] - coordinates for the dialog's position within the screen or + * event - with x/y coordinates or + * leave empty for default (screen centre) + * @return the manager object + */ + _dialogDatepicker: function(input, date, onSelect, settings, pos) { + var id, browserWidth, browserHeight, scrollX, scrollY, + inst = this._dialogInst; // internal instance + + if (!inst) { + this.uuid += 1; + id = "dp" + this.uuid; + this._dialogInput = $("<input type='text' id='" + id + + "' style='position: absolute; top: -100px; width: 0px;'/>"); + this._dialogInput.keydown(this._doKeyDown); + $("body").append(this._dialogInput); + inst = this._dialogInst = this._newInst(this._dialogInput, false); + inst.settings = {}; + $.data(this._dialogInput[0], "datepicker", inst); + } + datepicker_extendRemove(inst.settings, settings || {}); + date = (date && date.constructor === Date ? this._formatDate(inst, date) : date); + this._dialogInput.val(date); + + this._pos = (pos ? (pos.length ? pos : [pos.pageX, pos.pageY]) : null); + if (!this._pos) { + browserWidth = document.documentElement.clientWidth; + browserHeight = document.documentElement.clientHeight; + scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; + scrollY = document.documentElement.scrollTop || document.body.scrollTop; + this._pos = // should use actual width/height below + [(browserWidth / 2) - 100 + scrollX, (browserHeight / 2) - 150 + scrollY]; + } + + // move input on screen for focus, but hidden behind dialog + this._dialogInput.css("left", (this._pos[0] + 20) + "px").css("top", this._pos[1] + "px"); + inst.settings.onSelect = onSelect; + this._inDialog = true; + this.dpDiv.addClass(this._dialogClass); + this._showDatepicker(this._dialogInput[0]); + if ($.blockUI) { + $.blockUI(this.dpDiv); + } + $.data(this._dialogInput[0], "datepicker", inst); + return this; + }, + + /* Detach a datepicker from its control. + * @param target element - the target input field or division or span + */ + _destroyDatepicker: function(target) { + var nodeName, + $target = $(target), + inst = $.data(target, "datepicker"); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + $.removeData(target, "datepicker"); + if (nodeName === "input") { + inst.append.remove(); + inst.trigger.remove(); + $target.removeClass(this.markerClassName). + unbind("focus", this._showDatepicker). + unbind("keydown", this._doKeyDown). + unbind("keypress", this._doKeyPress). + unbind("keyup", this._doKeyUp); + } else if (nodeName === "div" || nodeName === "span") { + $target.removeClass(this.markerClassName).empty(); + } + }, + + /* Enable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _enableDatepicker: function(target) { + var nodeName, inline, + $target = $(target), + inst = $.data(target, "datepicker"); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + if (nodeName === "input") { + target.disabled = false; + inst.trigger.filter("button"). + each(function() { this.disabled = false; }).end(). + filter("img").css({opacity: "1.0", cursor: ""}); + } else if (nodeName === "div" || nodeName === "span") { + inline = $target.children("." + this._inlineClass); + inline.children().removeClass("ui-state-disabled"); + inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). + prop("disabled", false); + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value === target ? null : value); }); // delete entry + }, + + /* Disable the date picker to a jQuery selection. + * @param target element - the target input field or division or span + */ + _disableDatepicker: function(target) { + var nodeName, inline, + $target = $(target), + inst = $.data(target, "datepicker"); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + + nodeName = target.nodeName.toLowerCase(); + if (nodeName === "input") { + target.disabled = true; + inst.trigger.filter("button"). + each(function() { this.disabled = true; }).end(). + filter("img").css({opacity: "0.5", cursor: "default"}); + } else if (nodeName === "div" || nodeName === "span") { + inline = $target.children("." + this._inlineClass); + inline.children().addClass("ui-state-disabled"); + inline.find("select.ui-datepicker-month, select.ui-datepicker-year"). + prop("disabled", true); + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value === target ? null : value); }); // delete entry + this._disabledInputs[this._disabledInputs.length] = target; + }, + + /* Is the first field in a jQuery collection disabled as a datepicker? + * @param target element - the target input field or division or span + * @return boolean - true if disabled, false if enabled + */ + _isDisabledDatepicker: function(target) { + if (!target) { + return false; + } + for (var i = 0; i < this._disabledInputs.length; i++) { + if (this._disabledInputs[i] === target) { + return true; + } + } + return false; + }, + + /* Retrieve the instance data for the target control. + * @param target element - the target input field or division or span + * @return object - the associated instance data + * @throws error if a jQuery problem getting data + */ + _getInst: function(target) { + try { + return $.data(target, "datepicker"); + } + catch (err) { + throw "Missing instance data for this datepicker"; + } + }, + + /* Update or retrieve the settings for a date picker attached to an input field or division. + * @param target element - the target input field or division or span + * @param name object - the new settings to update or + * string - the name of the setting to change or retrieve, + * when retrieving also "all" for all instance settings or + * "defaults" for all global defaults + * @param value any - the new value for the setting + * (omit if above is an object or to retrieve a value) + */ + _optionDatepicker: function(target, name, value) { + var settings, date, minDate, maxDate, + inst = this._getInst(target); + + if (arguments.length === 2 && typeof name === "string") { + return (name === "defaults" ? $.extend({}, $.datepicker._defaults) : + (inst ? (name === "all" ? $.extend({}, inst.settings) : + this._get(inst, name)) : null)); + } + + settings = name || {}; + if (typeof name === "string") { + settings = {}; + settings[name] = value; + } + + if (inst) { + if (this._curInst === inst) { + this._hideDatepicker(); + } + + date = this._getDateDatepicker(target, true); + minDate = this._getMinMaxDate(inst, "min"); + maxDate = this._getMinMaxDate(inst, "max"); + datepicker_extendRemove(inst.settings, settings); + // reformat the old minDate/maxDate values if dateFormat changes and a new minDate/maxDate isn't provided + if (minDate !== null && settings.dateFormat !== undefined && settings.minDate === undefined) { + inst.settings.minDate = this._formatDate(inst, minDate); + } + if (maxDate !== null && settings.dateFormat !== undefined && settings.maxDate === undefined) { + inst.settings.maxDate = this._formatDate(inst, maxDate); + } + if ( "disabled" in settings ) { + if ( settings.disabled ) { + this._disableDatepicker(target); + } else { + this._enableDatepicker(target); + } + } + this._attachments($(target), inst); + this._autoSize(inst); + this._setDate(inst, date); + this._updateAlternate(inst); + this._updateDatepicker(inst); + } + }, + + // change method deprecated + _changeDatepicker: function(target, name, value) { + this._optionDatepicker(target, name, value); + }, + + /* Redraw the date picker attached to an input field or division. + * @param target element - the target input field or division or span + */ + _refreshDatepicker: function(target) { + var inst = this._getInst(target); + if (inst) { + this._updateDatepicker(inst); + } + }, + + /* Set the dates for a jQuery selection. + * @param target element - the target input field or division or span + * @param date Date - the new date + */ + _setDateDatepicker: function(target, date) { + var inst = this._getInst(target); + if (inst) { + this._setDate(inst, date); + this._updateDatepicker(inst); + this._updateAlternate(inst); + } + }, + + /* Get the date(s) for the first entry in a jQuery selection. + * @param target element - the target input field or division or span + * @param noDefault boolean - true if no default date is to be used + * @return Date - the current date + */ + _getDateDatepicker: function(target, noDefault) { + var inst = this._getInst(target); + if (inst && !inst.inline) { + this._setDateFromField(inst, noDefault); + } + return (inst ? this._getDate(inst) : null); + }, + + /* Handle keystrokes. */ + _doKeyDown: function(event) { + var onSelect, dateStr, sel, + inst = $.datepicker._getInst(event.target), + handled = true, + isRTL = inst.dpDiv.is(".ui-datepicker-rtl"); + + inst._keyEvent = true; + if ($.datepicker._datepickerShowing) { + switch (event.keyCode) { + case 9: $.datepicker._hideDatepicker(); + handled = false; + break; // hide on tab out + case 13: sel = $("td." + $.datepicker._dayOverClass + ":not(." + + $.datepicker._currentClass + ")", inst.dpDiv); + if (sel[0]) { + $.datepicker._selectDay(event.target, inst.selectedMonth, inst.selectedYear, sel[0]); + } + + onSelect = $.datepicker._get(inst, "onSelect"); + if (onSelect) { + dateStr = $.datepicker._formatDate(inst); + + // trigger custom callback + onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); + } else { + $.datepicker._hideDatepicker(); + } + + return false; // don't submit the form + case 27: $.datepicker._hideDatepicker(); + break; // hide on escape + case 33: $.datepicker._adjustDate(event.target, (event.ctrlKey ? + -$.datepicker._get(inst, "stepBigMonths") : + -$.datepicker._get(inst, "stepMonths")), "M"); + break; // previous month/year on page up/+ ctrl + case 34: $.datepicker._adjustDate(event.target, (event.ctrlKey ? + +$.datepicker._get(inst, "stepBigMonths") : + +$.datepicker._get(inst, "stepMonths")), "M"); + break; // next month/year on page down/+ ctrl + case 35: if (event.ctrlKey || event.metaKey) { + $.datepicker._clearDate(event.target); + } + handled = event.ctrlKey || event.metaKey; + break; // clear on ctrl or command +end + case 36: if (event.ctrlKey || event.metaKey) { + $.datepicker._gotoToday(event.target); + } + handled = event.ctrlKey || event.metaKey; + break; // current on ctrl or command +home + case 37: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, (isRTL ? +1 : -1), "D"); + } + handled = event.ctrlKey || event.metaKey; + // -1 day on ctrl or command +left + if (event.originalEvent.altKey) { + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + -$.datepicker._get(inst, "stepBigMonths") : + -$.datepicker._get(inst, "stepMonths")), "M"); + } + // next month/year on alt +left on Mac + break; + case 38: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, -7, "D"); + } + handled = event.ctrlKey || event.metaKey; + break; // -1 week on ctrl or command +up + case 39: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, (isRTL ? -1 : +1), "D"); + } + handled = event.ctrlKey || event.metaKey; + // +1 day on ctrl or command +right + if (event.originalEvent.altKey) { + $.datepicker._adjustDate(event.target, (event.ctrlKey ? + +$.datepicker._get(inst, "stepBigMonths") : + +$.datepicker._get(inst, "stepMonths")), "M"); + } + // next month/year on alt +right + break; + case 40: if (event.ctrlKey || event.metaKey) { + $.datepicker._adjustDate(event.target, +7, "D"); + } + handled = event.ctrlKey || event.metaKey; + break; // +1 week on ctrl or command +down + default: handled = false; + } + } else if (event.keyCode === 36 && event.ctrlKey) { // display the date picker on ctrl+home + $.datepicker._showDatepicker(this); + } else { + handled = false; + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }, + + /* Filter entered characters - based on date format. */ + _doKeyPress: function(event) { + var chars, chr, + inst = $.datepicker._getInst(event.target); + + if ($.datepicker._get(inst, "constrainInput")) { + chars = $.datepicker._possibleChars($.datepicker._get(inst, "dateFormat")); + chr = String.fromCharCode(event.charCode == null ? event.keyCode : event.charCode); + return event.ctrlKey || event.metaKey || (chr < " " || !chars || chars.indexOf(chr) > -1); + } + }, + + /* Synchronise manual entry and field/alternate field. */ + _doKeyUp: function(event) { + var date, + inst = $.datepicker._getInst(event.target); + + if (inst.input.val() !== inst.lastVal) { + try { + date = $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), + (inst.input ? inst.input.val() : null), + $.datepicker._getFormatConfig(inst)); + + if (date) { // only if valid + $.datepicker._setDateFromField(inst); + $.datepicker._updateAlternate(inst); + $.datepicker._updateDatepicker(inst); + } + } + catch (err) { + } + } + return true; + }, + + /* Pop-up the date picker for a given input field. + * If false returned from beforeShow event handler do not show. + * @param input element - the input field attached to the date picker or + * event - if triggered by focus + */ + _showDatepicker: function(input) { + input = input.target || input; + if (input.nodeName.toLowerCase() !== "input") { // find from button/image trigger + input = $("input", input.parentNode)[0]; + } + + if ($.datepicker._isDisabledDatepicker(input) || $.datepicker._lastInput === input) { // already here + return; + } + + var inst, beforeShow, beforeShowSettings, isFixed, + offset, showAnim, duration; + + inst = $.datepicker._getInst(input); + if ($.datepicker._curInst && $.datepicker._curInst !== inst) { + $.datepicker._curInst.dpDiv.stop(true, true); + if ( inst && $.datepicker._datepickerShowing ) { + $.datepicker._hideDatepicker( $.datepicker._curInst.input[0] ); + } + } + + beforeShow = $.datepicker._get(inst, "beforeShow"); + beforeShowSettings = beforeShow ? beforeShow.apply(input, [input, inst]) : {}; + if(beforeShowSettings === false){ + return; + } + datepicker_extendRemove(inst.settings, beforeShowSettings); + + inst.lastVal = null; + $.datepicker._lastInput = input; + $.datepicker._setDateFromField(inst); + + if ($.datepicker._inDialog) { // hide cursor + input.value = ""; + } + if (!$.datepicker._pos) { // position below input + $.datepicker._pos = $.datepicker._findPos(input); + $.datepicker._pos[1] += input.offsetHeight; // add the height + } + + isFixed = false; + $(input).parents().each(function() { + isFixed |= $(this).css("position") === "fixed"; + return !isFixed; + }); + + offset = {left: $.datepicker._pos[0], top: $.datepicker._pos[1]}; + $.datepicker._pos = null; + //to avoid flashes on Firefox + inst.dpDiv.empty(); + // determine sizing offscreen + inst.dpDiv.css({position: "absolute", display: "block", top: "-1000px"}); + $.datepicker._updateDatepicker(inst); + // fix width for dynamic number of date pickers + // and adjust position before showing + offset = $.datepicker._checkOffset(inst, offset, isFixed); + inst.dpDiv.css({position: ($.datepicker._inDialog && $.blockUI ? + "static" : (isFixed ? "fixed" : "absolute")), display: "none", + left: offset.left + "px", top: offset.top + "px"}); + + if (!inst.inline) { + showAnim = $.datepicker._get(inst, "showAnim"); + duration = $.datepicker._get(inst, "duration"); + inst.dpDiv.css( "z-index", datepicker_getZindex( $( input ) ) + 1 ); + $.datepicker._datepickerShowing = true; + + if ( $.effects && $.effects.effect[ showAnim ] ) { + inst.dpDiv.show(showAnim, $.datepicker._get(inst, "showOptions"), duration); + } else { + inst.dpDiv[showAnim || "show"](showAnim ? duration : null); + } + + if ( $.datepicker._shouldFocusInput( inst ) ) { + inst.input.focus(); + } + + $.datepicker._curInst = inst; + } + }, + + /* Generate the date picker content. */ + _updateDatepicker: function(inst) { + this.maxRows = 4; //Reset the max number of rows being displayed (see #7043) + datepicker_instActive = inst; // for delegate hover events + inst.dpDiv.empty().append(this._generateHTML(inst)); + this._attachHandlers(inst); + inst.dpDiv.find("." + this._dayOverClass + " a"); + + var origyearshtml, + numMonths = this._getNumberOfMonths(inst), + cols = numMonths[1], + width = 17; + + inst.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""); + if (cols > 1) { + inst.dpDiv.addClass("ui-datepicker-multi-" + cols).css("width", (width * cols) + "em"); + } + inst.dpDiv[(numMonths[0] !== 1 || numMonths[1] !== 1 ? "add" : "remove") + + "Class"]("ui-datepicker-multi"); + inst.dpDiv[(this._get(inst, "isRTL") ? "add" : "remove") + + "Class"]("ui-datepicker-rtl"); + + if (inst === $.datepicker._curInst && $.datepicker._datepickerShowing && $.datepicker._shouldFocusInput( inst ) ) { + inst.input.focus(); + } + + // deffered render of the years select (to avoid flashes on Firefox) + if( inst.yearshtml ){ + origyearshtml = inst.yearshtml; + setTimeout(function(){ + //assure that inst.yearshtml didn't change. + if( origyearshtml === inst.yearshtml && inst.yearshtml ){ + inst.dpDiv.find("select.ui-datepicker-year:first").replaceWith(inst.yearshtml); + } + origyearshtml = inst.yearshtml = null; + }, 0); + } + }, + + // #6694 - don't focus the input if it's already focused + // this breaks the change event in IE + // Support: IE and jQuery <1.9 + _shouldFocusInput: function( inst ) { + return inst.input && inst.input.is( ":visible" ) && !inst.input.is( ":disabled" ) && !inst.input.is( ":focus" ); + }, + + /* Check positioning to remain on screen. */ + _checkOffset: function(inst, offset, isFixed) { + var dpWidth = inst.dpDiv.outerWidth(), + dpHeight = inst.dpDiv.outerHeight(), + inputWidth = inst.input ? inst.input.outerWidth() : 0, + inputHeight = inst.input ? inst.input.outerHeight() : 0, + viewWidth = document.documentElement.clientWidth + (isFixed ? 0 : $(document).scrollLeft()), + viewHeight = document.documentElement.clientHeight + (isFixed ? 0 : $(document).scrollTop()); + + offset.left -= (this._get(inst, "isRTL") ? (dpWidth - inputWidth) : 0); + offset.left -= (isFixed && offset.left === inst.input.offset().left) ? $(document).scrollLeft() : 0; + offset.top -= (isFixed && offset.top === (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0; + + // now check if datepicker is showing outside window viewport - move to a better place if so. + offset.left -= Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ? + Math.abs(offset.left + dpWidth - viewWidth) : 0); + offset.top -= Math.min(offset.top, (offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ? + Math.abs(dpHeight + inputHeight) : 0); + + return offset; + }, + + /* Find an object's position on the screen. */ + _findPos: function(obj) { + var position, + inst = this._getInst(obj), + isRTL = this._get(inst, "isRTL"); + + while (obj && (obj.type === "hidden" || obj.nodeType !== 1 || $.expr.filters.hidden(obj))) { + obj = obj[isRTL ? "previousSibling" : "nextSibling"]; + } + + position = $(obj).offset(); + return [position.left, position.top]; + }, + + /* Hide the date picker from view. + * @param input element - the input field attached to the date picker + */ + _hideDatepicker: function(input) { + var showAnim, duration, postProcess, onClose, + inst = this._curInst; + + if (!inst || (input && inst !== $.data(input, "datepicker"))) { + return; + } + + if (this._datepickerShowing) { + showAnim = this._get(inst, "showAnim"); + duration = this._get(inst, "duration"); + postProcess = function() { + $.datepicker._tidyDialog(inst); + }; + + // DEPRECATED: after BC for 1.8.x $.effects[ showAnim ] is not needed + if ( $.effects && ( $.effects.effect[ showAnim ] || $.effects[ showAnim ] ) ) { + inst.dpDiv.hide(showAnim, $.datepicker._get(inst, "showOptions"), duration, postProcess); + } else { + inst.dpDiv[(showAnim === "slideDown" ? "slideUp" : + (showAnim === "fadeIn" ? "fadeOut" : "hide"))]((showAnim ? duration : null), postProcess); + } + + if (!showAnim) { + postProcess(); + } + this._datepickerShowing = false; + + onClose = this._get(inst, "onClose"); + if (onClose) { + onClose.apply((inst.input ? inst.input[0] : null), [(inst.input ? inst.input.val() : ""), inst]); + } + + this._lastInput = null; + if (this._inDialog) { + this._dialogInput.css({ position: "absolute", left: "0", top: "-100px" }); + if ($.blockUI) { + $.unblockUI(); + $("body").append(this.dpDiv); + } + } + this._inDialog = false; + } + }, + + /* Tidy up after a dialog display. */ + _tidyDialog: function(inst) { + inst.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar"); + }, + + /* Close date picker if clicked elsewhere. */ + _checkExternalClick: function(event) { + if (!$.datepicker._curInst) { + return; + } + + var $target = $(event.target), + inst = $.datepicker._getInst($target[0]); + + if ( ( ( $target[0].id !== $.datepicker._mainDivId && + $target.parents("#" + $.datepicker._mainDivId).length === 0 && + !$target.hasClass($.datepicker.markerClassName) && + !$target.closest("." + $.datepicker._triggerClass).length && + $.datepicker._datepickerShowing && !($.datepicker._inDialog && $.blockUI) ) ) || + ( $target.hasClass($.datepicker.markerClassName) && $.datepicker._curInst !== inst ) ) { + $.datepicker._hideDatepicker(); + } + }, + + /* Adjust one of the date sub-fields. */ + _adjustDate: function(id, offset, period) { + var target = $(id), + inst = this._getInst(target[0]); + + if (this._isDisabledDatepicker(target[0])) { + return; + } + this._adjustInstDate(inst, offset + + (period === "M" ? this._get(inst, "showCurrentAtPos") : 0), // undo positioning + period); + this._updateDatepicker(inst); + }, + + /* Action for current link. */ + _gotoToday: function(id) { + var date, + target = $(id), + inst = this._getInst(target[0]); + + if (this._get(inst, "gotoCurrent") && inst.currentDay) { + inst.selectedDay = inst.currentDay; + inst.drawMonth = inst.selectedMonth = inst.currentMonth; + inst.drawYear = inst.selectedYear = inst.currentYear; + } else { + date = new Date(); + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + } + this._notifyChange(inst); + this._adjustDate(target); + }, + + /* Action for selecting a new month/year. */ + _selectMonthYear: function(id, select, period) { + var target = $(id), + inst = this._getInst(target[0]); + + inst["selected" + (period === "M" ? "Month" : "Year")] = + inst["draw" + (period === "M" ? "Month" : "Year")] = + parseInt(select.options[select.selectedIndex].value,10); + + this._notifyChange(inst); + this._adjustDate(target); + }, + + /* Action for selecting a day. */ + _selectDay: function(id, month, year, td) { + var inst, + target = $(id); + + if ($(td).hasClass(this._unselectableClass) || this._isDisabledDatepicker(target[0])) { + return; + } + + inst = this._getInst(target[0]); + inst.selectedDay = inst.currentDay = $("a", td).html(); + inst.selectedMonth = inst.currentMonth = month; + inst.selectedYear = inst.currentYear = year; + this._selectDate(id, this._formatDate(inst, + inst.currentDay, inst.currentMonth, inst.currentYear)); + }, + + /* Erase the input field and hide the date picker. */ + _clearDate: function(id) { + var target = $(id); + this._selectDate(target, ""); + }, + + /* Update the input field with the selected date. */ + _selectDate: function(id, dateStr) { + var onSelect, + target = $(id), + inst = this._getInst(target[0]); + + dateStr = (dateStr != null ? dateStr : this._formatDate(inst)); + if (inst.input) { + inst.input.val(dateStr); + } + this._updateAlternate(inst); + + onSelect = this._get(inst, "onSelect"); + if (onSelect) { + onSelect.apply((inst.input ? inst.input[0] : null), [dateStr, inst]); // trigger custom callback + } else if (inst.input) { + inst.input.trigger("change"); // fire the change event + } + + if (inst.inline){ + this._updateDatepicker(inst); + } else { + this._hideDatepicker(); + this._lastInput = inst.input[0]; + if (typeof(inst.input[0]) !== "object") { + inst.input.focus(); // restore focus + } + this._lastInput = null; + } + }, + + /* Update any alternate field to synchronise with the main field. */ + _updateAlternate: function(inst) { + var altFormat, date, dateStr, + altField = this._get(inst, "altField"); + + if (altField) { // update alternate field too + altFormat = this._get(inst, "altFormat") || this._get(inst, "dateFormat"); + date = this._getDate(inst); + dateStr = this.formatDate(altFormat, date, this._getFormatConfig(inst)); + $(altField).each(function() { $(this).val(dateStr); }); + } + }, + + /* Set as beforeShowDay function to prevent selection of weekends. + * @param date Date - the date to customise + * @return [boolean, string] - is this date selectable?, what is its CSS class? + */ + noWeekends: function(date) { + var day = date.getDay(); + return [(day > 0 && day < 6), ""]; + }, + + /* Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. + * @param date Date - the date to get the week for + * @return number - the number of the week within the year that contains this date + */ + iso8601Week: function(date) { + var time, + checkDate = new Date(date.getTime()); + + // Find Thursday of this week starting on Monday + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); + + time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + }, + + /* Parse a string value into a date object. + * See formatDate below for the possible formats. + * + * @param format string - the expected format of the date + * @param value string - the date in the above format + * @param settings Object - attributes include: + * shortYearCutoff number - the cutoff year for determining the century (optional) + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return Date - the extracted date value or null if value is blank + */ + parseDate: function (format, value, settings) { + if (format == null || value == null) { + throw "Invalid arguments"; + } + + value = (typeof value === "object" ? value.toString() : value + ""); + if (value === "") { + return null; + } + + var iFormat, dim, extra, + iValue = 0, + shortYearCutoffTemp = (settings ? settings.shortYearCutoff : null) || this._defaults.shortYearCutoff, + shortYearCutoff = (typeof shortYearCutoffTemp !== "string" ? shortYearCutoffTemp : + new Date().getFullYear() % 100 + parseInt(shortYearCutoffTemp, 10)), + dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, + dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, + monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, + monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, + year = -1, + month = -1, + day = -1, + doy = -1, + literal = false, + date, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }, + // Extract a number from the string value + getNumber = function(match) { + var isDoubled = lookAhead(match), + size = (match === "@" ? 14 : (match === "!" ? 20 : + (match === "y" && isDoubled ? 4 : (match === "o" ? 3 : 2)))), + digits = new RegExp("^\\d{1," + size + "}"), + num = value.substring(iValue).match(digits); + if (!num) { + throw "Missing number at position " + iValue; + } + iValue += num[0].length; + return parseInt(num[0], 10); + }, + // Extract a name from the string value and convert to an index + getName = function(match, shortNames, longNames) { + var index = -1, + names = $.map(lookAhead(match) ? longNames : shortNames, function (v, k) { + return [ [k, v] ]; + }).sort(function (a, b) { + return -(a[1].length - b[1].length); + }); + + $.each(names, function (i, pair) { + var name = pair[1]; + if (value.substr(iValue, name.length).toLowerCase() === name.toLowerCase()) { + index = pair[0]; + iValue += name.length; + return false; + } + }); + if (index !== -1) { + return index + 1; + } else { + throw "Unknown name at position " + iValue; + } + }, + // Confirm that a literal character matches the string value + checkLiteral = function() { + if (value.charAt(iValue) !== format.charAt(iFormat)) { + throw "Unexpected literal at position " + iValue; + } + iValue++; + }; + + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + checkLiteral(); + } + } else { + switch (format.charAt(iFormat)) { + case "d": + day = getNumber("d"); + break; + case "D": + getName("D", dayNamesShort, dayNames); + break; + case "o": + doy = getNumber("o"); + break; + case "m": + month = getNumber("m"); + break; + case "M": + month = getName("M", monthNamesShort, monthNames); + break; + case "y": + year = getNumber("y"); + break; + case "@": + date = new Date(getNumber("@")); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "!": + date = new Date((getNumber("!") - this._ticksTo1970) / 10000); + year = date.getFullYear(); + month = date.getMonth() + 1; + day = date.getDate(); + break; + case "'": + if (lookAhead("'")){ + checkLiteral(); + } else { + literal = true; + } + break; + default: + checkLiteral(); + } + } + } + + if (iValue < value.length){ + extra = value.substr(iValue); + if (!/^\s+/.test(extra)) { + throw "Extra/unparsed characters found in date: " + extra; + } + } + + if (year === -1) { + year = new Date().getFullYear(); + } else if (year < 100) { + year += new Date().getFullYear() - new Date().getFullYear() % 100 + + (year <= shortYearCutoff ? 0 : -100); + } + + if (doy > -1) { + month = 1; + day = doy; + do { + dim = this._getDaysInMonth(year, month - 1); + if (day <= dim) { + break; + } + month++; + day -= dim; + } while (true); + } + + date = this._daylightSavingAdjust(new Date(year, month - 1, day)); + if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) { + throw "Invalid date"; // E.g. 31/02/00 + } + return date; + }, + + /* Standard date formats. */ + ATOM: "yy-mm-dd", // RFC 3339 (ISO 8601) + COOKIE: "D, dd M yy", + ISO_8601: "yy-mm-dd", + RFC_822: "D, d M y", + RFC_850: "DD, dd-M-y", + RFC_1036: "D, d M y", + RFC_1123: "D, d M yy", + RFC_2822: "D, d M yy", + RSS: "D, d M y", // RFC 822 + TICKS: "!", + TIMESTAMP: "@", + W3C: "yy-mm-dd", // ISO 8601 + + _ticksTo1970: (((1970 - 1) * 365 + Math.floor(1970 / 4) - Math.floor(1970 / 100) + + Math.floor(1970 / 400)) * 24 * 60 * 60 * 10000000), + + /* Format a date object into a string value. + * The format can be combinations of the following: + * d - day of month (no leading zero) + * dd - day of month (two digit) + * o - day of year (no leading zeros) + * oo - day of year (three digit) + * D - day name short + * DD - day name long + * m - month of year (no leading zero) + * mm - month of year (two digit) + * M - month name short + * MM - month name long + * y - year (two digit) + * yy - year (four digit) + * @ - Unix timestamp (ms since 01/01/1970) + * ! - Windows ticks (100ns since 01/01/0001) + * "..." - literal text + * '' - single quote + * + * @param format string - the desired format of the date + * @param date Date - the date value to format + * @param settings Object - attributes include: + * dayNamesShort string[7] - abbreviated names of the days from Sunday (optional) + * dayNames string[7] - names of the days from Sunday (optional) + * monthNamesShort string[12] - abbreviated names of the months (optional) + * monthNames string[12] - names of the months (optional) + * @return string - the date in the above format + */ + formatDate: function (format, date, settings) { + if (!date) { + return ""; + } + + var iFormat, + dayNamesShort = (settings ? settings.dayNamesShort : null) || this._defaults.dayNamesShort, + dayNames = (settings ? settings.dayNames : null) || this._defaults.dayNames, + monthNamesShort = (settings ? settings.monthNamesShort : null) || this._defaults.monthNamesShort, + monthNames = (settings ? settings.monthNames : null) || this._defaults.monthNames, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }, + // Format a number, with leading zero if necessary + formatNumber = function(match, value, len) { + var num = "" + value; + if (lookAhead(match)) { + while (num.length < len) { + num = "0" + num; + } + } + return num; + }, + // Format a name, short or long as requested + formatName = function(match, value, shortNames, longNames) { + return (lookAhead(match) ? longNames[value] : shortNames[value]); + }, + output = "", + literal = false; + + if (date) { + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + output += format.charAt(iFormat); + } + } else { + switch (format.charAt(iFormat)) { + case "d": + output += formatNumber("d", date.getDate(), 2); + break; + case "D": + output += formatName("D", date.getDay(), dayNamesShort, dayNames); + break; + case "o": + output += formatNumber("o", + Math.round((new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - new Date(date.getFullYear(), 0, 0).getTime()) / 86400000), 3); + break; + case "m": + output += formatNumber("m", date.getMonth() + 1, 2); + break; + case "M": + output += formatName("M", date.getMonth(), monthNamesShort, monthNames); + break; + case "y": + output += (lookAhead("y") ? date.getFullYear() : + (date.getYear() % 100 < 10 ? "0" : "") + date.getYear() % 100); + break; + case "@": + output += date.getTime(); + break; + case "!": + output += date.getTime() * 10000 + this._ticksTo1970; + break; + case "'": + if (lookAhead("'")) { + output += "'"; + } else { + literal = true; + } + break; + default: + output += format.charAt(iFormat); + } + } + } + } + return output; + }, + + /* Extract all possible characters from the date format. */ + _possibleChars: function (format) { + var iFormat, + chars = "", + literal = false, + // Check whether a format character is doubled + lookAhead = function(match) { + var matches = (iFormat + 1 < format.length && format.charAt(iFormat + 1) === match); + if (matches) { + iFormat++; + } + return matches; + }; + + for (iFormat = 0; iFormat < format.length; iFormat++) { + if (literal) { + if (format.charAt(iFormat) === "'" && !lookAhead("'")) { + literal = false; + } else { + chars += format.charAt(iFormat); + } + } else { + switch (format.charAt(iFormat)) { + case "d": case "m": case "y": case "@": + chars += "0123456789"; + break; + case "D": case "M": + return null; // Accept anything + case "'": + if (lookAhead("'")) { + chars += "'"; + } else { + literal = true; + } + break; + default: + chars += format.charAt(iFormat); + } + } + } + return chars; + }, + + /* Get a setting value, defaulting if necessary. */ + _get: function(inst, name) { + return inst.settings[name] !== undefined ? + inst.settings[name] : this._defaults[name]; + }, + + /* Parse existing date and initialise date picker. */ + _setDateFromField: function(inst, noDefault) { + if (inst.input.val() === inst.lastVal) { + return; + } + + var dateFormat = this._get(inst, "dateFormat"), + dates = inst.lastVal = inst.input ? inst.input.val() : null, + defaultDate = this._getDefaultDate(inst), + date = defaultDate, + settings = this._getFormatConfig(inst); + + try { + date = this.parseDate(dateFormat, dates, settings) || defaultDate; + } catch (event) { + dates = (noDefault ? "" : dates); + } + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + inst.currentDay = (dates ? date.getDate() : 0); + inst.currentMonth = (dates ? date.getMonth() : 0); + inst.currentYear = (dates ? date.getFullYear() : 0); + this._adjustInstDate(inst); + }, + + /* Retrieve the default date shown on opening. */ + _getDefaultDate: function(inst) { + return this._restrictMinMax(inst, + this._determineDate(inst, this._get(inst, "defaultDate"), new Date())); + }, + + /* A date may be specified as an exact value or a relative one. */ + _determineDate: function(inst, date, defaultDate) { + var offsetNumeric = function(offset) { + var date = new Date(); + date.setDate(date.getDate() + offset); + return date; + }, + offsetString = function(offset) { + try { + return $.datepicker.parseDate($.datepicker._get(inst, "dateFormat"), + offset, $.datepicker._getFormatConfig(inst)); + } + catch (e) { + // Ignore + } + + var date = (offset.toLowerCase().match(/^c/) ? + $.datepicker._getDate(inst) : null) || new Date(), + year = date.getFullYear(), + month = date.getMonth(), + day = date.getDate(), + pattern = /([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g, + matches = pattern.exec(offset); + + while (matches) { + switch (matches[2] || "d") { + case "d" : case "D" : + day += parseInt(matches[1],10); break; + case "w" : case "W" : + day += parseInt(matches[1],10) * 7; break; + case "m" : case "M" : + month += parseInt(matches[1],10); + day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); + break; + case "y": case "Y" : + year += parseInt(matches[1],10); + day = Math.min(day, $.datepicker._getDaysInMonth(year, month)); + break; + } + matches = pattern.exec(offset); + } + return new Date(year, month, day); + }, + newDate = (date == null || date === "" ? defaultDate : (typeof date === "string" ? offsetString(date) : + (typeof date === "number" ? (isNaN(date) ? defaultDate : offsetNumeric(date)) : new Date(date.getTime())))); + + newDate = (newDate && newDate.toString() === "Invalid Date" ? defaultDate : newDate); + if (newDate) { + newDate.setHours(0); + newDate.setMinutes(0); + newDate.setSeconds(0); + newDate.setMilliseconds(0); + } + return this._daylightSavingAdjust(newDate); + }, + + /* Handle switch to/from daylight saving. + * Hours may be non-zero on daylight saving cut-over: + * > 12 when midnight changeover, but then cannot generate + * midnight datetime, so jump to 1AM, otherwise reset. + * @param date (Date) the date to check + * @return (Date) the corrected date + */ + _daylightSavingAdjust: function(date) { + if (!date) { + return null; + } + date.setHours(date.getHours() > 12 ? date.getHours() + 2 : 0); + return date; + }, + + /* Set the date(s) directly. */ + _setDate: function(inst, date, noChange) { + var clear = !date, + origMonth = inst.selectedMonth, + origYear = inst.selectedYear, + newDate = this._restrictMinMax(inst, this._determineDate(inst, date, new Date())); + + inst.selectedDay = inst.currentDay = newDate.getDate(); + inst.drawMonth = inst.selectedMonth = inst.currentMonth = newDate.getMonth(); + inst.drawYear = inst.selectedYear = inst.currentYear = newDate.getFullYear(); + if ((origMonth !== inst.selectedMonth || origYear !== inst.selectedYear) && !noChange) { + this._notifyChange(inst); + } + this._adjustInstDate(inst); + if (inst.input) { + inst.input.val(clear ? "" : this._formatDate(inst)); + } + }, + + /* Retrieve the date(s) directly. */ + _getDate: function(inst) { + var startDate = (!inst.currentYear || (inst.input && inst.input.val() === "") ? null : + this._daylightSavingAdjust(new Date( + inst.currentYear, inst.currentMonth, inst.currentDay))); + return startDate; + }, + + /* Attach the onxxx handlers. These are declared statically so + * they work with static code transformers like Caja. + */ + _attachHandlers: function(inst) { + var stepMonths = this._get(inst, "stepMonths"), + id = "#" + inst.id.replace( /\\\\/g, "\\" ); + inst.dpDiv.find("[data-handler]").map(function () { + var handler = { + prev: function () { + $.datepicker._adjustDate(id, -stepMonths, "M"); + }, + next: function () { + $.datepicker._adjustDate(id, +stepMonths, "M"); + }, + hide: function () { + $.datepicker._hideDatepicker(); + }, + today: function () { + $.datepicker._gotoToday(id); + }, + selectDay: function () { + $.datepicker._selectDay(id, +this.getAttribute("data-month"), +this.getAttribute("data-year"), this); + return false; + }, + selectMonth: function () { + $.datepicker._selectMonthYear(id, this, "M"); + return false; + }, + selectYear: function () { + $.datepicker._selectMonthYear(id, this, "Y"); + return false; + } + }; + $(this).bind(this.getAttribute("data-event"), handler[this.getAttribute("data-handler")]); + }); + }, + + /* Generate the HTML for the current state of the date picker. */ + _generateHTML: function(inst) { + var maxDraw, prevText, prev, nextText, next, currentText, gotoDate, + controls, buttonPanel, firstDay, showWeek, dayNames, dayNamesMin, + monthNames, monthNamesShort, beforeShowDay, showOtherMonths, + selectOtherMonths, defaultDate, html, dow, row, group, col, selectedDate, + cornerClass, calender, thead, day, daysInMonth, leadDays, curRows, numRows, + printDate, dRow, tbody, daySettings, otherMonth, unselectable, + tempDate = new Date(), + today = this._daylightSavingAdjust( + new Date(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate())), // clear time + isRTL = this._get(inst, "isRTL"), + showButtonPanel = this._get(inst, "showButtonPanel"), + hideIfNoPrevNext = this._get(inst, "hideIfNoPrevNext"), + navigationAsDateFormat = this._get(inst, "navigationAsDateFormat"), + numMonths = this._getNumberOfMonths(inst), + showCurrentAtPos = this._get(inst, "showCurrentAtPos"), + stepMonths = this._get(inst, "stepMonths"), + isMultiMonth = (numMonths[0] !== 1 || numMonths[1] !== 1), + currentDate = this._daylightSavingAdjust((!inst.currentDay ? new Date(9999, 9, 9) : + new Date(inst.currentYear, inst.currentMonth, inst.currentDay))), + minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + drawMonth = inst.drawMonth - showCurrentAtPos, + drawYear = inst.drawYear; + + if (drawMonth < 0) { + drawMonth += 12; + drawYear--; + } + if (maxDate) { + maxDraw = this._daylightSavingAdjust(new Date(maxDate.getFullYear(), + maxDate.getMonth() - (numMonths[0] * numMonths[1]) + 1, maxDate.getDate())); + maxDraw = (minDate && maxDraw < minDate ? minDate : maxDraw); + while (this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1)) > maxDraw) { + drawMonth--; + if (drawMonth < 0) { + drawMonth = 11; + drawYear--; + } + } + } + inst.drawMonth = drawMonth; + inst.drawYear = drawYear; + + prevText = this._get(inst, "prevText"); + prevText = (!navigationAsDateFormat ? prevText : this.formatDate(prevText, + this._daylightSavingAdjust(new Date(drawYear, drawMonth - stepMonths, 1)), + this._getFormatConfig(inst))); + + prev = (this._canAdjustMonth(inst, -1, drawYear, drawMonth) ? + "<a class='ui-datepicker-prev ui-corner-all' data-handler='prev' data-event='click'" + + " title='" + prevText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "e" : "w") + "'>" + prevText + "</span></a>" : + (hideIfNoPrevNext ? "" : "<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='"+ prevText +"'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "e" : "w") + "'>" + prevText + "</span></a>")); + + nextText = this._get(inst, "nextText"); + nextText = (!navigationAsDateFormat ? nextText : this.formatDate(nextText, + this._daylightSavingAdjust(new Date(drawYear, drawMonth + stepMonths, 1)), + this._getFormatConfig(inst))); + + next = (this._canAdjustMonth(inst, +1, drawYear, drawMonth) ? + "<a class='ui-datepicker-next ui-corner-all' data-handler='next' data-event='click'" + + " title='" + nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "w" : "e") + "'>" + nextText + "</span></a>" : + (hideIfNoPrevNext ? "" : "<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='"+ nextText + "'><span class='ui-icon ui-icon-circle-triangle-" + ( isRTL ? "w" : "e") + "'>" + nextText + "</span></a>")); + + currentText = this._get(inst, "currentText"); + gotoDate = (this._get(inst, "gotoCurrent") && inst.currentDay ? currentDate : today); + currentText = (!navigationAsDateFormat ? currentText : + this.formatDate(currentText, gotoDate, this._getFormatConfig(inst))); + + controls = (!inst.inline ? "<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>" + + this._get(inst, "closeText") + "</button>" : ""); + + buttonPanel = (showButtonPanel) ? "<div class='ui-datepicker-buttonpane ui-widget-content'>" + (isRTL ? controls : "") + + (this._isInRange(inst, gotoDate) ? "<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'" + + ">" + currentText + "</button>" : "") + (isRTL ? "" : controls) + "</div>" : ""; + + firstDay = parseInt(this._get(inst, "firstDay"),10); + firstDay = (isNaN(firstDay) ? 0 : firstDay); + + showWeek = this._get(inst, "showWeek"); + dayNames = this._get(inst, "dayNames"); + dayNamesMin = this._get(inst, "dayNamesMin"); + monthNames = this._get(inst, "monthNames"); + monthNamesShort = this._get(inst, "monthNamesShort"); + beforeShowDay = this._get(inst, "beforeShowDay"); + showOtherMonths = this._get(inst, "showOtherMonths"); + selectOtherMonths = this._get(inst, "selectOtherMonths"); + defaultDate = this._getDefaultDate(inst); + html = ""; + dow; + for (row = 0; row < numMonths[0]; row++) { + group = ""; + this.maxRows = 4; + for (col = 0; col < numMonths[1]; col++) { + selectedDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, inst.selectedDay)); + cornerClass = " ui-corner-all"; + calender = ""; + if (isMultiMonth) { + calender += "<div class='ui-datepicker-group"; + if (numMonths[1] > 1) { + switch (col) { + case 0: calender += " ui-datepicker-group-first"; + cornerClass = " ui-corner-" + (isRTL ? "right" : "left"); break; + case numMonths[1]-1: calender += " ui-datepicker-group-last"; + cornerClass = " ui-corner-" + (isRTL ? "left" : "right"); break; + default: calender += " ui-datepicker-group-middle"; cornerClass = ""; break; + } + } + calender += "'>"; + } + calender += "<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix" + cornerClass + "'>" + + (/all|left/.test(cornerClass) && row === 0 ? (isRTL ? next : prev) : "") + + (/all|right/.test(cornerClass) && row === 0 ? (isRTL ? prev : next) : "") + + this._generateMonthYearHeader(inst, drawMonth, drawYear, minDate, maxDate, + row > 0 || col > 0, monthNames, monthNamesShort) + // draw month headers + "</div><table class='ui-datepicker-calendar'><thead>" + + "<tr>"; + thead = (showWeek ? "<th class='ui-datepicker-week-col'>" + this._get(inst, "weekHeader") + "</th>" : ""); + for (dow = 0; dow < 7; dow++) { // days of the week + day = (dow + firstDay) % 7; + thead += "<th scope='col'" + ((dow + firstDay + 6) % 7 >= 5 ? " class='ui-datepicker-week-end'" : "") + ">" + + "<span title='" + dayNames[day] + "'>" + dayNamesMin[day] + "</span></th>"; + } + calender += thead + "</tr></thead><tbody>"; + daysInMonth = this._getDaysInMonth(drawYear, drawMonth); + if (drawYear === inst.selectedYear && drawMonth === inst.selectedMonth) { + inst.selectedDay = Math.min(inst.selectedDay, daysInMonth); + } + leadDays = (this._getFirstDayOfMonth(drawYear, drawMonth) - firstDay + 7) % 7; + curRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate + numRows = (isMultiMonth ? this.maxRows > curRows ? this.maxRows : curRows : curRows); //If multiple months, use the higher number of rows (see #7043) + this.maxRows = numRows; + printDate = this._daylightSavingAdjust(new Date(drawYear, drawMonth, 1 - leadDays)); + for (dRow = 0; dRow < numRows; dRow++) { // create date picker rows + calender += "<tr>"; + tbody = (!showWeek ? "" : "<td class='ui-datepicker-week-col'>" + + this._get(inst, "calculateWeek")(printDate) + "</td>"); + for (dow = 0; dow < 7; dow++) { // create date picker days + daySettings = (beforeShowDay ? + beforeShowDay.apply((inst.input ? inst.input[0] : null), [printDate]) : [true, ""]); + otherMonth = (printDate.getMonth() !== drawMonth); + unselectable = (otherMonth && !selectOtherMonths) || !daySettings[0] || + (minDate && printDate < minDate) || (maxDate && printDate > maxDate); + tbody += "<td class='" + + ((dow + firstDay + 6) % 7 >= 5 ? " ui-datepicker-week-end" : "") + // highlight weekends + (otherMonth ? " ui-datepicker-other-month" : "") + // highlight days from other months + ((printDate.getTime() === selectedDate.getTime() && drawMonth === inst.selectedMonth && inst._keyEvent) || // user pressed key + (defaultDate.getTime() === printDate.getTime() && defaultDate.getTime() === selectedDate.getTime()) ? + // or defaultDate is current printedDate and defaultDate is selectedDate + " " + this._dayOverClass : "") + // highlight selected day + (unselectable ? " " + this._unselectableClass + " ui-state-disabled": "") + // highlight unselectable days + (otherMonth && !showOtherMonths ? "" : " " + daySettings[1] + // highlight custom dates + (printDate.getTime() === currentDate.getTime() ? " " + this._currentClass : "") + // highlight selected day + (printDate.getTime() === today.getTime() ? " ui-datepicker-today" : "")) + "'" + // highlight today (if different) + ((!otherMonth || showOtherMonths) && daySettings[2] ? " title='" + daySettings[2].replace(/'/g, "&#39;") + "'" : "") + // cell title + (unselectable ? "" : " data-handler='selectDay' data-event='click' data-month='" + printDate.getMonth() + "' data-year='" + printDate.getFullYear() + "'") + ">" + // actions + (otherMonth && !showOtherMonths ? "&#xa0;" : // display for other months + (unselectable ? "<span class='ui-state-default'>" + printDate.getDate() + "</span>" : "<a class='ui-state-default" + + (printDate.getTime() === today.getTime() ? " ui-state-highlight" : "") + + (printDate.getTime() === currentDate.getTime() ? " ui-state-active" : "") + // highlight selected day + (otherMonth ? " ui-priority-secondary" : "") + // distinguish dates from other months + "' href='#'>" + printDate.getDate() + "</a>")) + "</td>"; // display selectable date + printDate.setDate(printDate.getDate() + 1); + printDate = this._daylightSavingAdjust(printDate); + } + calender += tbody + "</tr>"; + } + drawMonth++; + if (drawMonth > 11) { + drawMonth = 0; + drawYear++; + } + calender += "</tbody></table>" + (isMultiMonth ? "</div>" + + ((numMonths[0] > 0 && col === numMonths[1]-1) ? "<div class='ui-datepicker-row-break'></div>" : "") : ""); + group += calender; + } + html += group; + } + html += buttonPanel; + inst._keyEvent = false; + return html; + }, + + /* Generate the month and year header. */ + _generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate, + secondary, monthNames, monthNamesShort) { + + var inMinYear, inMaxYear, month, years, thisYear, determineYear, year, endYear, + changeMonth = this._get(inst, "changeMonth"), + changeYear = this._get(inst, "changeYear"), + showMonthAfterYear = this._get(inst, "showMonthAfterYear"), + html = "<div class='ui-datepicker-title'>", + monthHtml = ""; + + // month selection + if (secondary || !changeMonth) { + monthHtml += "<span class='ui-datepicker-month'>" + monthNames[drawMonth] + "</span>"; + } else { + inMinYear = (minDate && minDate.getFullYear() === drawYear); + inMaxYear = (maxDate && maxDate.getFullYear() === drawYear); + monthHtml += "<select class='ui-datepicker-month' data-handler='selectMonth' data-event='change'>"; + for ( month = 0; month < 12; month++) { + if ((!inMinYear || month >= minDate.getMonth()) && (!inMaxYear || month <= maxDate.getMonth())) { + monthHtml += "<option value='" + month + "'" + + (month === drawMonth ? " selected='selected'" : "") + + ">" + monthNamesShort[month] + "</option>"; + } + } + monthHtml += "</select>"; + } + + if (!showMonthAfterYear) { + html += monthHtml + (secondary || !(changeMonth && changeYear) ? "&#xa0;" : ""); + } + + // year selection + if ( !inst.yearshtml ) { + inst.yearshtml = ""; + if (secondary || !changeYear) { + html += "<span class='ui-datepicker-year'>" + drawYear + "</span>"; + } else { + // determine range of years to display + years = this._get(inst, "yearRange").split(":"); + thisYear = new Date().getFullYear(); + determineYear = function(value) { + var year = (value.match(/c[+\-].*/) ? drawYear + parseInt(value.substring(1), 10) : + (value.match(/[+\-].*/) ? thisYear + parseInt(value, 10) : + parseInt(value, 10))); + return (isNaN(year) ? thisYear : year); + }; + year = determineYear(years[0]); + endYear = Math.max(year, determineYear(years[1] || "")); + year = (minDate ? Math.max(year, minDate.getFullYear()) : year); + endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear); + inst.yearshtml += "<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>"; + for (; year <= endYear; year++) { + inst.yearshtml += "<option value='" + year + "'" + + (year === drawYear ? " selected='selected'" : "") + + ">" + year + "</option>"; + } + inst.yearshtml += "</select>"; + + html += inst.yearshtml; + inst.yearshtml = null; + } + } + + html += this._get(inst, "yearSuffix"); + if (showMonthAfterYear) { + html += (secondary || !(changeMonth && changeYear) ? "&#xa0;" : "") + monthHtml; + } + html += "</div>"; // Close datepicker_header + return html; + }, + + /* Adjust one of the date sub-fields. */ + _adjustInstDate: function(inst, offset, period) { + var year = inst.drawYear + (period === "Y" ? offset : 0), + month = inst.drawMonth + (period === "M" ? offset : 0), + day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + (period === "D" ? offset : 0), + date = this._restrictMinMax(inst, this._daylightSavingAdjust(new Date(year, month, day))); + + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + if (period === "M" || period === "Y") { + this._notifyChange(inst); + } + }, + + /* Ensure a date is within any min/max bounds. */ + _restrictMinMax: function(inst, date) { + var minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + newDate = (minDate && date < minDate ? minDate : date); + return (maxDate && newDate > maxDate ? maxDate : newDate); + }, + + /* Notify change of month/year. */ + _notifyChange: function(inst) { + var onChange = this._get(inst, "onChangeMonthYear"); + if (onChange) { + onChange.apply((inst.input ? inst.input[0] : null), + [inst.selectedYear, inst.selectedMonth + 1, inst]); + } + }, + + /* Determine the number of months to show. */ + _getNumberOfMonths: function(inst) { + var numMonths = this._get(inst, "numberOfMonths"); + return (numMonths == null ? [1, 1] : (typeof numMonths === "number" ? [1, numMonths] : numMonths)); + }, + + /* Determine the current maximum date - ensure no time components are set. */ + _getMinMaxDate: function(inst, minMax) { + return this._determineDate(inst, this._get(inst, minMax + "Date"), null); + }, + + /* Find the number of days in a given month. */ + _getDaysInMonth: function(year, month) { + return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate(); + }, + + /* Find the day of the week of the first of a month. */ + _getFirstDayOfMonth: function(year, month) { + return new Date(year, month, 1).getDay(); + }, + + /* Determines if we should allow a "next/prev" month display change. */ + _canAdjustMonth: function(inst, offset, curYear, curMonth) { + var numMonths = this._getNumberOfMonths(inst), + date = this._daylightSavingAdjust(new Date(curYear, + curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1)); + + if (offset < 0) { + date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth())); + } + return this._isInRange(inst, date); + }, + + /* Is the given date in the accepted range? */ + _isInRange: function(inst, date) { + var yearSplit, currentYear, + minDate = this._getMinMaxDate(inst, "min"), + maxDate = this._getMinMaxDate(inst, "max"), + minYear = null, + maxYear = null, + years = this._get(inst, "yearRange"); + if (years){ + yearSplit = years.split(":"); + currentYear = new Date().getFullYear(); + minYear = parseInt(yearSplit[0], 10); + maxYear = parseInt(yearSplit[1], 10); + if ( yearSplit[0].match(/[+\-].*/) ) { + minYear += currentYear; + } + if ( yearSplit[1].match(/[+\-].*/) ) { + maxYear += currentYear; + } + } + + return ((!minDate || date.getTime() >= minDate.getTime()) && + (!maxDate || date.getTime() <= maxDate.getTime()) && + (!minYear || date.getFullYear() >= minYear) && + (!maxYear || date.getFullYear() <= maxYear)); + }, + + /* Provide the configuration settings for formatting/parsing. */ + _getFormatConfig: function(inst) { + var shortYearCutoff = this._get(inst, "shortYearCutoff"); + shortYearCutoff = (typeof shortYearCutoff !== "string" ? shortYearCutoff : + new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10)); + return {shortYearCutoff: shortYearCutoff, + dayNamesShort: this._get(inst, "dayNamesShort"), dayNames: this._get(inst, "dayNames"), + monthNamesShort: this._get(inst, "monthNamesShort"), monthNames: this._get(inst, "monthNames")}; + }, + + /* Format the given date for display. */ + _formatDate: function(inst, day, month, year) { + if (!day) { + inst.currentDay = inst.selectedDay; + inst.currentMonth = inst.selectedMonth; + inst.currentYear = inst.selectedYear; + } + var date = (day ? (typeof day === "object" ? day : + this._daylightSavingAdjust(new Date(year, month, day))) : + this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay))); + return this.formatDate(this._get(inst, "dateFormat"), date, this._getFormatConfig(inst)); + } +}); + /* - * jQuery UI Widget 1.10.3+amd - * https://github.com/blueimp/jQuery-File-Upload + * Bind hover events for datepicker elements. + * Done via delegate so the binding only occurs once in the lifetime of the parent div. + * Global datepicker_instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker. + */ +function datepicker_bindHover(dpDiv) { + var selector = "button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a"; + return dpDiv.delegate(selector, "mouseout", function() { + $(this).removeClass("ui-state-hover"); + if (this.className.indexOf("ui-datepicker-prev") !== -1) { + $(this).removeClass("ui-datepicker-prev-hover"); + } + if (this.className.indexOf("ui-datepicker-next") !== -1) { + $(this).removeClass("ui-datepicker-next-hover"); + } + }) + .delegate(selector, "mouseover", function(){ + if (!$.datepicker._isDisabledDatepicker( datepicker_instActive.inline ? dpDiv.parent()[0] : datepicker_instActive.input[0])) { + $(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"); + $(this).addClass("ui-state-hover"); + if (this.className.indexOf("ui-datepicker-prev") !== -1) { + $(this).addClass("ui-datepicker-prev-hover"); + } + if (this.className.indexOf("ui-datepicker-next") !== -1) { + $(this).addClass("ui-datepicker-next-hover"); + } + } + }); +} + +/* jQuery extend now ignores nulls! */ +function datepicker_extendRemove(target, props) { + $.extend(target, props); + for (var name in props) { + if (props[name] == null) { + target[name] = props[name]; + } + } + return target; +} + +/* Invoke the datepicker functionality. + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new datepicker functionality + @return jQuery object */ +$.fn.datepicker = function(options){ + + /* Verify an empty collection wasn't passed - Fixes #6976 */ + if ( !this.length ) { + return this; + } + + /* Initialise the date picker. */ + if (!$.datepicker.initialized) { + $(document).mousedown($.datepicker._checkExternalClick); + $.datepicker.initialized = true; + } + + /* Append datepicker main container to body if not exist. */ + if ($("#"+$.datepicker._mainDivId).length === 0) { + $("body").append($.datepicker.dpDiv); + } + + var otherArgs = Array.prototype.slice.call(arguments, 1); + if (typeof options === "string" && (options === "isDisabled" || options === "getDate" || options === "widget")) { + return $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this[0]].concat(otherArgs)); + } + if (options === "option" && arguments.length === 2 && typeof arguments[1] === "string") { + return $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this[0]].concat(otherArgs)); + } + return this.each(function() { + typeof options === "string" ? + $.datepicker["_" + options + "Datepicker"]. + apply($.datepicker, [this].concat(otherArgs)) : + $.datepicker._attachDatepicker(this, options); + }); +}; + +$.datepicker = new Datepicker(); // singleton instance +$.datepicker.initialized = false; +$.datepicker.uuid = new Date().getTime(); +$.datepicker.version = "1.11.0"; + +return $.datepicker; + +})); +/*! + * jQuery UI Position 1.11.0 + * http://jqueryui.com * - * Copyright 2013 jQuery Foundation and other contributors + * Copyright 2014 jQuery Foundation and other contributors * Released under the MIT license. * http://jquery.org/license * - * http://api.jqueryui.com/jQuery.widget/ + * http://api.jqueryui.com/position/ */ +(function( factory ) { + if ( typeof define === "function" && define.amd ) { -(function (factory) { - if (typeof define === "function" && define.amd) { - // Register as an anonymous AMD module: - define(["jquery"], factory); - } else { - // Browser globals: - factory(jQuery); - } -}(function( $, undefined ) { + // AMD. Register as an anonymous module. + define( [ "jquery" ], factory ); + } else { -var uuid = 0, - slice = Array.prototype.slice, - _cleanData = $.cleanData; -$.cleanData = function( elems ) { - for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { - try { - $( elem ).triggerHandler( "remove" ); - // http://bugs.jquery.com/ticket/8235 - } catch( e ) {} + // Browser globals + factory( jQuery ); } - _cleanData( elems ); +}(function( $ ) { +(function() { + +$.ui = $.ui || {}; + +var cachedScrollbarWidth, supportsOffsetFractions, + max = Math.max, + abs = Math.abs, + round = Math.round, + rhorizontal = /left|center|right/, + rvertical = /top|center|bottom/, + roffset = /[\+\-]\d+(\.[\d]+)?%?/, + rposition = /^\w+/, + rpercent = /%$/, + _position = $.fn.position; + +function getOffsets( offsets, width, height ) { + return [ + parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ), + parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 ) + ]; +} + +function parseCss( element, property ) { + return parseInt( $.css( element, property ), 10 ) || 0; +} + +function getDimensions( elem ) { + var raw = elem[0]; + if ( raw.nodeType === 9 ) { + return { + width: elem.width(), + height: elem.height(), + offset: { top: 0, left: 0 } + }; + } + if ( $.isWindow( raw ) ) { + return { + width: elem.width(), + height: elem.height(), + offset: { top: elem.scrollTop(), left: elem.scrollLeft() } + }; + } + if ( raw.preventDefault ) { + return { + width: 0, + height: 0, + offset: { top: raw.pageY, left: raw.pageX } + }; + } + return { + width: elem.outerWidth(), + height: elem.outerHeight(), + offset: elem.offset() + }; +} + +$.position = { + scrollbarWidth: function() { + if ( cachedScrollbarWidth !== undefined ) { + return cachedScrollbarWidth; + } + var w1, w2, + div = $( "<div style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'><div style='height:100px;width:auto;'></div></div>" ), + innerDiv = div.children()[0]; + + $( "body" ).append( div ); + w1 = innerDiv.offsetWidth; + div.css( "overflow", "scroll" ); + + w2 = innerDiv.offsetWidth; + + if ( w1 === w2 ) { + w2 = div[0].clientWidth; + } + + div.remove(); + + return (cachedScrollbarWidth = w1 - w2); + }, + getScrollInfo: function( within ) { + var overflowX = within.isWindow || within.isDocument ? "" : + within.element.css( "overflow-x" ), + overflowY = within.isWindow || within.isDocument ? "" : + within.element.css( "overflow-y" ), + hasOverflowX = overflowX === "scroll" || + ( overflowX === "auto" && within.width < within.element[0].scrollWidth ), + hasOverflowY = overflowY === "scroll" || + ( overflowY === "auto" && within.height < within.element[0].scrollHeight ); + return { + width: hasOverflowY ? $.position.scrollbarWidth() : 0, + height: hasOverflowX ? $.position.scrollbarWidth() : 0 + }; + }, + getWithinInfo: function( element ) { + var withinElement = $( element || window ), + isWindow = $.isWindow( withinElement[0] ), + isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9; + return { + element: withinElement, + isWindow: isWindow, + isDocument: isDocument, + offset: withinElement.offset() || { left: 0, top: 0 }, + scrollLeft: withinElement.scrollLeft(), + scrollTop: withinElement.scrollTop(), + width: isWindow ? withinElement.width() : withinElement.outerWidth(), + height: isWindow ? withinElement.height() : withinElement.outerHeight() + }; + } }; +$.fn.position = function( options ) { + if ( !options || !options.of ) { + return _position.apply( this, arguments ); + } + + // make a copy, we don't want to modify arguments + options = $.extend( {}, options ); + + var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions, + target = $( options.of ), + within = $.position.getWithinInfo( options.within ), + scrollInfo = $.position.getScrollInfo( within ), + collision = ( options.collision || "flip" ).split( " " ), + offsets = {}; + + dimensions = getDimensions( target ); + if ( target[0].preventDefault ) { + // force left top to allow flipping + options.at = "left top"; + } + targetWidth = dimensions.width; + targetHeight = dimensions.height; + targetOffset = dimensions.offset; + // clone to reuse original targetOffset later + basePosition = $.extend( {}, targetOffset ); + + // force my and at to have valid horizontal and vertical positions + // if a value is missing or invalid, it will be converted to center + $.each( [ "my", "at" ], function() { + var pos = ( options[ this ] || "" ).split( " " ), + horizontalOffset, + verticalOffset; + + if ( pos.length === 1) { + pos = rhorizontal.test( pos[ 0 ] ) ? + pos.concat( [ "center" ] ) : + rvertical.test( pos[ 0 ] ) ? + [ "center" ].concat( pos ) : + [ "center", "center" ]; + } + pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center"; + pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center"; + + // calculate offsets + horizontalOffset = roffset.exec( pos[ 0 ] ); + verticalOffset = roffset.exec( pos[ 1 ] ); + offsets[ this ] = [ + horizontalOffset ? horizontalOffset[ 0 ] : 0, + verticalOffset ? verticalOffset[ 0 ] : 0 + ]; + + // reduce to just the positions without the offsets + options[ this ] = [ + rposition.exec( pos[ 0 ] )[ 0 ], + rposition.exec( pos[ 1 ] )[ 0 ] + ]; + }); + + // normalize collision option + if ( collision.length === 1 ) { + collision[ 1 ] = collision[ 0 ]; + } + + if ( options.at[ 0 ] === "right" ) { + basePosition.left += targetWidth; + } else if ( options.at[ 0 ] === "center" ) { + basePosition.left += targetWidth / 2; + } + + if ( options.at[ 1 ] === "bottom" ) { + basePosition.top += targetHeight; + } else if ( options.at[ 1 ] === "center" ) { + basePosition.top += targetHeight / 2; + } + + atOffset = getOffsets( offsets.at, targetWidth, targetHeight ); + basePosition.left += atOffset[ 0 ]; + basePosition.top += atOffset[ 1 ]; + + return this.each(function() { + var collisionPosition, using, + elem = $( this ), + elemWidth = elem.outerWidth(), + elemHeight = elem.outerHeight(), + marginLeft = parseCss( this, "marginLeft" ), + marginTop = parseCss( this, "marginTop" ), + collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width, + collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height, + position = $.extend( {}, basePosition ), + myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() ); + + if ( options.my[ 0 ] === "right" ) { + position.left -= elemWidth; + } else if ( options.my[ 0 ] === "center" ) { + position.left -= elemWidth / 2; + } + + if ( options.my[ 1 ] === "bottom" ) { + position.top -= elemHeight; + } else if ( options.my[ 1 ] === "center" ) { + position.top -= elemHeight / 2; + } + + position.left += myOffset[ 0 ]; + position.top += myOffset[ 1 ]; + + // if the browser doesn't support fractions, then round for consistent results + if ( !supportsOffsetFractions ) { + position.left = round( position.left ); + position.top = round( position.top ); + } + + collisionPosition = { + marginLeft: marginLeft, + marginTop: marginTop + }; + + $.each( [ "left", "top" ], function( i, dir ) { + if ( $.ui.position[ collision[ i ] ] ) { + $.ui.position[ collision[ i ] ][ dir ]( position, { + targetWidth: targetWidth, + targetHeight: targetHeight, + elemWidth: elemWidth, + elemHeight: elemHeight, + collisionPosition: collisionPosition, + collisionWidth: collisionWidth, + collisionHeight: collisionHeight, + offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ], + my: options.my, + at: options.at, + within: within, + elem: elem + }); + } + }); + + if ( options.using ) { + // adds feedback as second argument to using callback, if present + using = function( props ) { + var left = targetOffset.left - position.left, + right = left + targetWidth - elemWidth, + top = targetOffset.top - position.top, + bottom = top + targetHeight - elemHeight, + feedback = { + target: { + element: target, + left: targetOffset.left, + top: targetOffset.top, + width: targetWidth, + height: targetHeight + }, + element: { + element: elem, + left: position.left, + top: position.top, + width: elemWidth, + height: elemHeight + }, + horizontal: right < 0 ? "left" : left > 0 ? "right" : "center", + vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle" + }; + if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) { + feedback.horizontal = "center"; + } + if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) { + feedback.vertical = "middle"; + } + if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) { + feedback.important = "horizontal"; + } else { + feedback.important = "vertical"; + } + options.using.call( this, props, feedback ); + }; + } + + elem.offset( $.extend( position, { using: using } ) ); + }); +}; + +$.ui.position = { + fit: { + left: function( position, data ) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollLeft : within.offset.left, + outerWidth = within.width, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = withinOffset - collisionPosLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset, + newOverRight; + + // element is wider than within + if ( data.collisionWidth > outerWidth ) { + // element is initially over the left side of within + if ( overLeft > 0 && overRight <= 0 ) { + newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset; + position.left += overLeft - newOverRight; + // element is initially over right side of within + } else if ( overRight > 0 && overLeft <= 0 ) { + position.left = withinOffset; + // element is initially over both left and right sides of within + } else { + if ( overLeft > overRight ) { + position.left = withinOffset + outerWidth - data.collisionWidth; + } else { + position.left = withinOffset; + } + } + // too far left -> align with left edge + } else if ( overLeft > 0 ) { + position.left += overLeft; + // too far right -> align with right edge + } else if ( overRight > 0 ) { + position.left -= overRight; + // adjust based on position and margin + } else { + position.left = max( position.left - collisionPosLeft, position.left ); + } + }, + top: function( position, data ) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollTop : within.offset.top, + outerHeight = data.within.height, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = withinOffset - collisionPosTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset, + newOverBottom; + + // element is taller than within + if ( data.collisionHeight > outerHeight ) { + // element is initially over the top of within + if ( overTop > 0 && overBottom <= 0 ) { + newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset; + position.top += overTop - newOverBottom; + // element is initially over bottom of within + } else if ( overBottom > 0 && overTop <= 0 ) { + position.top = withinOffset; + // element is initially over both top and bottom of within + } else { + if ( overTop > overBottom ) { + position.top = withinOffset + outerHeight - data.collisionHeight; + } else { + position.top = withinOffset; + } + } + // too far up -> align with top + } else if ( overTop > 0 ) { + position.top += overTop; + // too far down -> align with bottom edge + } else if ( overBottom > 0 ) { + position.top -= overBottom; + // adjust based on position and margin + } else { + position.top = max( position.top - collisionPosTop, position.top ); + } + } + }, + flip: { + left: function( position, data ) { + var within = data.within, + withinOffset = within.offset.left + within.scrollLeft, + outerWidth = within.width, + offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = collisionPosLeft - offsetLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft, + myOffset = data.my[ 0 ] === "left" ? + -data.elemWidth : + data.my[ 0 ] === "right" ? + data.elemWidth : + 0, + atOffset = data.at[ 0 ] === "left" ? + data.targetWidth : + data.at[ 0 ] === "right" ? + -data.targetWidth : + 0, + offset = -2 * data.offset[ 0 ], + newOverRight, + newOverLeft; + + if ( overLeft < 0 ) { + newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset; + if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) { + position.left += myOffset + atOffset + offset; + } + } else if ( overRight > 0 ) { + newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft; + if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) { + position.left += myOffset + atOffset + offset; + } + } + }, + top: function( position, data ) { + var within = data.within, + withinOffset = within.offset.top + within.scrollTop, + outerHeight = within.height, + offsetTop = within.isWindow ? within.scrollTop : within.offset.top, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = collisionPosTop - offsetTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop, + top = data.my[ 1 ] === "top", + myOffset = top ? + -data.elemHeight : + data.my[ 1 ] === "bottom" ? + data.elemHeight : + 0, + atOffset = data.at[ 1 ] === "top" ? + data.targetHeight : + data.at[ 1 ] === "bottom" ? + -data.targetHeight : + 0, + offset = -2 * data.offset[ 1 ], + newOverTop, + newOverBottom; + if ( overTop < 0 ) { + newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset; + if ( ( position.top + myOffset + atOffset + offset) > overTop && ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) ) { + position.top += myOffset + atOffset + offset; + } + } else if ( overBottom > 0 ) { + newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop; + if ( ( position.top + myOffset + atOffset + offset) > overBottom && ( newOverTop > 0 || abs( newOverTop ) < overBottom ) ) { + position.top += myOffset + atOffset + offset; + } + } + } + }, + flipfit: { + left: function() { + $.ui.position.flip.left.apply( this, arguments ); + $.ui.position.fit.left.apply( this, arguments ); + }, + top: function() { + $.ui.position.flip.top.apply( this, arguments ); + $.ui.position.fit.top.apply( this, arguments ); + } + } +}; + +// fraction support test +(function() { + var testElement, testElementParent, testElementStyle, offsetLeft, i, + body = document.getElementsByTagName( "body" )[ 0 ], + div = document.createElement( "div" ); + + //Create a "fake body" for testing based on method used in jQuery.support + testElement = document.createElement( body ? "div" : "body" ); + testElementStyle = { + visibility: "hidden", + width: 0, + height: 0, + border: 0, + margin: 0, + background: "none" + }; + if ( body ) { + $.extend( testElementStyle, { + position: "absolute", + left: "-1000px", + top: "-1000px" + }); + } + for ( i in testElementStyle ) { + testElement.style[ i ] = testElementStyle[ i ]; + } + testElement.appendChild( div ); + testElementParent = body || document.documentElement; + testElementParent.insertBefore( testElement, testElementParent.firstChild ); + + div.style.cssText = "position: absolute; left: 10.7432222px;"; + + offsetLeft = $( div ).offset().left; + supportsOffsetFractions = offsetLeft > 10 && offsetLeft < 11; + + testElement.innerHTML = ""; + testElementParent.removeChild( testElement ); +})(); + +})(); + +return $.ui.position; + +})); +/*! + * jQuery UI Widget 1.11.0 + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/jQuery.widget/ + */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define( [ "jquery" ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + +var widget_uuid = 0, + widget_slice = Array.prototype.slice; + +$.cleanData = (function( orig ) { + return function( elems ) { + for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { + try { + $( elem ).triggerHandler( "remove" ); + // http://bugs.jquery.com/ticket/8235 + } catch( e ) {} + } + orig( elems ); + }; +})( $.cleanData ); + $.widget = function( name, base, prototype ) { var fullName, existingConstructor, constructor, basePrototype, // proxiedPrototype allows the provided prototype to remain unmodified // so that it can be used as a mixin for multiple widgets (#8876) proxiedPrototype = {}, @@ -23051,11 +15909,11 @@ }); constructor.prototype = $.widget.extend( basePrototype, { // TODO: remove support for widgetEventPrefix // always use the name + a colon as the prefix, e.g., draggable:start // don't prefix for widgets that aren't DOM-based - widgetEventPrefix: existingConstructor ? basePrototype.widgetEventPrefix : name + widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name }, proxiedPrototype, { constructor: constructor, namespace: namespace, widgetName: name, widgetFullName: fullName @@ -23079,14 +15937,16 @@ } else { base._childConstructors.push( constructor ); } $.widget.bridge( name, constructor ); + + return constructor; }; $.widget.extend = function( target ) { - var input = slice.call( arguments, 1 ), + var input = widget_slice.call( arguments, 1 ), inputIndex = 0, inputLength = input.length, key, value; for ( ; inputIndex < inputLength; inputIndex++ ) { @@ -23111,11 +15971,11 @@ $.widget.bridge = function( name, object ) { var fullName = object.prototype.widgetFullName || name; $.fn[ name ] = function( options ) { var isMethodCall = typeof options === "string", - args = slice.call( arguments, 1 ), + args = widget_slice.call( arguments, 1 ), returnValue = this; // allow multiple hashes to be passed on init options = !isMethodCall && args.length ? $.widget.extend.apply( null, [ options ].concat(args) ) : @@ -23123,10 +15983,14 @@ if ( isMethodCall ) { this.each(function() { var methodValue, instance = $.data( this, fullName ); + if ( options === "instance" ) { + returnValue = instance; + return false; + } if ( !instance ) { return $.error( "cannot call methods on " + name + " prior to initialization; " + "attempted to call method '" + options + "'" ); } if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { @@ -23142,11 +16006,14 @@ }); } else { this.each(function() { var instance = $.data( this, fullName ); if ( instance ) { - instance.option( options || {} )._init(); + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } } else { $.data( this, fullName, new object( options, this ) ); } }); } @@ -23169,11 +16036,11 @@ create: null }, _createWidget: function( options, element ) { element = $( element || this.defaultElement || this )[ 0 ]; this.element = $( element ); - this.uuid = uuid++; + this.uuid = widget_uuid++; this.eventNamespace = "." + this.widgetName + this.uuid; this.options = $.widget.extend( {}, this.options, this._getCreateOptions(), options ); @@ -23212,13 +16079,10 @@ this._destroy(); // we can probably remove the unbind calls in 2.0 // all event bindings should go through this._on() this.element .unbind( this.eventNamespace ) - // 1.9 BC for #7810 - // TODO remove dual storage - .removeData( this.widgetName ) .removeData( this.widgetFullName ) // support: jquery <1.6.3 // http://bugs.jquery.com/ticket/9413 .removeData( $.camelCase( this.widgetFullName ) ); this.widget() @@ -23260,16 +16124,16 @@ for ( i = 0; i < parts.length - 1; i++ ) { curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; curOption = curOption[ parts[ i ] ]; } key = parts.pop(); - if ( value === undefined ) { + if ( arguments.length === 1 ) { return curOption[ key ] === undefined ? null : curOption[ key ]; } curOption[ key ] = value; } else { - if ( value === undefined ) { + if ( arguments.length === 1 ) { return this.options[ key ] === undefined ? null : this.options[ key ]; } options[ key ] = value; } } @@ -23290,24 +16154,27 @@ _setOption: function( key, value ) { this.options[ key ] = value; if ( key === "disabled" ) { this.widget() - .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) - .attr( "aria-disabled", value ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); + .toggleClass( this.widgetFullName + "-disabled", !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + } } return this; }, enable: function() { - return this._setOption( "disabled", false ); + return this._setOptions({ disabled: false }); }, disable: function() { - return this._setOption( "disabled", true ); + return this._setOptions({ disabled: true }); }, _on: function( suppressDisabledCheck, element, handlers ) { var delegateElement, instance = this; @@ -23323,11 +16190,10 @@ if ( !handlers ) { handlers = element; element = this.element; delegateElement = this.widget(); } else { - // accept selectors, DOM elements element = delegateElement = $( element ); this.bindings = this.bindings.add( element ); } $.each( handlers, function( event, handler ) { @@ -23348,11 +16214,11 @@ if ( typeof handler !== "string" ) { handlerProxy.guid = handler.guid = handler.guid || handlerProxy.guid || $.guid++; } - var match = event.match( /^(\w+)\s*(.*)$/ ), + var match = event.match( /^([\w:-]*)\s*(.*)$/ ), eventName = match[1] + instance.eventNamespace, selector = match[2]; if ( selector ) { delegateElement.delegate( selector, eventName, handlerProxy ); } else { @@ -23463,12 +16329,1511 @@ }); } }; }); +return $.widget; + })); /* + * jQuery UI Timepicker + * + * Copyright 2010-2013, Francois Gelinas + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://fgelinas.com/code/timepicker + * + * Depends: + * jquery.ui.core.js + * jquery.ui.position.js (only if position settings are used) + * + * Change version 0.1.0 - moved the t-rex up here + * + ____ + ___ .-~. /_"-._ + `-._~-. / /_ "~o\ :Y + \ \ / : \~x. ` ') + ] Y / | Y< ~-.__j + / ! _.--~T : l l< /.-~ + / / ____.--~ . ` l /~\ \<|Y + / / .-~~" /| . ',-~\ \L| + / / / .^ \ Y~Y \.^>/l_ "--' + / Y .-"( . l__ j_j l_/ /~_.-~ . + Y l / \ ) ~~~." / `/"~ / \.__/l_ + | \ _.-" ~-{__ l : l._Z~-.___.--~ + | ~---~ / ~~"---\_ ' __[> + l . _.^ ___ _>-y~ + \ \ . .-~ .-~ ~>--" / + \ ~---" / ./ _.-' + "-.,_____.,_ _.--~\ _.-~ + ~~ ( _} -Row + `. ~( + ) \ + /,`--'~\--'~\ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ->T-Rex<- +*/ + + +(function ($) { + + $.extend($.ui, { timepicker: { version: "0.3.3"} }); + + var PROP_NAME = 'timepicker', + tpuuid = new Date().getTime(); + + /* Time picker manager. + Use the singleton instance of this class, $.timepicker, to interact with the time picker. + Settings for (groups of) time pickers are maintained in an instance object, + allowing multiple different settings on the same page. */ + + function Timepicker() { + this.debug = true; // Change this to true to start debugging + this._curInst = null; // The current instance in use + this._disabledInputs = []; // List of time picker inputs that have been disabled + this._timepickerShowing = false; // True if the popup picker is showing , false if not + this._inDialog = false; // True if showing within a "dialog", false if not + this._dialogClass = 'ui-timepicker-dialog'; // The name of the dialog marker class + this._mainDivId = 'ui-timepicker-div'; // The ID of the main timepicker division + this._inlineClass = 'ui-timepicker-inline'; // The name of the inline marker class + this._currentClass = 'ui-timepicker-current'; // The name of the current hour / minutes marker class + this._dayOverClass = 'ui-timepicker-days-cell-over'; // The name of the day hover marker class + + this.regional = []; // Available regional settings, indexed by language code + this.regional[''] = { // Default regional settings + hourText: 'Hour', // Display text for hours section + minuteText: 'Minute', // Display text for minutes link + amPmText: ['AM', 'PM'], // Display text for AM PM + closeButtonText: 'Done', // Text for the confirmation button (ok button) + nowButtonText: 'Now', // Text for the now button + deselectButtonText: 'Deselect' // Text for the deselect button + }; + this._defaults = { // Global defaults for all the time picker instances + showOn: 'focus', // 'focus' for popup on focus, + // 'button' for trigger button, or 'both' for either (not yet implemented) + button: null, // 'button' element that will trigger the timepicker + showAnim: 'fadeIn', // Name of jQuery animation for popup + showOptions: {}, // Options for enhanced animations + appendText: '', // Display text following the input box, e.g. showing the format + + beforeShow: null, // Define a callback function executed before the timepicker is shown + onSelect: null, // Define a callback function when a hour / minutes is selected + onClose: null, // Define a callback function when the timepicker is closed + + timeSeparator: ':', // The character to use to separate hours and minutes. + periodSeparator: ' ', // The character to use to separate the time from the time period. + showPeriod: false, // Define whether or not to show AM/PM with selected time + showPeriodLabels: true, // Show the AM/PM labels on the left of the time picker + showLeadingZero: true, // Define whether or not to show a leading zero for hours < 10. [true/false] + showMinutesLeadingZero: true, // Define whether or not to show a leading zero for minutes < 10. + altField: '', // Selector for an alternate field to store selected time into + defaultTime: 'now', // Used as default time when input field is empty or for inline timePicker + // (set to 'now' for the current time, '' for no highlighted time) + myPosition: 'left top', // Position of the dialog relative to the input. + // see the position utility for more info : http://jqueryui.com/demos/position/ + atPosition: 'left bottom', // Position of the input element to match + // Note : if the position utility is not loaded, the timepicker will attach left top to left bottom + //NEW: 2011-02-03 + onHourShow: null, // callback for enabling / disabling on selectable hours ex : function(hour) { return true; } + onMinuteShow: null, // callback for enabling / disabling on time selection ex : function(hour,minute) { return true; } + + hours: { + starts: 0, // first displayed hour + ends: 23 // last displayed hour + }, + minutes: { + starts: 0, // first displayed minute + ends: 55, // last displayed minute + interval: 5, // interval of displayed minutes + manual: [] // optional extra manual entries for minutes + }, + rows: 4, // number of rows for the input tables, minimum 2, makes more sense if you use multiple of 2 + // 2011-08-05 0.2.4 + showHours: true, // display the hours section of the dialog + showMinutes: true, // display the minute section of the dialog + optionalMinutes: false, // optionally parse inputs of whole hours with minutes omitted + + // buttons + showCloseButton: false, // shows an OK button to confirm the edit + showNowButton: false, // Shows the 'now' button + showDeselectButton: false, // Shows the deselect time button + + maxTime: { + hour: null, + minute: null + }, + minTime: { + hour: null, + minute: null + } + + }; + $.extend(this._defaults, this.regional['']); + + this.tpDiv = $('<div id="' + this._mainDivId + '" class="ui-timepicker ui-widget ui-helper-clearfix ui-corner-all " style="display: none"></div>'); + } + + $.extend(Timepicker.prototype, { + /* Class name added to elements to indicate already configured with a time picker. */ + markerClassName: 'hasTimepicker', + + /* Debug logging (if enabled). */ + log: function () { + if (this.debug) + console.log.apply('', arguments); + }, + + _widgetTimepicker: function () { + return this.tpDiv; + }, + + /* Override the default settings for all instances of the time picker. + @param settings object - the new settings to use as defaults (anonymous object) + @return the manager object */ + setDefaults: function (settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* Attach the time picker to a jQuery selection. + @param target element - the target input field or division or span + @param settings object - the new settings to use for this time picker instance (anonymous) */ + _attachTimepicker: function (target, settings) { + // check for settings on the control itself - in namespace 'time:' + var inlineSettings = null; + for (var attrName in this._defaults) { + var attrValue = target.getAttribute('time:' + attrName); + if (attrValue) { + inlineSettings = inlineSettings || {}; + try { + inlineSettings[attrName] = eval(attrValue); + } catch (err) { + inlineSettings[attrName] = attrValue; + } + } + } + var nodeName = target.nodeName.toLowerCase(); + var inline = (nodeName == 'div' || nodeName == 'span'); + + if (!target.id) { + this.uuid += 1; + target.id = 'tp' + this.uuid; + } + var inst = this._newInst($(target), inline); + inst.settings = $.extend({}, settings || {}, inlineSettings || {}); + if (nodeName == 'input') { + this._connectTimepicker(target, inst); + // init inst.hours and inst.minutes from the input value + this._setTimeFromField(inst); + } else if (inline) { + this._inlineTimepicker(target, inst); + } + + + }, + + /* Create a new instance object. */ + _newInst: function (target, inline) { + var id = target[0].id.replace(/([^A-Za-z0-9_-])/g, '\\\\$1'); // escape jQuery meta chars + return { + id: id, input: target, // associated target + inline: inline, // is timepicker inline or not : + tpDiv: (!inline ? this.tpDiv : // presentation div + $('<div class="' + this._inlineClass + ' ui-timepicker ui-widget ui-helper-clearfix"></div>')) + }; + }, + + /* Attach the time picker to an input field. */ + _connectTimepicker: function (target, inst) { + var input = $(target); + inst.append = $([]); + inst.trigger = $([]); + if (input.hasClass(this.markerClassName)) { return; } + this._attachments(input, inst); + input.addClass(this.markerClassName). + keydown(this._doKeyDown). + keyup(this._doKeyUp). + bind("setData.timepicker", function (event, key, value) { + inst.settings[key] = value; + }). + bind("getData.timepicker", function (event, key) { + return this._get(inst, key); + }); + $.data(target, PROP_NAME, inst); + }, + + /* Handle keystrokes. */ + _doKeyDown: function (event) { + var inst = $.timepicker._getInst(event.target); + var handled = true; + inst._keyEvent = true; + if ($.timepicker._timepickerShowing) { + switch (event.keyCode) { + case 9: $.timepicker._hideTimepicker(); + handled = false; + break; // hide on tab out + case 13: + $.timepicker._updateSelectedValue(inst); + $.timepicker._hideTimepicker(); + + return false; // don't submit the form + break; // select the value on enter + case 27: $.timepicker._hideTimepicker(); + break; // hide on escape + default: handled = false; + } + } + else if (event.keyCode == 36 && event.ctrlKey) { // display the time picker on ctrl+home + $.timepicker._showTimepicker(this); + } + else { + handled = false; + } + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }, + + /* Update selected time on keyUp */ + /* Added verion 0.0.5 */ + _doKeyUp: function (event) { + var inst = $.timepicker._getInst(event.target); + $.timepicker._setTimeFromField(inst); + $.timepicker._updateTimepicker(inst); + }, + + /* Make attachments based on settings. */ + _attachments: function (input, inst) { + var appendText = this._get(inst, 'appendText'); + var isRTL = this._get(inst, 'isRTL'); + if (inst.append) { inst.append.remove(); } + if (appendText) { + inst.append = $('<span class="' + this._appendClass + '">' + appendText + '</span>'); + input[isRTL ? 'before' : 'after'](inst.append); + } + input.unbind('focus.timepicker', this._showTimepicker); + input.unbind('click.timepicker', this._adjustZIndex); + + if (inst.trigger) { inst.trigger.remove(); } + + var showOn = this._get(inst, 'showOn'); + if (showOn == 'focus' || showOn == 'both') { // pop-up time picker when in the marked field + input.bind("focus.timepicker", this._showTimepicker); + input.bind("click.timepicker", this._adjustZIndex); + } + if (showOn == 'button' || showOn == 'both') { // pop-up time picker when 'button' element is clicked + var button = this._get(inst, 'button'); + + // Add button if button element is not set + if(button == null) { + button = $('<button class="ui-timepicker-trigger" type="button">...</button>'); + input.after(button); + } + + $(button).bind("click.timepicker", function () { + if ($.timepicker._timepickerShowing && $.timepicker._lastInput == input[0]) { + $.timepicker._hideTimepicker(); + } else if (!inst.input.is(':disabled')) { + $.timepicker._showTimepicker(input[0]); + } + return false; + }); + + } + }, + + + /* Attach an inline time picker to a div. */ + _inlineTimepicker: function(target, inst) { + var divSpan = $(target); + if (divSpan.hasClass(this.markerClassName)) + return; + divSpan.addClass(this.markerClassName).append(inst.tpDiv). + bind("setData.timepicker", function(event, key, value){ + inst.settings[key] = value; + }).bind("getData.timepicker", function(event, key){ + return this._get(inst, key); + }); + $.data(target, PROP_NAME, inst); + + this._setTimeFromField(inst); + this._updateTimepicker(inst); + inst.tpDiv.show(); + }, + + _adjustZIndex: function(input) { + input = input.target || input; + var inst = $.timepicker._getInst(input); + inst.tpDiv.css('zIndex', $.timepicker._getZIndex(input) +1); + }, + + /* Pop-up the time picker for a given input field. + @param input element - the input field attached to the time picker or + event - if triggered by focus */ + _showTimepicker: function (input) { + input = input.target || input; + if (input.nodeName.toLowerCase() != 'input') { input = $('input', input.parentNode)[0]; } // find from button/image trigger + + if ($.timepicker._isDisabledTimepicker(input) || $.timepicker._lastInput == input) { return; } // already here + + // fix v 0.0.8 - close current timepicker before showing another one + $.timepicker._hideTimepicker(); + + var inst = $.timepicker._getInst(input); + if ($.timepicker._curInst && $.timepicker._curInst != inst) { + $.timepicker._curInst.tpDiv.stop(true, true); + } + var beforeShow = $.timepicker._get(inst, 'beforeShow'); + extendRemove(inst.settings, (beforeShow ? beforeShow.apply(input, [input, inst]) : {})); + inst.lastVal = null; + $.timepicker._lastInput = input; + + $.timepicker._setTimeFromField(inst); + + // calculate default position + if ($.timepicker._inDialog) { input.value = ''; } // hide cursor + if (!$.timepicker._pos) { // position below input + $.timepicker._pos = $.timepicker._findPos(input); + $.timepicker._pos[1] += input.offsetHeight; // add the height + } + var isFixed = false; + $(input).parents().each(function () { + isFixed |= $(this).css('position') == 'fixed'; + return !isFixed; + }); + + var offset = { left: $.timepicker._pos[0], top: $.timepicker._pos[1] }; + + $.timepicker._pos = null; + // determine sizing offscreen + inst.tpDiv.css({ position: 'absolute', display: 'block', top: '-1000px' }); + $.timepicker._updateTimepicker(inst); + + + // position with the ui position utility, if loaded + if ( ( ! inst.inline ) && ( typeof $.ui.position == 'object' ) ) { + inst.tpDiv.position({ + of: inst.input, + my: $.timepicker._get( inst, 'myPosition' ), + at: $.timepicker._get( inst, 'atPosition' ), + // offset: $( "#offset" ).val(), + // using: using, + collision: 'flip' + }); + var offset = inst.tpDiv.offset(); + $.timepicker._pos = [offset.top, offset.left]; + } + + + // reset clicked state + inst._hoursClicked = false; + inst._minutesClicked = false; + + // fix width for dynamic number of time pickers + // and adjust position before showing + offset = $.timepicker._checkOffset(inst, offset, isFixed); + inst.tpDiv.css({ position: ($.timepicker._inDialog && $.blockUI ? + 'static' : (isFixed ? 'fixed' : 'absolute')), display: 'none', + left: offset.left + 'px', top: offset.top + 'px' + }); + if ( ! inst.inline ) { + var showAnim = $.timepicker._get(inst, 'showAnim'); + var duration = $.timepicker._get(inst, 'duration'); + + var postProcess = function () { + $.timepicker._timepickerShowing = true; + var borders = $.timepicker._getBorders(inst.tpDiv); + inst.tpDiv.find('iframe.ui-timepicker-cover'). // IE6- only + css({ left: -borders[0], top: -borders[1], + width: inst.tpDiv.outerWidth(), height: inst.tpDiv.outerHeight() + }); + }; + + // Fixed the zIndex problem for real (I hope) - FG - v 0.2.9 + $.timepicker._adjustZIndex(input); + //inst.tpDiv.css('zIndex', $.timepicker._getZIndex(input) +1); + + if ($.effects && $.effects[showAnim]) { + inst.tpDiv.show(showAnim, $.timepicker._get(inst, 'showOptions'), duration, postProcess); + } + else { + inst.tpDiv.show((showAnim ? duration : null), postProcess); + } + if (!showAnim || !duration) { postProcess(); } + if (inst.input.is(':visible') && !inst.input.is(':disabled')) { inst.input.focus(); } + $.timepicker._curInst = inst; + } + }, + + // This is an enhanced copy of the zIndex function of UI core 1.8.?? For backward compatibility. + // Enhancement returns maximum zindex value discovered while traversing parent elements, + // rather than the first zindex value found. Ensures the timepicker popup will be in front, + // even in funky scenarios like non-jq dialog containers with large fixed zindex values and + // nested zindex-influenced elements of their own. + _getZIndex: function (target) { + var elem = $(target); + var maxValue = 0; + var position, value; + while (elem.length && elem[0] !== document) { + position = elem.css("position"); + if (position === "absolute" || position === "relative" || position === "fixed") { + value = parseInt(elem.css("zIndex"), 10); + if (!isNaN(value) && value !== 0) { + if (value > maxValue) { maxValue = value; } + } + } + elem = elem.parent(); + } + + return maxValue; + }, + + /* Refresh the time picker + @param target element - The target input field or inline container element. */ + _refreshTimepicker: function(target) { + var inst = this._getInst(target); + if (inst) { + this._updateTimepicker(inst); + } + }, + + + /* Generate the time picker content. */ + _updateTimepicker: function (inst) { + inst.tpDiv.empty().append(this._generateHTML(inst)); + this._rebindDialogEvents(inst); + + }, + + _rebindDialogEvents: function (inst) { + var borders = $.timepicker._getBorders(inst.tpDiv), + self = this; + inst.tpDiv + .find('iframe.ui-timepicker-cover') // IE6- only + .css({ left: -borders[0], top: -borders[1], + width: inst.tpDiv.outerWidth(), height: inst.tpDiv.outerHeight() + }) + .end() + // after the picker html is appended bind the click & double click events (faster in IE this way + // then letting the browser interpret the inline events) + // the binding for the minute cells also exists in _updateMinuteDisplay + .find('.ui-timepicker-minute-cell') + .unbind() + .bind("click", { fromDoubleClick:false }, $.proxy($.timepicker.selectMinutes, this)) + .bind("dblclick", { fromDoubleClick:true }, $.proxy($.timepicker.selectMinutes, this)) + .end() + .find('.ui-timepicker-hour-cell') + .unbind() + .bind("click", { fromDoubleClick:false }, $.proxy($.timepicker.selectHours, this)) + .bind("dblclick", { fromDoubleClick:true }, $.proxy($.timepicker.selectHours, this)) + .end() + .find('.ui-timepicker td a') + .unbind() + .bind('mouseout', function () { + $(this).removeClass('ui-state-hover'); + if (this.className.indexOf('ui-timepicker-prev') != -1) $(this).removeClass('ui-timepicker-prev-hover'); + if (this.className.indexOf('ui-timepicker-next') != -1) $(this).removeClass('ui-timepicker-next-hover'); + }) + .bind('mouseover', function () { + if ( ! self._isDisabledTimepicker(inst.inline ? inst.tpDiv.parent()[0] : inst.input[0])) { + $(this).parents('.ui-timepicker-calendar').find('a').removeClass('ui-state-hover'); + $(this).addClass('ui-state-hover'); + if (this.className.indexOf('ui-timepicker-prev') != -1) $(this).addClass('ui-timepicker-prev-hover'); + if (this.className.indexOf('ui-timepicker-next') != -1) $(this).addClass('ui-timepicker-next-hover'); + } + }) + .end() + .find('.' + this._dayOverClass + ' a') + .trigger('mouseover') + .end() + .find('.ui-timepicker-now').bind("click", function(e) { + $.timepicker.selectNow(e); + }).end() + .find('.ui-timepicker-deselect').bind("click",function(e) { + $.timepicker.deselectTime(e); + }).end() + .find('.ui-timepicker-close').bind("click",function(e) { + $.timepicker._hideTimepicker(); + }).end(); + }, + + /* Generate the HTML for the current state of the time picker. */ + _generateHTML: function (inst) { + + var h, m, row, col, html, hoursHtml, minutesHtml = '', + showPeriod = (this._get(inst, 'showPeriod') == true), + showPeriodLabels = (this._get(inst, 'showPeriodLabels') == true), + showLeadingZero = (this._get(inst, 'showLeadingZero') == true), + showHours = (this._get(inst, 'showHours') == true), + showMinutes = (this._get(inst, 'showMinutes') == true), + amPmText = this._get(inst, 'amPmText'), + rows = this._get(inst, 'rows'), + amRows = 0, + pmRows = 0, + amItems = 0, + pmItems = 0, + amFirstRow = 0, + pmFirstRow = 0, + hours = Array(), + hours_options = this._get(inst, 'hours'), + hoursPerRow = null, + hourCounter = 0, + hourLabel = this._get(inst, 'hourText'), + showCloseButton = this._get(inst, 'showCloseButton'), + closeButtonText = this._get(inst, 'closeButtonText'), + showNowButton = this._get(inst, 'showNowButton'), + nowButtonText = this._get(inst, 'nowButtonText'), + showDeselectButton = this._get(inst, 'showDeselectButton'), + deselectButtonText = this._get(inst, 'deselectButtonText'), + showButtonPanel = showCloseButton || showNowButton || showDeselectButton; + + + + // prepare all hours and minutes, makes it easier to distribute by rows + for (h = hours_options.starts; h <= hours_options.ends; h++) { + hours.push (h); + } + hoursPerRow = Math.ceil(hours.length / rows); // always round up + + if (showPeriodLabels) { + for (hourCounter = 0; hourCounter < hours.length; hourCounter++) { + if (hours[hourCounter] < 12) { + amItems++; + } + else { + pmItems++; + } + } + hourCounter = 0; + + amRows = Math.floor(amItems / hours.length * rows); + pmRows = Math.floor(pmItems / hours.length * rows); + + // assign the extra row to the period that is more densely populated + if (rows != amRows + pmRows) { + // Make sure: AM Has Items and either PM Does Not, AM has no rows yet, or AM is more dense + if (amItems && (!pmItems || !amRows || (pmRows && amItems / amRows >= pmItems / pmRows))) { + amRows++; + } else { + pmRows++; + } + } + amFirstRow = Math.min(amRows, 1); + pmFirstRow = amRows + 1; + + if (amRows == 0) { + hoursPerRow = Math.ceil(pmItems / pmRows); + } else if (pmRows == 0) { + hoursPerRow = Math.ceil(amItems / amRows); + } else { + hoursPerRow = Math.ceil(Math.max(amItems / amRows, pmItems / pmRows)); + } + } + + + html = '<table class="ui-timepicker-table ui-widget-content ui-corner-all"><tr>'; + + if (showHours) { + + html += '<td class="ui-timepicker-hours">' + + '<div class="ui-timepicker-title ui-widget-header ui-helper-clearfix ui-corner-all">' + + hourLabel + + '</div>' + + '<table class="ui-timepicker">'; + + for (row = 1; row <= rows; row++) { + html += '<tr>'; + // AM + if (row == amFirstRow && showPeriodLabels) { + html += '<th rowspan="' + amRows.toString() + '" class="periods" scope="row">' + amPmText[0] + '</th>'; + } + // PM + if (row == pmFirstRow && showPeriodLabels) { + html += '<th rowspan="' + pmRows.toString() + '" class="periods" scope="row">' + amPmText[1] + '</th>'; + } + for (col = 1; col <= hoursPerRow; col++) { + if (showPeriodLabels && row < pmFirstRow && hours[hourCounter] >= 12) { + html += this._generateHTMLHourCell(inst, undefined, showPeriod, showLeadingZero); + } else { + html += this._generateHTMLHourCell(inst, hours[hourCounter], showPeriod, showLeadingZero); + hourCounter++; + } + } + html += '</tr>'; + } + html += '</table>' + // Close the hours cells table + '</td>'; // Close the Hour td + } + + if (showMinutes) { + html += '<td class="ui-timepicker-minutes">'; + html += this._generateHTMLMinutes(inst); + html += '</td>'; + } + + html += '</tr>'; + + + if (showButtonPanel) { + var buttonPanel = '<tr><td colspan="3"><div class="ui-timepicker-buttonpane ui-widget-content">'; + if (showNowButton) { + buttonPanel += '<button type="button" class="ui-timepicker-now ui-state-default ui-corner-all" ' + + ' data-timepicker-instance-id="#' + inst.id.replace(/\\\\/g,"\\") + '" >' + + nowButtonText + '</button>'; + } + if (showDeselectButton) { + buttonPanel += '<button type="button" class="ui-timepicker-deselect ui-state-default ui-corner-all" ' + + ' data-timepicker-instance-id="#' + inst.id.replace(/\\\\/g,"\\") + '" >' + + deselectButtonText + '</button>'; + } + if (showCloseButton) { + buttonPanel += '<button type="button" class="ui-timepicker-close ui-state-default ui-corner-all" ' + + ' data-timepicker-instance-id="#' + inst.id.replace(/\\\\/g,"\\") + '" >' + + closeButtonText + '</button>'; + } + + html += buttonPanel + '</div></td></tr>'; + } + html += '</table>'; + + return html; + }, + + /* Special function that update the minutes selection in currently visible timepicker + * called on hour selection when onMinuteShow is defined */ + _updateMinuteDisplay: function (inst) { + var newHtml = this._generateHTMLMinutes(inst); + inst.tpDiv.find('td.ui-timepicker-minutes').html(newHtml); + this._rebindDialogEvents(inst); + // after the picker html is appended bind the click & double click events (faster in IE this way + // then letting the browser interpret the inline events) + // yes I know, duplicate code, sorry +/* .find('.ui-timepicker-minute-cell') + .bind("click", { fromDoubleClick:false }, $.proxy($.timepicker.selectMinutes, this)) + .bind("dblclick", { fromDoubleClick:true }, $.proxy($.timepicker.selectMinutes, this)); +*/ + + }, + + /* + * Generate the minutes table + * This is separated from the _generateHTML function because is can be called separately (when hours changes) + */ + _generateHTMLMinutes: function (inst) { + + var m, row, html = '', + rows = this._get(inst, 'rows'), + minutes = Array(), + minutes_options = this._get(inst, 'minutes'), + minutesPerRow = null, + minuteCounter = 0, + showMinutesLeadingZero = (this._get(inst, 'showMinutesLeadingZero') == true), + onMinuteShow = this._get(inst, 'onMinuteShow'), + minuteLabel = this._get(inst, 'minuteText'); + + if ( ! minutes_options.starts) { + minutes_options.starts = 0; + } + if ( ! minutes_options.ends) { + minutes_options.ends = 59; + } + if ( ! minutes_options.manual) { + minutes_options.manual = []; + } + for (m = minutes_options.starts; m <= minutes_options.ends; m += minutes_options.interval) { + minutes.push(m); + } + for (i = 0; i < minutes_options.manual.length;i++) { + var currMin = minutes_options.manual[i]; + + // Validate & filter duplicates of manual minute input + if (typeof currMin != 'number' || currMin < 0 || currMin > 59 || $.inArray(currMin, minutes) >= 0) { + continue; + } + minutes.push(currMin); + } + + // Sort to get correct order after adding manual minutes + // Use compare function to sort by number, instead of string (default) + minutes.sort(function(a, b) { + return a-b; + }); + + minutesPerRow = Math.round(minutes.length / rows + 0.49); // always round up + + /* + * The minutes table + */ + // if currently selected minute is not enabled, we have a problem and need to select a new minute. + if (onMinuteShow && + (onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours , inst.minutes]) == false) ) { + // loop minutes and select first available + for (minuteCounter = 0; minuteCounter < minutes.length; minuteCounter += 1) { + m = minutes[minuteCounter]; + if (onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours, m])) { + inst.minutes = m; + break; + } + } + } + + + + html += '<div class="ui-timepicker-title ui-widget-header ui-helper-clearfix ui-corner-all">' + + minuteLabel + + '</div>' + + '<table class="ui-timepicker">'; + + minuteCounter = 0; + for (row = 1; row <= rows; row++) { + html += '<tr>'; + while (minuteCounter < row * minutesPerRow) { + var m = minutes[minuteCounter]; + var displayText = ''; + if (m !== undefined ) { + displayText = (m < 10) && showMinutesLeadingZero ? "0" + m.toString() : m.toString(); + } + html += this._generateHTMLMinuteCell(inst, m, displayText); + minuteCounter++; + } + html += '</tr>'; + } + + html += '</table>'; + + return html; + }, + + /* Generate the content of a "Hour" cell */ + _generateHTMLHourCell: function (inst, hour, showPeriod, showLeadingZero) { + + var displayHour = hour; + if ((hour > 12) && showPeriod) { + displayHour = hour - 12; + } + if ((displayHour == 0) && showPeriod) { + displayHour = 12; + } + if ((displayHour < 10) && showLeadingZero) { + displayHour = '0' + displayHour; + } + + var html = ""; + var enabled = true; + var onHourShow = this._get(inst, 'onHourShow'); //custom callback + var maxTime = this._get(inst, 'maxTime'); + var minTime = this._get(inst, 'minTime'); + + if (hour == undefined) { + html = '<td><span class="ui-state-default ui-state-disabled">&nbsp;</span></td>'; + return html; + } + + if (onHourShow) { + enabled = onHourShow.apply((inst.input ? inst.input[0] : null), [hour]); + } + + if (enabled) { + if ( !isNaN(parseInt(maxTime.hour)) && hour > maxTime.hour ) enabled = false; + if ( !isNaN(parseInt(minTime.hour)) && hour < minTime.hour ) enabled = false; + } + + if (enabled) { + html = '<td class="ui-timepicker-hour-cell" data-timepicker-instance-id="#' + inst.id.replace(/\\\\/g,"\\") + '" data-hour="' + hour.toString() + '">' + + '<a class="ui-state-default ' + + (hour == inst.hours ? 'ui-state-active' : '') + + '">' + + displayHour.toString() + + '</a></td>'; + } + else { + html = + '<td>' + + '<span class="ui-state-default ui-state-disabled ' + + (hour == inst.hours ? ' ui-state-active ' : ' ') + + '">' + + displayHour.toString() + + '</span>' + + '</td>'; + } + return html; + }, + + /* Generate the content of a "Hour" cell */ + _generateHTMLMinuteCell: function (inst, minute, displayText) { + var html = ""; + var enabled = true; + var hour = inst.hours; + var onMinuteShow = this._get(inst, 'onMinuteShow'); //custom callback + var maxTime = this._get(inst, 'maxTime'); + var minTime = this._get(inst, 'minTime'); + + if (onMinuteShow) { + //NEW: 2011-02-03 we should give the hour as a parameter as well! + enabled = onMinuteShow.apply((inst.input ? inst.input[0] : null), [inst.hours,minute]); //trigger callback + } + + if (minute == undefined) { + html = '<td><span class="ui-state-default ui-state-disabled">&nbsp;</span></td>'; + return html; + } + + if (enabled && hour !== null) { + if ( !isNaN(parseInt(maxTime.hour)) && !isNaN(parseInt(maxTime.minute)) && hour >= maxTime.hour && minute > maxTime.minute ) enabled = false; + if ( !isNaN(parseInt(minTime.hour)) && !isNaN(parseInt(minTime.minute)) && hour <= minTime.hour && minute < minTime.minute ) enabled = false; + } + + if (enabled) { + html = '<td class="ui-timepicker-minute-cell" data-timepicker-instance-id="#' + inst.id.replace(/\\\\/g,"\\") + '" data-minute="' + minute.toString() + '" >' + + '<a class="ui-state-default ' + + (minute == inst.minutes ? 'ui-state-active' : '') + + '" >' + + displayText + + '</a></td>'; + } + else { + + html = '<td>' + + '<span class="ui-state-default ui-state-disabled" >' + + displayText + + '</span>' + + '</td>'; + } + return html; + }, + + + /* Detach a timepicker from its control. + @param target element - the target input field or division or span */ + _destroyTimepicker: function(target) { + var $target = $(target); + var inst = $.data(target, PROP_NAME); + if (!$target.hasClass(this.markerClassName)) { + return; + } + var nodeName = target.nodeName.toLowerCase(); + $.removeData(target, PROP_NAME); + if (nodeName == 'input') { + inst.append.remove(); + inst.trigger.remove(); + $target.removeClass(this.markerClassName) + .unbind('focus.timepicker', this._showTimepicker) + .unbind('click.timepicker', this._adjustZIndex); + } else if (nodeName == 'div' || nodeName == 'span') + $target.removeClass(this.markerClassName).empty(); + }, + + /* Enable the date picker to a jQuery selection. + @param target element - the target input field or division or span */ + _enableTimepicker: function(target) { + var $target = $(target), + target_id = $target.attr('id'), + inst = $.data(target, PROP_NAME); + + if (!$target.hasClass(this.markerClassName)) { + return; + } + var nodeName = target.nodeName.toLowerCase(); + if (nodeName == 'input') { + target.disabled = false; + var button = this._get(inst, 'button'); + $(button).removeClass('ui-state-disabled').disabled = false; + inst.trigger.filter('button'). + each(function() { this.disabled = false; }).end(); + } + else if (nodeName == 'div' || nodeName == 'span') { + var inline = $target.children('.' + this._inlineClass); + inline.children().removeClass('ui-state-disabled'); + inline.find('button').each( + function() { this.disabled = false } + ) + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value == target_id ? null : value); }); // delete entry + }, + + /* Disable the time picker to a jQuery selection. + @param target element - the target input field or division or span */ + _disableTimepicker: function(target) { + var $target = $(target); + var inst = $.data(target, PROP_NAME); + if (!$target.hasClass(this.markerClassName)) { + return; + } + var nodeName = target.nodeName.toLowerCase(); + if (nodeName == 'input') { + var button = this._get(inst, 'button'); + + $(button).addClass('ui-state-disabled').disabled = true; + target.disabled = true; + + inst.trigger.filter('button'). + each(function() { this.disabled = true; }).end(); + + } + else if (nodeName == 'div' || nodeName == 'span') { + var inline = $target.children('.' + this._inlineClass); + inline.children().addClass('ui-state-disabled'); + inline.find('button').each( + function() { this.disabled = true } + ) + + } + this._disabledInputs = $.map(this._disabledInputs, + function(value) { return (value == target ? null : value); }); // delete entry + this._disabledInputs[this._disabledInputs.length] = $target.attr('id'); + }, + + /* Is the first field in a jQuery collection disabled as a timepicker? + @param target_id element - the target input field or division or span + @return boolean - true if disabled, false if enabled */ + _isDisabledTimepicker: function (target_id) { + if ( ! target_id) { return false; } + for (var i = 0; i < this._disabledInputs.length; i++) { + if (this._disabledInputs[i] == target_id) { return true; } + } + return false; + }, + + /* Check positioning to remain on screen. */ + _checkOffset: function (inst, offset, isFixed) { + var tpWidth = inst.tpDiv.outerWidth(); + var tpHeight = inst.tpDiv.outerHeight(); + var inputWidth = inst.input ? inst.input.outerWidth() : 0; + var inputHeight = inst.input ? inst.input.outerHeight() : 0; + var viewWidth = document.documentElement.clientWidth + $(document).scrollLeft(); + var viewHeight = document.documentElement.clientHeight + $(document).scrollTop(); + + offset.left -= (this._get(inst, 'isRTL') ? (tpWidth - inputWidth) : 0); + offset.left -= (isFixed && offset.left == inst.input.offset().left) ? $(document).scrollLeft() : 0; + offset.top -= (isFixed && offset.top == (inst.input.offset().top + inputHeight)) ? $(document).scrollTop() : 0; + + // now check if timepicker is showing outside window viewport - move to a better place if so. + offset.left -= Math.min(offset.left, (offset.left + tpWidth > viewWidth && viewWidth > tpWidth) ? + Math.abs(offset.left + tpWidth - viewWidth) : 0); + offset.top -= Math.min(offset.top, (offset.top + tpHeight > viewHeight && viewHeight > tpHeight) ? + Math.abs(tpHeight + inputHeight) : 0); + + return offset; + }, + + /* Find an object's position on the screen. */ + _findPos: function (obj) { + var inst = this._getInst(obj); + var isRTL = this._get(inst, 'isRTL'); + while (obj && (obj.type == 'hidden' || obj.nodeType != 1)) { + obj = obj[isRTL ? 'previousSibling' : 'nextSibling']; + } + var position = $(obj).offset(); + return [position.left, position.top]; + }, + + /* Retrieve the size of left and top borders for an element. + @param elem (jQuery object) the element of interest + @return (number[2]) the left and top borders */ + _getBorders: function (elem) { + var convert = function (value) { + return { thin: 1, medium: 2, thick: 3}[value] || value; + }; + return [parseFloat(convert(elem.css('border-left-width'))), + parseFloat(convert(elem.css('border-top-width')))]; + }, + + + /* Close time picker if clicked elsewhere. */ + _checkExternalClick: function (event) { + if (!$.timepicker._curInst) { return; } + var $target = $(event.target); + if ($target[0].id != $.timepicker._mainDivId && + $target.parents('#' + $.timepicker._mainDivId).length == 0 && + !$target.hasClass($.timepicker.markerClassName) && + !$target.hasClass($.timepicker._triggerClass) && + $.timepicker._timepickerShowing && !($.timepicker._inDialog && $.blockUI)) + $.timepicker._hideTimepicker(); + }, + + /* Hide the time picker from view. + @param input element - the input field attached to the time picker */ + _hideTimepicker: function (input) { + var inst = this._curInst; + if (!inst || (input && inst != $.data(input, PROP_NAME))) { return; } + if (this._timepickerShowing) { + var showAnim = this._get(inst, 'showAnim'); + var duration = this._get(inst, 'duration'); + var postProcess = function () { + $.timepicker._tidyDialog(inst); + this._curInst = null; + }; + if ($.effects && $.effects[showAnim]) { + inst.tpDiv.hide(showAnim, $.timepicker._get(inst, 'showOptions'), duration, postProcess); + } + else { + inst.tpDiv[(showAnim == 'slideDown' ? 'slideUp' : + (showAnim == 'fadeIn' ? 'fadeOut' : 'hide'))]((showAnim ? duration : null), postProcess); + } + if (!showAnim) { postProcess(); } + + this._timepickerShowing = false; + + this._lastInput = null; + if (this._inDialog) { + this._dialogInput.css({ position: 'absolute', left: '0', top: '-100px' }); + if ($.blockUI) { + $.unblockUI(); + $('body').append(this.tpDiv); + } + } + this._inDialog = false; + + var onClose = this._get(inst, 'onClose'); + if (onClose) { + onClose.apply( + (inst.input ? inst.input[0] : null), + [(inst.input ? inst.input.val() : ''), inst]); // trigger custom callback + } + + } + }, + + + + /* Tidy up after a dialog display. */ + _tidyDialog: function (inst) { + inst.tpDiv.removeClass(this._dialogClass).unbind('.ui-timepicker'); + }, + + /* Retrieve the instance data for the target control. + @param target element - the target input field or division or span + @return object - the associated instance data + @throws error if a jQuery problem getting data */ + _getInst: function (target) { + try { + return $.data(target, PROP_NAME); + } + catch (err) { + throw 'Missing instance data for this timepicker'; + } + }, + + /* Get a setting value, defaulting if necessary. */ + _get: function (inst, name) { + return inst.settings[name] !== undefined ? + inst.settings[name] : this._defaults[name]; + }, + + /* Parse existing time and initialise time picker. */ + _setTimeFromField: function (inst) { + if (inst.input.val() == inst.lastVal) { return; } + var defaultTime = this._get(inst, 'defaultTime'); + + var timeToParse = defaultTime == 'now' ? this._getCurrentTimeRounded(inst) : defaultTime; + if ((inst.inline == false) && (inst.input.val() != '')) { timeToParse = inst.input.val() } + + if (timeToParse instanceof Date) { + inst.hours = timeToParse.getHours(); + inst.minutes = timeToParse.getMinutes(); + } else { + var timeVal = inst.lastVal = timeToParse; + if (timeToParse == '') { + inst.hours = -1; + inst.minutes = -1; + } else { + var time = this.parseTime(inst, timeVal); + inst.hours = time.hours; + inst.minutes = time.minutes; + } + } + + + $.timepicker._updateTimepicker(inst); + }, + + /* Update or retrieve the settings for an existing time picker. + @param target element - the target input field or division or span + @param name object - the new settings to update or + string - the name of the setting to change or retrieve, + when retrieving also 'all' for all instance settings or + 'defaults' for all global defaults + @param value any - the new value for the setting + (omit if above is an object or to retrieve a value) */ + _optionTimepicker: function(target, name, value) { + var inst = this._getInst(target); + if (arguments.length == 2 && typeof name == 'string') { + return (name == 'defaults' ? $.extend({}, $.timepicker._defaults) : + (inst ? (name == 'all' ? $.extend({}, inst.settings) : + this._get(inst, name)) : null)); + } + var settings = name || {}; + if (typeof name == 'string') { + settings = {}; + settings[name] = value; + } + if (inst) { + extendRemove(inst.settings, settings); + if (this._curInst == inst) { + this._hideTimepicker(); + this._updateTimepicker(inst); + } + if (inst.inline) { + this._updateTimepicker(inst); + } + } + }, + + + /* Set the time for a jQuery selection. + @param target element - the target input field or division or span + @param time String - the new time */ + _setTimeTimepicker: function(target, time) { + var inst = this._getInst(target); + if (inst) { + this._setTime(inst, time); + this._updateTimepicker(inst); + this._updateAlternate(inst, time); + } + }, + + /* Set the time directly. */ + _setTime: function(inst, time, noChange) { + var origHours = inst.hours; + var origMinutes = inst.minutes; + if (time instanceof Date) { + inst.hours = time.getHours(); + inst.minutes = time.getMinutes(); + } else { + var time = this.parseTime(inst, time); + inst.hours = time.hours; + inst.minutes = time.minutes; + } + + if ((origHours != inst.hours || origMinutes != inst.minutes) && !noChange) { + inst.input.trigger('change'); + } + this._updateTimepicker(inst); + this._updateSelectedValue(inst); + }, + + /* Return the current time, ready to be parsed, rounded to the closest minute by interval */ + _getCurrentTimeRounded: function (inst) { + var currentTime = new Date(), + currentMinutes = currentTime.getMinutes(), + minutes_options = this._get(inst, 'minutes'), + // round to closest interval + adjustedMinutes = Math.round(currentMinutes / minutes_options.interval) * minutes_options.interval; + currentTime.setMinutes(adjustedMinutes); + return currentTime; + }, + + /* + * Parse a time string into hours and minutes + */ + parseTime: function (inst, timeVal) { + var retVal = new Object(); + retVal.hours = -1; + retVal.minutes = -1; + + if(!timeVal) + return ''; + + var timeSeparator = this._get(inst, 'timeSeparator'), + amPmText = this._get(inst, 'amPmText'), + showHours = this._get(inst, 'showHours'), + showMinutes = this._get(inst, 'showMinutes'), + optionalMinutes = this._get(inst, 'optionalMinutes'), + showPeriod = (this._get(inst, 'showPeriod') == true), + p = timeVal.indexOf(timeSeparator); + + // check if time separator found + if (p != -1) { + retVal.hours = parseInt(timeVal.substr(0, p), 10); + retVal.minutes = parseInt(timeVal.substr(p + 1), 10); + } + // check for hours only + else if ( (showHours) && ( !showMinutes || optionalMinutes ) ) { + retVal.hours = parseInt(timeVal, 10); + } + // check for minutes only + else if ( ( ! showHours) && (showMinutes) ) { + retVal.minutes = parseInt(timeVal, 10); + } + + if (showHours) { + var timeValUpper = timeVal.toUpperCase(); + if ((retVal.hours < 12) && (showPeriod) && (timeValUpper.indexOf(amPmText[1].toUpperCase()) != -1)) { + retVal.hours += 12; + } + // fix for 12 AM + if ((retVal.hours == 12) && (showPeriod) && (timeValUpper.indexOf(amPmText[0].toUpperCase()) != -1)) { + retVal.hours = 0; + } + } + + return retVal; + }, + + selectNow: function(event) { + var id = $(event.target).attr("data-timepicker-instance-id"), + $target = $(id), + inst = this._getInst($target[0]); + //if (!inst || (input && inst != $.data(input, PROP_NAME))) { return; } + var currentTime = new Date(); + inst.hours = currentTime.getHours(); + inst.minutes = currentTime.getMinutes(); + this._updateSelectedValue(inst); + this._updateTimepicker(inst); + this._hideTimepicker(); + }, + + deselectTime: function(event) { + var id = $(event.target).attr("data-timepicker-instance-id"), + $target = $(id), + inst = this._getInst($target[0]); + inst.hours = -1; + inst.minutes = -1; + this._updateSelectedValue(inst); + this._hideTimepicker(); + }, + + + selectHours: function (event) { + var $td = $(event.currentTarget), + id = $td.attr("data-timepicker-instance-id"), + newHours = parseInt($td.attr("data-hour")), + fromDoubleClick = event.data.fromDoubleClick, + $target = $(id), + inst = this._getInst($target[0]), + showMinutes = (this._get(inst, 'showMinutes') == true); + + // don't select if disabled + if ( $.timepicker._isDisabledTimepicker($target.attr('id')) ) { return false } + + $td.parents('.ui-timepicker-hours:first').find('a').removeClass('ui-state-active'); + $td.children('a').addClass('ui-state-active'); + inst.hours = newHours; + + // added for onMinuteShow callback + var onMinuteShow = this._get(inst, 'onMinuteShow'), + maxTime = this._get(inst, 'maxTime'), + minTime = this._get(inst, 'minTime'); + if (onMinuteShow || maxTime.minute || minTime.minute) { + // this will trigger a callback on selected hour to make sure selected minute is allowed. + this._updateMinuteDisplay(inst); + } + + this._updateSelectedValue(inst); + + inst._hoursClicked = true; + if ((inst._minutesClicked) || (fromDoubleClick) || (showMinutes == false)) { + $.timepicker._hideTimepicker(); + } + // return false because if used inline, prevent the url to change to a hashtag + return false; + }, + + selectMinutes: function (event) { + var $td = $(event.currentTarget), + id = $td.attr("data-timepicker-instance-id"), + newMinutes = parseInt($td.attr("data-minute")), + fromDoubleClick = event.data.fromDoubleClick, + $target = $(id), + inst = this._getInst($target[0]), + showHours = (this._get(inst, 'showHours') == true); + + // don't select if disabled + if ( $.timepicker._isDisabledTimepicker($target.attr('id')) ) { return false } + + $td.parents('.ui-timepicker-minutes:first').find('a').removeClass('ui-state-active'); + $td.children('a').addClass('ui-state-active'); + + inst.minutes = newMinutes; + this._updateSelectedValue(inst); + + inst._minutesClicked = true; + if ((inst._hoursClicked) || (fromDoubleClick) || (showHours == false)) { + $.timepicker._hideTimepicker(); + // return false because if used inline, prevent the url to change to a hashtag + return false; + } + + // return false because if used inline, prevent the url to change to a hashtag + return false; + }, + + _updateSelectedValue: function (inst) { + var newTime = this._getParsedTime(inst); + if (inst.input) { + inst.input.val(newTime); + inst.input.trigger('change'); + } + var onSelect = this._get(inst, 'onSelect'); + if (onSelect) { onSelect.apply((inst.input ? inst.input[0] : null), [newTime, inst]); } // trigger custom callback + this._updateAlternate(inst, newTime); + return newTime; + }, + + /* this function process selected time and return it parsed according to instance options */ + _getParsedTime: function(inst) { + + if (inst.hours == -1 && inst.minutes == -1) { + return ''; + } + + // default to 0 AM if hours is not valid + if ((inst.hours < inst.hours.starts) || (inst.hours > inst.hours.ends )) { inst.hours = 0; } + // default to 0 minutes if minute is not valid + if ((inst.minutes < inst.minutes.starts) || (inst.minutes > inst.minutes.ends)) { inst.minutes = 0; } + + var period = "", + showPeriod = (this._get(inst, 'showPeriod') == true), + showLeadingZero = (this._get(inst, 'showLeadingZero') == true), + showHours = (this._get(inst, 'showHours') == true), + showMinutes = (this._get(inst, 'showMinutes') == true), + optionalMinutes = (this._get(inst, 'optionalMinutes') == true), + amPmText = this._get(inst, 'amPmText'), + selectedHours = inst.hours ? inst.hours : 0, + selectedMinutes = inst.minutes ? inst.minutes : 0, + displayHours = selectedHours ? selectedHours : 0, + parsedTime = ''; + + // fix some display problem when hours or minutes are not selected yet + if (displayHours == -1) { displayHours = 0 } + if (selectedMinutes == -1) { selectedMinutes = 0 } + + if (showPeriod) { + if (inst.hours == 0) { + displayHours = 12; + } + if (inst.hours < 12) { + period = amPmText[0]; + } + else { + period = amPmText[1]; + if (displayHours > 12) { + displayHours -= 12; + } + } + } + + var h = displayHours.toString(); + if (showLeadingZero && (displayHours < 10)) { h = '0' + h; } + + var m = selectedMinutes.toString(); + if (selectedMinutes < 10) { m = '0' + m; } + + if (showHours) { + parsedTime += h; + } + if (showHours && showMinutes && (!optionalMinutes || m != 0)) { + parsedTime += this._get(inst, 'timeSeparator'); + } + if (showMinutes && (!optionalMinutes || m != 0)) { + parsedTime += m; + } + if (showHours) { + if (period.length > 0) { parsedTime += this._get(inst, 'periodSeparator') + period; } + } + + return parsedTime; + }, + + /* Update any alternate field to synchronise with the main field. */ + _updateAlternate: function(inst, newTime) { + var altField = this._get(inst, 'altField'); + if (altField) { // update alternate field too + $(altField).each(function(i,e) { + $(e).val(newTime); + }); + } + }, + + _getTimeAsDateTimepicker: function(input) { + var inst = this._getInst(input); + if (inst.hours == -1 && inst.minutes == -1) { + return ''; + } + + // default to 0 AM if hours is not valid + if ((inst.hours < inst.hours.starts) || (inst.hours > inst.hours.ends )) { inst.hours = 0; } + // default to 0 minutes if minute is not valid + if ((inst.minutes < inst.minutes.starts) || (inst.minutes > inst.minutes.ends)) { inst.minutes = 0; } + + return new Date(0, 0, 0, inst.hours, inst.minutes, 0); + }, + /* This might look unused but it's called by the $.fn.timepicker function with param getTime */ + /* added v 0.2.3 - gitHub issue #5 - Thanks edanuff */ + _getTimeTimepicker : function(input) { + var inst = this._getInst(input); + return this._getParsedTime(inst); + }, + _getHourTimepicker: function(input) { + var inst = this._getInst(input); + if ( inst == undefined) { return -1; } + return inst.hours; + }, + _getMinuteTimepicker: function(input) { + var inst= this._getInst(input); + if ( inst == undefined) { return -1; } + return inst.minutes; + } + + }); + + + + /* Invoke the timepicker functionality. + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new timepicker functionality + @return jQuery object */ + $.fn.timepicker = function (options) { + /* Initialise the time picker. */ + if (!$.timepicker.initialized) { + $(document).mousedown($.timepicker._checkExternalClick); + $.timepicker.initialized = true; + } + + /* Append timepicker main container to body if not exist. */ + if ($("#"+$.timepicker._mainDivId).length === 0) { + $('body').append($.timepicker.tpDiv); + } + + var otherArgs = Array.prototype.slice.call(arguments, 1); + if (typeof options == 'string' && (options == 'getTime' || options == 'getTimeAsDate' || options == 'getHour' || options == 'getMinute' )) + return $.timepicker['_' + options + 'Timepicker']. + apply($.timepicker, [this[0]].concat(otherArgs)); + if (options == 'option' && arguments.length == 2 && typeof arguments[1] == 'string') + return $.timepicker['_' + options + 'Timepicker']. + apply($.timepicker, [this[0]].concat(otherArgs)); + return this.each(function () { + typeof options == 'string' ? + $.timepicker['_' + options + 'Timepicker']. + apply($.timepicker, [this].concat(otherArgs)) : + $.timepicker._attachTimepicker(this, options); + }); + }; + + /* jQuery extend now ignores nulls! */ + function extendRemove(target, props) { + $.extend(target, props); + for (var name in props) + if (props[name] == null || props[name] == undefined) + target[name] = props[name]; + return target; + }; + + $.timepicker = new Timepicker(); // singleton instance + $.timepicker.initialized = false; + $.timepicker.uuid = new Date().getTime(); + $.timepicker.version = "0.3.3"; + + // Workaround for #4055 + // Add another global to avoid noConflict issues with inline event handlers + window['TP_jQuery_' + tpuuid] = $; + +})(jQuery); +/* * jQuery Iframe Transport Plugin 1.8.2 * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan * https://blueimp.net @@ -25100,10 +19465,8225 @@ } }); })); +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +// This is CodeMirror (http://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. +// +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + module.exports = mod(); + else if (typeof define == "function" && define.amd) // AMD + return define([], mod); + else // Plain browser env + this.CodeMirror = mod(); +})(function() { + "use strict"; + + // BROWSER SNIFFING + + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + + var gecko = /gecko\/\d/i.test(navigator.userAgent); + // ie_uptoN means Internet Explorer version N or lower + var ie_upto10 = /MSIE \d/.test(navigator.userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent); + var ie = ie_upto10 || ie_11up; + var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]); + var webkit = /WebKit\//.test(navigator.userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); + var chrome = /Chrome\//.test(navigator.userAgent); + var presto = /Opera\//.test(navigator.userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var khtml = /KHTML\//.test(navigator.userAgent); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); + var phantom = /PhantomJS/.test(navigator.userAgent); + + var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); + var mac = ios || /Mac/.test(navigator.platform); + var windows = /win/i.test(navigator.platform); + + var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) presto_version = Number(presto_version[1]); + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && ie_version >= 9); + + // Optimize some code when these features are not used. + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + // EDITOR CONSTRUCTOR + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. + + function CodeMirror(place, options) { + if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); + + this.options = options = options || {}; + // Determine effective options based on given values and defaults. + copyObj(defaults, options, false); + setGuttersForLineNumbers(options); + + var doc = options.value; + if (typeof doc == "string") doc = new Doc(doc, options.mode); + this.doc = doc; + + var display = this.display = new Display(place, doc); + display.wrapper.CodeMirror = this; + updateGutters(this); + themeChanged(this); + if (options.lineWrapping) + this.display.wrapper.className += " CodeMirror-wrap"; + if (options.autofocus && !mobile) focusInput(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in readInput + draggingText: false, + highlight: new Delayed() // stores highlight worker timeout + }; + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie && ie_version < 11) setTimeout(bind(resetInput, this, true), 20); + + registerEventHandlers(this); + ensureGlobalHandlers(); + + var cm = this; + runInOp(this, function() { + cm.curOp.forceUpdate = true; + attachDoc(cm, doc); + + if ((options.autofocus && !mobile) || activeElt() == display.input) + setTimeout(bind(onFocus, cm), 20); + else + onBlur(cm); + + for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) + optionHandlers[opt](cm, options[opt], Init); + maybeUpdateLineNumberWidth(cm); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](cm); + }); + } + + // DISPLAY CONSTRUCTOR + + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc) { + var d = this; + + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. + if (webkit) input.style.width = "1000px"; + else input.setAttribute("wrap", "off"); + // If border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) input.style.border = "1px solid black"; + input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); input.setAttribute("spellcheck", "false"); + + // Wraps and hides input textarea + d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The fake scrollbar elements. + d.scrollbarH = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + d.scrollbarV = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + // Covers bottom-right square when both scrollbars are present. + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + // Will contain the actual code, positioned to cover the viewport. + d.lineDiv = elt("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. + d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + // Moved around its parent to cover visible view. + d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); + // Set to the height of the document, allowing scrolling. + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerCutOff + "px; width: 1px;"); + // Will contain the gutters, if any. + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Actual scrollable element. + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.inputDiv, d.scrollbarH, d.scrollbarV, + d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + // Needed to hide big blue blinking cursor on Mobile Safari + if (ios) input.style.width = "0px"; + if (!webkit) d.scroller.draggable = true; + // Needed to handle Tab key in KHTML + if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; } + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) d.scrollbarH.style.minHeight = d.scrollbarV.style.minWidth = "18px"; + + if (place.appendChild) place.appendChild(d.wrapper); + else place(d.wrapper); + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + // Information about the rendered lines. + d.view = []; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastSizeC = 0; + d.updateLineNumbers = null; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // See readInput and resetInput + d.prevInput = ""; + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. + d.alignWidgets = false; + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. + d.pollingFast = false; + // Self-resetting timeout for the poller + d.poll = new Delayed(); + + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + + // Tracks when resetInput has punted to just putting a short + // string into the textarea instead of the full selection. + d.inaccurateSelection = false; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + // True when shift is held down. + d.shift = false; + + // Used to track whether anything happened since the context menu + // was opened. + d.selForContextMenu = null; + } + + // STATE UPDATES + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function(line) { + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + }); + cm.doc.frontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) regChange(cm); + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + addClass(cm.display.wrapper, "CodeMirror-wrap"); + cm.display.sizer.style.minWidth = ""; + } else { + rmClass(cm.display.wrapper, "CodeMirror-wrap"); + findMaxLine(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function(){updateScrollbars(cm);}, 100); + } + + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function(line) { + if (lineIsHidden(cm.doc, line)) return 0; + + var widgetsHeight = 0; + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; + } + + if (wrapping) + return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; + else + return widgetsHeight + th; + }; + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function(line) { + var estHeight = est(line); + if (estHeight != line.height) updateLineHeight(line, estHeight); + }); + } + + function keyMapChanged(cm) { + var map = keyMap[cm.options.keyMap], style = map.style; + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-keymap-\S+/g, "") + + (style ? " cm-keymap-" + style : ""); + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + function guttersChanged(cm) { + updateGutters(cm); + regChange(cm); + setTimeout(function(){alignHorizontally(cm);}, 20); + } + + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. + function updateGutters(cm) { + var gutters = cm.display.gutters, specs = cm.options.gutters; + removeChildren(gutters); + for (var i = 0; i < specs.length; ++i) { + var gutterClass = specs[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + cm.display.lineGutter = gElt; + gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = i ? "" : "none"; + updateGutterSpace(cm); + } + + function updateGutterSpace(cm) { + var width = cm.display.gutters.offsetWidth; + cm.display.sizer.style.marginLeft = width + "px"; + cm.display.scrollbarH.style.left = cm.options.fixedGutter ? width + "px" : 0; + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { + if (line.height == 0) return 0; + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(0, true); + cur = found.from.line; + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found = merged.find(0, true); + len -= cur.text.length - found.from.ch; + cur = found.to.line; + len += cur.text.length - found.to.ch; + } + return len; + } + + // Find the longest line in the document. + function findMaxLine(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(d.maxLine); + d.maxLineChanged = true; + doc.iter(function(line) { + var len = lineLength(line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // Make sure the gutters options contains the element + // "CodeMirror-linenumbers" when the lineNumbers option is true. + function setGuttersForLineNumbers(options) { + var found = indexOf(options.gutters, "CodeMirror-linenumbers"); + if (found == -1 && options.lineNumbers) { + options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); + } else if (found > -1 && !options.lineNumbers) { + options.gutters = options.gutters.slice(0); + options.gutters.splice(found, 1); + } + } + + // SCROLLBARS + + function hScrollbarTakesSpace(cm) { + return cm.display.scroller.clientHeight - cm.display.wrapper.clientHeight < scrollerCutOff - 3; + } + + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var scroll = cm.display.scroller; + return { + clientHeight: scroll.clientHeight, + barHeight: cm.display.scrollbarV.clientHeight, + scrollWidth: scroll.scrollWidth, clientWidth: scroll.clientWidth, + hScrollbarTakesSpace: hScrollbarTakesSpace(cm), + barWidth: cm.display.scrollbarH.clientWidth, + docHeight: Math.round(cm.doc.height + paddingVert(cm.display)) + }; + } + + // Re-synchronize the fake scrollbars with the actual size of the + // content. + function updateScrollbars(cm, measure) { + if (!measure) measure = measureForScrollbars(cm); + var d = cm.display, sWidth = scrollbarWidth(d.measure); + var scrollHeight = measure.docHeight + scrollerCutOff; + var needsH = measure.scrollWidth > measure.clientWidth; + if (needsH && measure.scrollWidth <= measure.clientWidth + 1 && + sWidth > 0 && !measure.hScrollbarTakesSpace) + needsH = false; // (Issue #2562) + var needsV = scrollHeight > measure.clientHeight; + + if (needsV) { + d.scrollbarV.style.display = "block"; + d.scrollbarV.style.bottom = needsH ? sWidth + "px" : "0"; + // A bug in IE8 can cause this value to be negative, so guard it. + d.scrollbarV.firstChild.style.height = + Math.max(0, scrollHeight - measure.clientHeight + (measure.barHeight || d.scrollbarV.clientHeight)) + "px"; + } else { + d.scrollbarV.style.display = ""; + d.scrollbarV.firstChild.style.height = "0"; + } + if (needsH) { + d.scrollbarH.style.display = "block"; + d.scrollbarH.style.right = needsV ? sWidth + "px" : "0"; + d.scrollbarH.firstChild.style.width = + (measure.scrollWidth - measure.clientWidth + (measure.barWidth || d.scrollbarH.clientWidth)) + "px"; + } else { + d.scrollbarH.style.display = ""; + d.scrollbarH.firstChild.style.width = "0"; + } + if (needsH && needsV) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = d.scrollbarFiller.style.width = sWidth + "px"; + } else d.scrollbarFiller.style.display = ""; + if (needsH && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = sWidth + "px"; + d.gutterFiller.style.width = d.gutters.offsetWidth + "px"; + } else d.gutterFiller.style.display = ""; + + if (!cm.state.checkedOverlayScrollbar && measure.clientHeight > 0) { + if (sWidth === 0) { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = w; + var barMouseDown = function(e) { + if (e_target(e) != d.scrollbarV && e_target(e) != d.scrollbarH) + operation(cm, onMouseDown)(e); + }; + on(d.scrollbarV, "mousedown", barMouseDown); + on(d.scrollbarH, "mousedown", barMouseDown); + } + cm.state.checkedOverlayScrollbar = true; + } + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewport may contain top, + // height, and ensure (see op.scrollToPos) properties. + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; + top = Math.floor(top - paddingTop(display)); + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; + if (ensureFrom < from) + return {from: ensureFrom, + to: lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight)}; + if (Math.min(ensureTo, doc.lastLine()) >= to) + return {from: lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight), + to: ensureTo}; + } + return {from: from, to: Math.max(to, from + 1)}; + } + + // LINE NUMBERS + + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. + function alignHorizontally(cm) { + var display = cm.display, view = display.view; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) if (!view[i].hidden) { + if (cm.options.fixedGutter && view[i].gutter) + view[i].gutter.style.left = left; + var align = view[i].alignable; + if (align) for (var j = 0; j < align.length; j++) + align[j].style.left = left; + } + if (cm.options.fixedGutter) + display.gutters.style.left = (comp + gutterW) + "px"; + } + + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) return false; + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding); + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + updateGutterSpace(cm); + return true; + } + return false; + } + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)); + } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. + function compensateForHScroll(display) { + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left; + } + + // DISPLAY DRAWING + + function DisplayUpdate(cm, viewport, force) { + var display = cm.display; + + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.oldViewFrom = display.viewFrom; this.oldViewTo = display.viewTo; + this.oldScrollerWidth = display.scroller.clientWidth; + this.force = force; + this.dims = getDimensions(cm); + } + + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayIfNeeded(cm, update) { + var display = cm.display, doc = cm.doc; + if (update.editorIsHidden) { + resetView(cm); + return false; + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && + countDirtyView(cm) == 0) + return false; + + if (maybeUpdateLineNumberWidth(cm)) { + resetView(cm); + update.dims = getDimensions(cm); + } + + // Compute a suitable new viewport (from & to) + var end = doc.first + doc.size; + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); + if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); + if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); + if (sawCollapsedSpans) { + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || + display.lastSizeC != update.wrapperHeight; + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !update.force && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + return false; + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var focused = activeElt(); + if (toUpdate > 4) display.lineDiv.style.display = "none"; + patchDisplay(cm, display.updateLineNumbers, update.dims); + if (toUpdate > 4) display.lineDiv.style.display = ""; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + if (focused && activeElt() != focused && focused.offsetHeight) focused.focus(); + + // Prevent selection and cursors from interfering with the scroll + // width. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + + if (different) { + display.lastSizeC = update.wrapperHeight; + startWorker(cm, 400); + } + + display.updateLineNumbers = null; + + return true; + } + + function postUpdateDisplay(cm, update) { + var force = update.force, viewport = update.viewport; + for (var first = true;; first = false) { + if (first && cm.options.lineWrapping && update.oldScrollerWidth != cm.display.scroller.clientWidth) { + force = true; + } else { + force = false; + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - scrollerCutOff - + cm.display.scroller.clientHeight, viewport.top)}; + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + update.visible = visibleLines(cm.display, cm.doc, viewport); + if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) + break; + } + if (!updateDisplayIfNeeded(cm, update)) break; + updateHeightsInViewport(cm); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + } + + signalLater(cm, "update", cm); + if (cm.display.viewFrom != update.oldViewFrom || cm.display.viewTo != update.oldViewTo) + signalLater(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + updateHeightsInViewport(cm); + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + } + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = cm.display.heightForcer.style.top = measure.docHeight + "px"; + cm.display.gutters.style.height = Math.max(measure.docHeight, measure.clientHeight - scrollerCutOff) + "px"; + } + + function checkForWebkitWidthBug(cm, measure) { + // Work around Webkit bug where it sometimes reserves space for a + // non-existing phantom scrollbar in the scroller (Issue #2420) + if (cm.display.sizer.offsetWidth + cm.display.gutters.offsetWidth < cm.display.scroller.clientWidth - 1) { + cm.display.sizer.style.minHeight = cm.display.heightForcer.style.top = "0px"; + cm.display.gutters.style.height = measure.docHeight + "px"; + } + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], height; + if (cur.hidden) continue; + if (ie && ie_version < 8) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = cur.node.getBoundingClientRect(); + height = box.bottom - box.top; + } + var diff = cur.line.height - height; + if (height < 2) height = textHeight(display); + if (diff > .001 || diff < -.001) { + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) for (var j = 0; j < cur.rest.length; j++) + updateWidgetHeight(cur.rest[j]); + } + } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) + line.widgets[i].height = line.widgets[i].node.offsetHeight; + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + left[cm.options.gutters[i]] = n.offsetLeft; + width[cm.options.gutters[i]] = n.offsetWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth}; + } + + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { + var display = cm.display, lineNumbers = cm.options.lineNumbers; + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) + node.style.display = "none"; + else + node.parentNode.removeChild(node); + return next; + } + + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) { + } else if (!lineView.node) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) cur = rm(cur); + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false; + updateLineForChanges(cm, lineView, lineN, dims); + } + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); + } + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } + while (cur) cur = rm(cur); + } + + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") updateLineText(cm, lineView); + else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); + else if (type == "class") updateLineClasses(lineView); + else if (type == "widget") updateLineWidgets(lineView, dims); + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + lineView.text.parentNode.replaceChild(lineView.node, lineView.text); + lineView.node.appendChild(lineView.text); + if (ie && ie_version < 8) lineView.node.style.zIndex = 2; + } + return lineView.node; + } + + function updateLineBackground(lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) cls += " CodeMirror-linebackground"; + if (lineView.background) { + if (cls) lineView.background.className = cls; + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built; + } + return buildLineContent(cm, lineView); + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) lineView.node = built.pre; + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(lineView) { + updateLineBackground(lineView); + if (lineView.line.wrapClass) + ensureLineWrapped(lineView).className = lineView.line.wrapClass; + else if (lineView.node != lineView.text) + lineView.node.className = ""; + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + var markers = lineView.line.gutterMarkers; + if (cm.options.lineNumbers || markers) { + var wrap = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = + wrap.insertBefore(elt("div", null, "CodeMirror-gutter-wrapper", "position: absolute; left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"), + lineView.text); + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), + "CodeMirror-linenumber CodeMirror-gutter-elt", + "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " + + cm.display.lineNumInnerWidth + "px")); + if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); + } + } + } + + function updateLineWidgets(lineView, dims) { + if (lineView.alignable) lineView.alignable = null; + for (var node = lineView.node.firstChild, next; node; node = next) { + var next = node.nextSibling; + if (node.className == "CodeMirror-linewidget") + lineView.node.removeChild(node); + } + insertLineWidgets(lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) lineView.bgClass = built.bgClass; + if (built.textClass) lineView.textClass = built.textClass; + + updateLineClasses(lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(lineView, dims); + return lineView.node; + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(lineView, dims) { + insertLineWidgetsFor(lineView.line, lineView, dims, true); + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + insertLineWidgetsFor(lineView.rest[i], lineView, dims, false); + } + + function insertLineWidgetsFor(line, lineView, dims, allowAbove) { + if (!line.widgets) return; + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); + if (!widget.handleMouseEvents) node.ignoreEvents = true; + positionLineWidget(widget, node, lineView, dims); + if (allowAbove && widget.above) + wrap.insertBefore(node, lineView.gutter || lineView.text); + else + wrap.appendChild(node); + signalLater(widget, "redraw"); + } + } + + function positionLineWidget(widget, node, lineView, dims) { + if (widget.noHScroll) { + (lineView.alignable || (lineView.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; + } + } + + // POSITION OBJECT + + // A Pos instance represents a position within the text. + var Pos = CodeMirror.Pos = function(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + }; + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; }; + + function copyPos(x) {return Pos(x.line, x.ch);} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } + + // SELECTION / CURSOR + + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + function Selection(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + } + + Selection.prototype = { + primary: function() { return this.ranges[this.primIndex]; }, + equals: function(other) { + if (other == this) return true; + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false; + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false; + } + return true; + }, + deepCopy: function() { + for (var out = [], i = 0; i < this.ranges.length; i++) + out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); + return new Selection(out, this.primIndex); + }, + somethingSelected: function() { + for (var i = 0; i < this.ranges.length; i++) + if (!this.ranges[i].empty()) return true; + return false; + }, + contains: function(pos, end) { + if (!end) end = pos; + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + return i; + } + return -1; + } + }; + + function Range(anchor, head) { + this.anchor = anchor; this.head = head; + } + + Range.prototype = { + from: function() { return minPos(this.anchor, this.head); }, + to: function() { return maxPos(this.anchor, this.head); }, + empty: function() { + return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch; + } + }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(ranges, primIndex) { + var prim = ranges[primIndex]; + ranges.sort(function(a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + if (cmp(prev.to(), cur.from()) >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) --primIndex; + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex); + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0); + } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} + function clipPos(doc, pos) { + if (pos.line < doc.first) return Pos(doc.first, 0); + var last = doc.first + doc.size - 1; + if (pos.line > last) return Pos(last, getLine(doc, last).text.length); + return clipToLen(pos, getLine(doc, pos.line).text.length); + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) return Pos(pos.line, linelen); + else if (ch < 0) return Pos(pos.line, 0); + else return pos; + } + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} + function clipPosArray(doc, array) { + for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]); + return out; + } + + // SELECTION UPDATES + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(doc, range, head, other) { + if (doc.cm && doc.cm.display.shift || doc.extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head); + } else { + return new Range(other || head, head); + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options) { + setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + for (var out = [], i = 0; i < doc.sel.ranges.length; i++) + out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null); + var newSel = normalizeSelection(out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); + } + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1); + else return sel; + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + sel = filterSelectionChange(doc, sel); + + var bias = options && options.bias || + (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + ensureCursorVisible(doc.cm); + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) return; + + doc.sel = sel; + + if (doc.cm) { + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true; + signalCursorActivity(doc.cm); + } + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) out = sel.ranges.slice(0, i); + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(out, sel.primIndex) : sel; + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, bias, mayClear) { + var flipped = false, curPos = pos; + var dir = bias || 1; + doc.cantEdit = false; + search: for (;;) { + var line = getLine(doc, curPos.line); + if (line.markedSpans) { + for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } + } + if (!m.atomic) continue; + var newPos = m.find(dir < 0 ? -1 : 1); + if (cmp(newPos, curPos) == 0) { + newPos.ch += dir; + if (newPos.ch < 0) { + if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); + else newPos = null; + } else if (newPos.ch > line.text.length) { + if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); + else newPos = null; + } + if (!newPos) { + if (flipped) { + // Driven in a corner -- no valid cursor position found at all + // -- try again *with* clearing, if we didn't already + if (!mayClear) return skipAtomic(doc, pos, bias, true); + // Otherwise, turn off editing until further notice, and return the start of the doc + doc.cantEdit = true; + return Pos(doc.first, 0); + } + flipped = true; newPos = pos; dir = -dir; + } + } + curPos = newPos; + continue search; + } + } + } + return curPos; + } + } + + // SELECTION DRAWING + + // Redraw the selection and/or cursor + function drawSelection(cm) { + var display = cm.display, doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + drawSelectionCursor(cm, range, curFragment); + if (!collapsed) + drawSelectionRange(cm, range, selFragment); + } + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + } + + return result; + } + + function showSelection(cm, drawn) { + removeChildrenAndAdd(cm.display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(cm.display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + cm.display.inputDiv.style.top = drawn.teTop + "px"; + cm.display.inputDiv.style.left = drawn.teLeft + "px"; + } + } + + function updateSelection(cm) { + showSelection(cm, drawSelection(cm)); + } + + // Draws a cursor for the given range + function drawSelectionCursor(cm, range, output) { + var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + // Draws the given range as a highlighted selection + function drawSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; + var fragment = document.createDocumentFragment(); + var padding = paddingH(cm.display), leftSide = padding.left, rightSide = display.lineSpace.offsetWidth - padding.right; + + function add(left, top, width, bottom) { + if (top < 0) top = 0; + top = Math.round(top); + bottom = Math.round(bottom); + fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + + "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) + + "px; height: " + (bottom - top) + "px")); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias); + } + + iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { + var leftPos = coords(from, "left"), rightPos, left, right; + if (from == to) { + rightPos = leftPos; + left = right = leftPos.left; + } else { + rightPos = coords(to - 1, "right"); + if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } + left = leftPos.left; + right = rightPos.right; + } + if (fromArg == null && from == 0) left = leftSide; + if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part + add(left, leftPos.top, null, leftPos.bottom); + left = leftSide; + if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); + } + if (toArg == null && to == lineLen) right = rightSide; + if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) + start = leftPos; + if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) + end = rightPos; + if (left < leftSide + 1) left = leftSide; + add(left, rightPos.top, right - left, rightPos.bottom); + }); + return {start: start, end: end}; + } + + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); + } else { + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + add(leftSide, leftEnd.bottom, null, rightStart.top); + } + + output.appendChild(fragment); + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) return; + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursorDiv.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + display.blinker = setInterval(function() { + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); + else if (cm.options.cursorBlinkRate < 0) + display.cursorDiv.style.visibility = "hidden"; + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) + cm.state.highlight.set(time, bind(highlightWorker, cm)); + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.frontier < doc.first) doc.frontier = doc.first; + if (doc.frontier >= cm.display.viewTo) return; + var end = +new Date + cm.options.workTime; + var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); + var changedLines = []; + + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { + if (doc.frontier >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles; + var highlighted = highlightLine(cm, line, state, true); + line.styles = highlighted.styles; + var oldCls = line.styleClasses, newCls = highlighted.classes; + if (newCls) line.styleClasses = newCls; + else if (oldCls) line.styleClasses = null; + var ischange = !oldStyles || oldStyles.length != line.styles.length || + oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); + for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; + if (ischange) changedLines.push(doc.frontier); + line.stateAfter = copyState(doc.mode, state); + } else { + processLine(cm, line.text, state); + line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; + } + ++doc.frontier; + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true; + } + }); + if (changedLines.length) runInOp(cm, function() { + for (var i = 0; i < changedLines.length; i++) + regLineChange(cm, changedLines[i], "text"); + }); + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) return doc.first; + var line = getLine(doc, search - 1); + if (line.stateAfter && (!precise || search <= doc.frontier)) return search; + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + + function getStateBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) return true; + var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; + if (!state) state = startState(doc.mode); + else state = copyState(doc.mode, state); + doc.iter(pos, n, function(line) { + processLine(cm, line.text, state); + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo; + line.stateAfter = save ? copyState(doc.mode, state) : null; + ++pos; + }); + if (precise) doc.frontier = pos; + return state; + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop;} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} + function paddingH(display) { + if (display.cachedPaddingH) return display.cachedPaddingH; + var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; + if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data; + return data; + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && cm.display.scroller.clientWidth; + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + heights.push((cur.bottom + next.top) / 2 - rect.top); + } + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + return {map: lineView.measure.map, cache: lineView.measure.cache}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineView.rest[i] == line) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineNo(lineView.rest[i]) > lineN) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}; + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view; + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias); + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + return cm.display.view[findViewIndex(cm, lineN)]; + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + return ext; + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) + view = null; + else if (view && view.changes) + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + if (!view) + view = updateExternalMeasurement(cm, line); + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + }; + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias, varHeight) { + if (prepared.before) ch = -1; + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + prepared.rect = prepared.view.text.getBoundingClientRect(); + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) prepared.cache[key] = found; + } + return {left: found.left, right: found.right, + top: varHeight ? found.rtop : found.top, + bottom: varHeight ? found.rbottom : found.bottom}; + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function measureCharInner(cm, prepared, ch, bias) { + var map = prepared.map; + + var node, start, end, collapse; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + var mStart = map[i], mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) collapse = "right"; + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + collapse = bias; + if (bias == "left" && start == 0) + while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } + if (bias == "right" && start == mEnd - mStart) + while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } + break; + } + } + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned + while (start && isExtendingChar(prepared.line.text.charAt(mStart + start))) --start; + while (mStart + end < mEnd && isExtendingChar(prepared.line.text.charAt(mStart + end))) ++end; + if (ie && ie_version < 9 && start == 0 && end == mEnd - mStart) { + rect = node.parentNode.getBoundingClientRect(); + } else if (ie && cm.options.lineWrapping) { + var rects = range(node, start, end).getClientRects(); + if (rects.length) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = nullRect; + } else { + rect = range(node, start, end).getBoundingClientRect() || nullRect; + } + if (rect.left || rect.right || start == 0) break; + end = start; + start = start - 1; + collapse = "right"; + } + if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect); + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) collapse = bias = "right"; + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = node.getBoundingClientRect(); + } + if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; + else + rect = nullRect; + } + + var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; + var mid = (rtop + rbot) / 2; + var heights = prepared.view.measure.heights; + for (var i = 0; i < heights.length - 1; i++) + if (mid < heights[i]) break; + var top = i ? heights[i - 1] : 0, bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) result.bogus = true; + if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } + + return result; + } + + // Work around problem with bounding client rects on ranges being + // returned incorrectly when zoomed on IE10 and below. + function maybeUpdateRectForZooming(measure, rect) { + if (!window.screen || screen.logicalXDPI == null || + screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) + return rect; + var scaleX = screen.logicalXDPI / screen.deviceXDPI; + var scaleY = screen.logicalYDPI / screen.deviceYDPI; + return {left: rect.left * scaleX, right: rect.right * scaleX, + top: rect.top * scaleY, bottom: rect.bottom * scaleY}; + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + lineView.measure.caches[i] = {}; + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + clearLineMeasurementCacheFor(cm.display.view[i]); + } + + function clearCaches(cm) { + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; + if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; + cm.display.lineNumChars = null; + } + + function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } + function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } + + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"/null (editor), or "page". + function intoCoordSystem(cm, lineObj, rect, context) { + if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { + var size = widgetHeight(lineObj.widgets[i]); + rect.top += size; rect.bottom += size; + } + if (context == "line") return rect; + if (!context) context = "local"; + var yOff = heightAtLine(lineObj); + if (context == "local") yOff += paddingTop(cm.display); + else yOff -= cm.display.viewOffset; + if (context == "page" || context == "window") { + var lOff = cm.display.lineSpace.getBoundingClientRect(); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect; + } + + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"/null. + function fromCoordSystem(cm, coords, context) { + if (context == "div") return coords; + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = cm.display.sizer.getBoundingClientRect(); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) lineObj = getLine(cm.doc, pos.line); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context); + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj); + function get(ch, right) { + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); + if (right) m.left = m.right; else m.right = m.left; + return intoCoordSystem(cm, lineObj, m, context); + } + function getBidi(ch, partPos) { + var part = order[partPos], right = part.level % 2; + if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { + part = order[--partPos]; + ch = bidiRight(part) - (part.level % 2 ? 0 : 1); + right = true; + } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { + part = order[++partPos]; + ch = bidiLeft(part) - part.level % 2; + right = false; + } + if (right && ch == part.to && ch > part.from) return get(ch - 1); + return get(ch, right); + } + var order = getOrder(lineObj), ch = pos.ch; + if (!order) return get(ch); + var partPos = getBidiPartAt(order, ch); + var val = getBidi(ch, partPos); + if (bidiOther != null) val.other = getBidi(ch, bidiOther); + return val; + } + + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0, pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch; + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height}; + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. + function PosWithInfo(line, ch, outside, xRel) { + var pos = Pos(line, ch); + pos.xRel = xRel; + if (outside) pos.outside = true; + return pos; + } + + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) return PosWithInfo(doc.first, 0, true, -1); + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) + return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); + if (x < 0) x = 0; + + var lineObj = getLine(doc, lineN); + for (;;) { + var found = coordsCharInner(cm, lineObj, lineN, x, y); + var merged = collapsedSpanAtEnd(lineObj); + var mergedPos = merged && merged.find(0, true); + if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) + lineN = lineNo(lineObj = mergedPos.to.line); + else + return found; + } + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + var innerOff = y - heightAtLine(lineObj); + var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; + var preparedMeasure = prepareMeasureForLine(cm, lineObj); + + function getX(ch) { + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure); + wrongLine = true; + if (innerOff > sp.bottom) return sp.left - adjust; + else if (innerOff < sp.top) return sp.left + adjust; + else wrongLine = false; + return sp.left; + } + + var bidi = getOrder(lineObj), dist = lineObj.text.length; + var from = lineLeft(lineObj), to = lineRight(lineObj); + var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; + + if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); + // Do a binary search between these bounds. + for (;;) { + if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { + var ch = x < fromX || x - fromX <= toX - x ? from : to; + var xDiff = x - (ch == from ? fromX : toX); + while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; + var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, + xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); + return pos; + } + var step = Math.ceil(dist / 2), middle = from + step; + if (bidi) { + middle = from; + for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); + } + var middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} + else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} + } + } + + var measureText; + // Compute the default text height. + function textHeight(display) { + if (display.cachedTextHeight != null) return display.cachedTextHeight; + if (measureText == null) { + measureText = elt("pre"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) display.cachedTextHeight = height; + removeChildren(display.measure); + return height || 1; + } + + // Compute the default character width. + function charWidth(display) { + if (display.cachedCharWidth != null) return display.cachedCharWidth; + var anchor = elt("span", "xxxxxxxxxx"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(display.measure, pre); + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; + if (width > 2) display.cachedCharWidth = width; + return width || 10; + } + + // OPERATIONS + + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. + + var operationGroup = null; + + var nextOpId = 0; + // Start a new operation. + function startOperation(cm) { + cm.curOp = { + cm: cm, + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: null, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on + cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + id: ++nextOpId // Unique ID + }; + if (operationGroup) { + operationGroup.ops.push(cm.curOp); + } else { + cm.curOp.ownsGroup = operationGroup = { + ops: [cm.curOp], + delayedCallbacks: [] + }; + } + } + + function fireCallbacksForOps(group) { + // Calls delayed callbacks and cursorActivity handlers until no + // new ones appear + var callbacks = group.delayedCallbacks, i = 0; + do { + for (; i < callbacks.length; i++) + callbacks[i](); + for (var j = 0; j < group.ops.length; j++) { + var op = group.ops[j]; + if (op.cursorActivityHandlers) + while (op.cursorActivityCalled < op.cursorActivityHandlers.length) + op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm); + } + } while (i < callbacks.length); + } + + // Finish an operation, updating the display and signalling delayed events + function endOperation(cm) { + var op = cm.curOp, group = op.ownsGroup; + if (!group) return; + + try { fireCallbacksForOps(group); } + finally { + operationGroup = null; + for (var i = 0; i < group.ops.length; i++) + group.ops[i].cm.curOp = null; + endOperations(group); + } + } + + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R1(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W1(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R2(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W2(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_finish(ops[i]); + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; + if (op.updateMaxLine) findMaxLine(cm); + + op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping; + op.update = op.mustUpdate && + new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); + } + + function endOperation_W1(op) { + op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); + } + + function endOperation_R2(op) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) updateHeightsInViewport(cm); + + op.barMeasure = measureForScrollbars(cm); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplay_W2 will use these properties to do the actual resizing + if (display.maxLineChanged && !cm.options.lineWrapping) { + op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo + + scrollerCutOff - display.scroller.clientWidth); + } + + if (op.updatedDisplay || op.selectionChanged) + op.newSelectionNodes = drawSelection(cm); + } + + function endOperation_W2(op) { + var cm = op.cm; + + if (op.adjustWidthTo != null) { + cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; + if (op.maxScrollLeft < cm.doc.scrollLeft) + setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); + cm.display.maxLineChanged = false; + } + + if (op.newSelectionNodes) + showSelection(cm, op.newSelectionNodes); + if (op.updatedDisplay) + setDocumentHeight(cm, op.barMeasure); + if (op.updatedDisplay || op.startHeight != cm.doc.height) + updateScrollbars(cm, op.barMeasure); + + if (op.selectionChanged) restartBlink(cm); + + if (cm.state.focused && op.updateInput) + resetInput(cm, op.typing); + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; + + if (op.adjustWidthTo != null && Math.abs(op.barMeasure.scrollWidth - cm.display.scroller.scrollWidth) > 1) + updateScrollbars(cm); + + if (op.updatedDisplay) postUpdateDisplay(cm, op.update); + + // Abort mouse wheel delta measurement, when scrolling explicitly + if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) + display.wheelStartX = display.wheelStartY = null; + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) { + var top = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); + display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = top; + } + if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { + var left = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, op.scrollLeft)); + display.scroller.scrollLeft = display.scrollbarH.scrollLeft = doc.scrollLeft = left; + alignHorizontally(cm); + } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); + if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); + } + + // Fire events for markers that are hidden/unidden by editing or + // undoing + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) for (var i = 0; i < hidden.length; ++i) + if (!hidden[i].lines.length) signal(hidden[i], "hide"); + if (unhidden) for (var i = 0; i < unhidden.length; ++i) + if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); + + if (display.wrapper.offsetHeight) + doc.scrollTop = cm.display.scroller.scrollTop; + + // Apply workaround for two webkit bugs + if (op.updatedDisplay && webkit) { + if (cm.options.lineWrapping) + checkForWebkitWidthBug(cm, op.barMeasure); // (Issue #2420) + if (op.barMeasure.scrollWidth > op.barMeasure.clientWidth && + op.barMeasure.scrollWidth < op.barMeasure.clientWidth + 1 && + !hScrollbarTakesSpace(cm)) + updateScrollbars(cm); // (Issue #2562) + } + + // Fire change events, and delayed event handlers + if (op.changeObjs) + signal(cm, "changes", cm, op.changeObjs); + } + + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) return f(); + startOperation(cm); + try { return f(); } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) return f.apply(cm, arguments); + startOperation(cm); + try { return f.apply(cm, arguments); } + finally { endOperation(cm); } + }; + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) return f.apply(this, arguments); + startOperation(this); + try { return f.apply(this, arguments); } + finally { endOperation(this); } + }; + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) return f.apply(this, arguments); + startOperation(cm); + try { return f.apply(this, arguments); } + finally { endOperation(cm); } + }; + } + + // VIEW TRACKING + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array; + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. + function regChange(cm, from, to, lendiff) { + if (from == null) from = cm.doc.first; + if (to == null) to = cm.doc.first + cm.doc.size; + if (!lendiff) lendiff = 0; + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + display.updateLineNumbers = from; + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + resetView(cm); + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut = viewCuttingPoint(cm, from, from, -1); + if (cut) { + display.view = display.view.slice(0, cut.index); + display.viewTo = cut.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + ext.lineN += lendiff; + else if (from < ext.lineN + ext.size) + display.externalMeasured = null; + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + display.externalMeasured = null; + + if (line < display.viewFrom || line >= display.viewTo) return; + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) return; + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) arr.push(type); + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) return null; + n -= cm.display.viewFrom; + if (n < 0) return null; + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) return i; + } + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) + return {index: index, lineN: newN}; + for (var i = 0, n = cm.display.viewFrom; i < index; i++) + n += view[i].size; + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) return null; + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) return null; + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN}; + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); + else if (display.viewFrom < from) + display.view = display.view.slice(findViewIndex(cm, from)); + display.viewFrom = from; + if (display.viewTo < to) + display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); + else if (display.viewTo > to) + display.view = display.view.slice(0, findViewIndex(cm, to)); + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty; + } + return dirty; + } + + // INPUT HANDLING + + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. + function slowPoll(cm) { + if (cm.display.pollingFast) return; + cm.display.poll.set(cm.options.pollInterval, function() { + readInput(cm); + if (cm.state.focused) slowPoll(cm); + }); + } + + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. + function fastPoll(cm) { + var missed = false; + cm.display.pollingFast = true; + function p() { + var changed = readInput(cm); + if (!changed && !missed) {missed = true; cm.display.poll.set(60, p);} + else {cm.display.pollingFast = false; slowPoll(cm);} + } + cm.display.poll.set(20, p); + } + + // This will be set to an array of strings when copying, so that, + // when pasting, we know what kind of selections the copied text + // was made out of. + var lastCopied = null; + + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). + function readInput(cm) { + var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. + if (!cm.state.focused || (hasSelection(input) && !prevInput) || isReadOnly(cm) || cm.options.disableInput) + return false; + // See paste handler for more on the fakedLastChar kludge + if (cm.state.pasteIncoming && cm.state.fakedLastChar) { + input.value = input.value.substring(0, input.value.length - 1); + cm.state.fakedLastChar = false; + } + var text = input.value; + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) return false; + // Work around nonsensical selection resetting in IE9/10, and + // inexplicable appearance of private area unicode characters on + // some key combos in Mac (#2689). + if (ie && ie_version >= 9 && cm.display.inputHasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + resetInput(cm); + return false; + } + + var withOp = !cm.curOp; + if (withOp) startOperation(cm); + cm.display.shift = false; + + if (text.charCodeAt(0) == 0x200b && doc.sel == cm.display.selForContextMenu && !prevInput) + prevInput = "\u200b"; + // Find the part of the input that is actually new + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; + var inserted = text.slice(same), textLines = splitLines(inserted); + + // When pasing N lines into N selections, insert one line per selection + var multiPaste = null; + if (cm.state.pasteIncoming && doc.sel.ranges.length > 1) { + if (lastCopied && lastCopied.join("\n") == inserted) + multiPaste = doc.sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines); + else if (textLines.length == doc.sel.ranges.length) + multiPaste = map(textLines, function(l) { return [l]; }); + } + + // Normal behavior is to insert the new text into every selection + for (var i = doc.sel.ranges.length - 1; i >= 0; i--) { + var range = doc.sel.ranges[i]; + var from = range.from(), to = range.to(); + // Handle deletion + if (same < prevInput.length) + from = Pos(from.line, from.ch - (prevInput.length - same)); + // Handle overwrite + else if (cm.state.overwrite && range.empty() && !cm.state.pasteIncoming) + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); + var updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines, + origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + // When an 'electric' character is inserted, immediately trigger a reindent + if (inserted && !cm.state.pasteIncoming && cm.options.electricChars && + cm.options.smartIndent && range.head.ch < 100 && + (!i || doc.sel.ranges[i - 1].head.line != range.head.line)) { + var mode = cm.getModeAt(range.head); + var end = changeEnd(changeEvent); + if (mode.electricChars) { + for (var j = 0; j < mode.electricChars.length; j++) + if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { + indentLine(cm, end.line, "smart"); + break; + } + } else if (mode.electricInput) { + if (mode.electricInput.test(getLine(doc, end.line).text.slice(0, end.ch))) + indentLine(cm, end.line, "smart"); + } + } + } + ensureCursorVisible(cm); + cm.curOp.updateInput = updateInput; + cm.curOp.typing = true; + + // Don't leave long text in the textarea, since it makes further polling slow + if (text.length > 1000 || text.indexOf("\n") > -1) input.value = cm.display.prevInput = ""; + else cm.display.prevInput = text; + if (withOp) endOperation(cm); + cm.state.pasteIncoming = cm.state.cutIncoming = false; + return true; + } + + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + function resetInput(cm, typing) { + var minimal, selected, doc = cm.doc; + if (cm.somethingSelected()) { + cm.display.prevInput = ""; + var range = doc.sel.primary(); + minimal = hasCopyEvent && + (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); + var content = minimal ? "-" : selected || cm.getSelection(); + cm.display.input.value = content; + if (cm.state.focused) selectInput(cm.display.input); + if (ie && ie_version >= 9) cm.display.inputHasSelection = content; + } else if (!typing) { + cm.display.prevInput = cm.display.input.value = ""; + if (ie && ie_version >= 9) cm.display.inputHasSelection = null; + } + cm.display.inaccurateSelection = minimal; + } + + function focusInput(cm) { + if (cm.options.readOnly != "nocursor" && (!mobile || activeElt() != cm.display.input)) + cm.display.input.focus(); + } + + function ensureFocus(cm) { + if (!cm.state.focused) { focusInput(cm); onFocus(cm); } + } + + function isReadOnly(cm) { + return cm.options.readOnly || cm.doc.cantEdit; + } + + // EVENT HANDLERS + + // Attach the necessary event handlers when initializing the editor + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + // Older IE's will not fire a second mousedown for a double click + if (ie && ie_version < 11) + on(d.scroller, "dblclick", operation(cm, function(e) { + if (signalDOMEvent(cm, e)) return; + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; + e_preventDefault(e); + var word = cm.findWordAt(pos); + extendSelection(cm.doc, word.anchor, word.head); + })); + else + on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); + // Prevent normal selection in the editor (we handle our own) + on(d.lineSpace, "selectstart", function(e) { + if (!eventInWidget(d, e)) e_preventDefault(e); + }); + // Some browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for these browsers. + if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. + on(d.scroller, "scroll", function() { + if (d.scroller.clientHeight) { + setScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + on(d.scrollbarV, "scroll", function() { + if (d.scroller.clientHeight) setScrollTop(cm, d.scrollbarV.scrollTop); + }); + on(d.scrollbarH, "scroll", function() { + if (d.scroller.clientHeight) setScrollLeft(cm, d.scrollbarH.scrollLeft); + }); + + // Listen to wheel events in order to try and update the viewport on time. + on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); + on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + + // Prevent clicks in the scrollbars from killing focus + function reFocus() { if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); } + on(d.scrollbarH, "mousedown", reFocus); + on(d.scrollbarV, "mousedown", reFocus); + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + on(d.input, "keyup", function(e) { onKeyUp.call(cm, e); }); + on(d.input, "input", function() { + if (ie && ie_version >= 9 && cm.display.inputHasSelection) cm.display.inputHasSelection = null; + fastPoll(cm); + }); + on(d.input, "keydown", operation(cm, onKeyDown)); + on(d.input, "keypress", operation(cm, onKeyPress)); + on(d.input, "focus", bind(onFocus, cm)); + on(d.input, "blur", bind(onBlur, cm)); + + function drag_(e) { + if (!signalDOMEvent(cm, e)) e_stop(e); + } + if (cm.options.dragDrop) { + on(d.scroller, "dragstart", function(e){onDragStart(cm, e);}); + on(d.scroller, "dragenter", drag_); + on(d.scroller, "dragover", drag_); + on(d.scroller, "drop", operation(cm, onDrop)); + } + on(d.scroller, "paste", function(e) { + if (eventInWidget(d, e)) return; + cm.state.pasteIncoming = true; + focusInput(cm); + fastPoll(cm); + }); + on(d.input, "paste", function() { + // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 + // Add a char to the end of textarea before paste occur so that + // selection doesn't span to the end of textarea. + if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) { + var start = d.input.selectionStart, end = d.input.selectionEnd; + d.input.value += "$"; + // The selection end needs to be set before the start, otherwise there + // can be an intermediate non-empty selection between the two, which + // can override the middle-click paste buffer on linux and cause the + // wrong thing to get pasted. + d.input.selectionEnd = end; + d.input.selectionStart = start; + cm.state.fakedLastChar = true; + } + cm.state.pasteIncoming = true; + fastPoll(cm); + }); + + function prepareCopyCut(e) { + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (d.inaccurateSelection) { + d.prevInput = ""; + d.inaccurateSelection = false; + d.input.value = lastCopied.join("\n"); + selectInput(d.input); + } + } else { + var text = [], ranges = []; + for (var i = 0; i < cm.doc.sel.ranges.length; i++) { + var line = cm.doc.sel.ranges[i].head.line; + var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; + ranges.push(lineRange); + text.push(cm.getRange(lineRange.anchor, lineRange.head)); + } + if (e.type == "cut") { + cm.setSelections(ranges, null, sel_dontScroll); + } else { + d.prevInput = ""; + d.input.value = text.join("\n"); + selectInput(d.input); + } + lastCopied = text; + } + if (e.type == "cut") cm.state.cutIncoming = true; + } + on(d.input, "cut", prepareCopyCut); + on(d.input, "copy", prepareCopyCut); + + // Needed to handle Tab key in KHTML + if (khtml) on(d.sizer, "mouseup", function() { + if (activeElt() == d.input) d.input.blur(); + focusInput(cm); + }); + } + + // Called when the window resizes + function onResize(cm) { + // Might be a text scaling operation, clear size caches. + var d = cm.display; + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + cm.setSize(); + } + + // MOUSE EVENTS + + // Return true when the given mouse event happened in a widget + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || n.ignoreEvents || n.parentNode == display.sizer && n != display.mover) return true; + } + } + + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { + var display = cm.display; + if (!liberal) { + var target = e_target(e); + if (target == display.scrollbarH || target == display.scrollbarV || + target == display.scrollbarFiller || target == display.gutterFiller) return null; + } + var x, y, space = display.lineSpace.getBoundingClientRect(); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e) { return null; } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); + } + return coords; + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. + function onMouseDown(e) { + if (signalDOMEvent(this, e)) return; + var cm = this, display = cm.display; + display.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. + display.scroller.draggable = false; + setTimeout(function(){display.scroller.draggable = true;}, 100); + } + return; + } + if (clickInGutter(cm, e)) return; + var start = posFromMouse(cm, e); + window.focus(); + + switch (e_button(e)) { + case 1: + if (start) + leftButtonDown(cm, e, start); + else if (e_target(e) == display.scroller) + e_preventDefault(e); + break; + case 2: + if (webkit) cm.state.lastMiddleDown = +new Date; + if (start) extendSelection(cm.doc, start); + setTimeout(bind(focusInput, cm), 20); + e_preventDefault(e); + break; + case 3: + if (captureRightClick) onContextMenu(cm, e); + break; + } + } + + var lastClick, lastDoubleClick; + function leftButtonDown(cm, e, start) { + setTimeout(bind(ensureFocus, cm), 0); + + var now = +new Date, type; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { + type = "triple"; + } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { + type = "double"; + lastDoubleClick = {time: now, pos: start}; + } else { + type = "single"; + lastClick = {time: now, pos: start}; + } + + var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey; + if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && + type == "single" && sel.contains(start) > -1 && sel.somethingSelected()) + leftButtonStartDrag(cm, e, start, modifier); + else + leftButtonSelect(cm, e, start, type, modifier); + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, e, start, modifier) { + var display = cm.display; + var dragEnd = operation(cm, function(e2) { + if (webkit) display.scroller.draggable = false; + cm.state.draggingText = false; + off(document, "mouseup", dragEnd); + off(display.scroller, "drop", dragEnd); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + if (!modifier) + extendSelection(cm.doc, start); + focusInput(cm); + // Work around unexplainable focus problem in IE9 (#2127) + if (ie && ie_version == 9) + setTimeout(function() {document.body.focus(); focusInput(cm);}, 20); + } + }); + // Let the drag handler handle this. + if (webkit) display.scroller.draggable = true; + cm.state.draggingText = dragEnd; + // IE's approach to draggable + if (display.scroller.dragDrop) display.scroller.dragDrop(); + on(document, "mouseup", dragEnd); + on(display.scroller, "drop", dragEnd); + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, e, start, type, addNew) { + var display = cm.display, doc = cm.doc; + e_preventDefault(e); + + var ourRange, ourIndex, startSel = doc.sel; + if (addNew && !e.shiftKey) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + ourRange = doc.sel.ranges[ourIndex]; + else + ourRange = new Range(start, start); + } else { + ourRange = doc.sel.primary(); + } + + if (e.altKey) { + type = "rect"; + if (!addNew) ourRange = new Range(start, start); + start = posFromMouse(cm, e, true, true); + ourIndex = -1; + } else if (type == "double") { + var word = cm.findWordAt(start); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, word.anchor, word.head); + else + ourRange = word; + } else if (type == "triple") { + var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, line.anchor, line.head); + else + ourRange = line; + } else { + ourRange = extendRange(doc, ourRange, start); + } + + if (!addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + startSel = doc.sel; + } else if (ourIndex > -1) { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } else { + ourIndex = doc.sel.ranges.length; + setSelection(doc, normalizeSelection(doc.sel.ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) return; + lastPos = pos; + + if (type == "rect") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); + else if (text.length > leftPos) + ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); + } + if (!ranges.length) ranges.push(new Range(start, start)); + setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), + {origin: "*mouse", scroll: false}); + cm.scrollIntoView(pos); + } else { + var oldRange = ourRange; + var anchor = oldRange.anchor, head = pos; + if (type != "single") { + if (type == "double") + var range = cm.findWordAt(pos); + else + var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))); + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + } + var ranges = startSel.ranges.slice(0); + ranges[ourIndex] = new Range(clipPos(doc, anchor), head); + setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true, type == "rect"); + if (!cur) return; + if (cmp(cur, lastPos) != 0) { + ensureFocus(cm); + extendTo(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) setTimeout(operation(cm, function() { + if (counter != curCount) return; + display.scroller.scrollTop += outside; + extend(e); + }), 50); + } + } + + function done(e) { + counter = Infinity; + e_preventDefault(e); + focusInput(cm); + off(document, "mousemove", move); + off(document, "mouseup", up); + doc.history.lastSelOrigin = null; + } + + var move = operation(cm, function(e) { + if (!e_button(e)) done(e); + else extend(e); + }); + var up = operation(cm, done); + on(document, "mousemove", move); + on(document, "mouseup", up); + } + + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. + function gutterEvent(cm, e, type, prevent, signalfn) { + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; + if (prevent) e_preventDefault(e); + + var display = cm.display; + var lineBox = display.lineDiv.getBoundingClientRect(); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.options.gutters.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.options.gutters[i]; + signalfn(cm, type, cm, line, gutter, e); + return e_defaultPrevented(e); + } + } + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true, signalLater); + } + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) + return; + e_preventDefault(e); + if (ie) lastDrop = +new Date; + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || isReadOnly(cm)) return; + // Might be a file drop, in which case we simply extract the text + // and insert it. + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var loadFile = function(file, i) { + var reader = new FileReader; + reader.onload = operation(cm, function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(cm.doc, pos); + var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); + } + }); + reader.readAsText(file); + }; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } else { // Normal drop + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(bind(focusInput, cm), 20); + return; + } + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + if (cm.state.draggingText && !(mac ? e.metaKey : e.ctrlKey)) + var selected = cm.listSelections(); + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) for (var i = 0; i < selected.length; ++i) + replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); + cm.replaceSelection(text, "around", "paste"); + focusInput(cm); + } + } + catch(e){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; + + e.dataTransfer.setData("Text", cm.getSelection()); + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (presto) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (presto) img.parentNode.removeChild(img); + } + } + + // SCROLL EVENTS + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. + function setScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) return; + cm.doc.scrollTop = val; + if (!gecko) updateDisplaySimple(cm, {top: val}); + if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; + if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val; + if (gecko) updateDisplaySimple(cm); + startWorker(cm, 100); + } + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. + function setScrollLeft(cm, val, isScroller) { + if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; + if (cm.display.scrollbarH.scrollLeft != val) cm.display.scrollbarH.scrollLeft = val; + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) wheelPixelsPerUnit = -.53; + else if (gecko) wheelPixelsPerUnit = 15; + else if (chrome) wheelPixelsPerUnit = -.7; + else if (safari) wheelPixelsPerUnit = -1/3; + + function onScrollWheel(cm, e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; + else if (dy == null) dy = e.wheelDelta; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + if (!(dx && scroll.scrollWidth > scroll.clientWidth || + dy && scroll.scrollHeight > scroll.clientHeight)) return; + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer; + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { + if (dy) + setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); + setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); + e_preventDefault(e); + display.wheelStartX = null; // Abort measurement, if in progress + return; + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) top = Math.max(0, top + pixels - 50); + else bot = Math.min(cm.doc.height, bot + pixels + 50); + updateDisplaySimple(cm, {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function() { + if (display.wheelStartX == null) return; + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) return; + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + // KEY EVENTS + + // Run a handler that was bound to a key. + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) return false; + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + if (cm.display.pollingFast && readInput(cm)) cm.display.pollingFast = false; + var prevShift = cm.display.shift, done = false; + try { + if (isReadOnly(cm)) cm.state.suppressEdits = true; + if (dropShift) cm.display.shift = false; + done = bound(cm) != Pass; + } finally { + cm.display.shift = prevShift; + cm.state.suppressEdits = false; + } + return done; + } + + // Collect the currently active keymaps. + function allKeyMaps(cm) { + var maps = cm.state.keyMaps.slice(0); + if (cm.options.extraKeys) maps.push(cm.options.extraKeys); + maps.push(cm.options.keyMap); + return maps; + } + + var maybeTransition; + // Handle a key from the keydown event. + function handleKeyBinding(cm, e) { + // Handle automatic keymap transitions + var startMap = getKeyMap(cm.options.keyMap), next = startMap.auto; + clearTimeout(maybeTransition); + if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() { + if (getKeyMap(cm.options.keyMap) == startMap) { + cm.options.keyMap = (next.call ? next.call(null, cm) : next); + keyMapChanged(cm); + } + }, 50); + + var name = keyName(e, true), handled = false; + if (!name) return false; + var keymaps = allKeyMaps(cm); + + if (e.shiftKey) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + handled = lookupKey("Shift-" + name, keymaps, function(b) {return doHandleBinding(cm, b, true);}) + || lookupKey(name, keymaps, function(b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + return doHandleBinding(cm, b); + }); + } else { + handled = lookupKey(name, keymaps, function(b) { return doHandleBinding(cm, b); }); + } + + if (handled) { + e_preventDefault(e); + restartBlink(cm); + signalLater(cm, "keyHandled", cm, name, e); + } + return handled; + } + + // Handle a key from the keypress event + function handleCharBinding(cm, e, ch) { + var handled = lookupKey("'" + ch + "'", allKeyMaps(cm), + function(b) { return doHandleBinding(cm, b, true); }); + if (handled) { + e_preventDefault(e); + restartBlink(cm); + signalLater(cm, "keyHandled", cm, "'" + ch + "'", e); + } + return handled; + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + ensureFocus(cm); + if (signalDOMEvent(cm, e)) return; + // IE does strange things with escape. + if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false; + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; + var handled = handleKeyBinding(cm, e); + if (presto) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + cm.replaceSelection("", null, "cut"); + } + + // Turn mouse into crosshair when Alt is held on Mac. + if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) + showCrossHair(cm); + } + + function showCrossHair(cm) { + var lineDiv = cm.display.lineDiv; + addClass(lineDiv, "CodeMirror-crosshair"); + + function up(e) { + if (e.keyCode == 18 || !e.altKey) { + rmClass(lineDiv, "CodeMirror-crosshair"); + off(document, "keyup", up); + off(document, "mouseover", up); + } + } + on(document, "keyup", up); + on(document, "mouseover", up); + } + + function onKeyUp(e) { + if (e.keyCode == 16) this.doc.sel.shift = false; + signalDOMEvent(this, e); + } + + function onKeyPress(e) { + var cm = this; + if (signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return; + var keyCode = e.keyCode, charCode = e.charCode; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if (((presto && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return; + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + if (handleCharBinding(cm, e, ch)) return; + if (ie && ie_version >= 9) cm.display.inputHasSelection = null; + fastPoll(cm); + } + + // FOCUS/BLUR EVENTS + + function onFocus(cm) { + if (cm.options.readOnly == "nocursor") return; + if (!cm.state.focused) { + signal(cm, "focus", cm); + cm.state.focused = true; + addClass(cm.display.wrapper, "CodeMirror-focused"); + // The prevInput test prevents this from firing when a context + // menu is closed (since the resetInput would kill the + // select-all detection hack) + if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { + resetInput(cm); + if (webkit) setTimeout(bind(resetInput, cm, true), 0); // Issue #1730 + } + } + slowPoll(cm); + restartBlink(cm); + } + function onBlur(cm) { + if (cm.state.focused) { + signal(cm, "blur", cm); + cm.state.focused = false; + rmClass(cm.display.wrapper, "CodeMirror-focused"); + } + clearInterval(cm.display.blinker); + setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150); + } + + // CONTEXT MENU HANDLING + + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. + function onContextMenu(cm, e) { + if (signalDOMEvent(cm, e, "contextmenu")) return; + var display = cm.display; + if (eventInWidget(display, e) || contextMenuInGutter(cm, e)) return; + + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || presto) return; // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && cm.doc.sel.contains(pos) == -1) + operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); + + var oldCSS = display.input.style.cssText; + display.inputDiv.style.position = "absolute"; + display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " + + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + + "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712) + focusInput(cm); + if (webkit) window.scrollTo(null, oldScrollY); + resetInput(cm); + // Adds "Select all" to context menu in FF + if (!cm.somethingSelected()) display.input.value = display.prevInput = " "; + display.selForContextMenu = cm.doc.sel; + clearTimeout(display.detectingSelectAll); + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. + function prepareSelectAllHack() { + if (display.input.selectionStart != null) { + var selected = cm.somethingSelected(); + var extval = display.input.value = "\u200b" + (selected ? display.input.value : ""); + display.prevInput = selected ? "" : "\u200b"; + display.input.selectionStart = 1; display.input.selectionEnd = extval.length; + // Re-set this, in case some other handler touched the + // selection in the meantime. + display.selForContextMenu = cm.doc.sel; + } + } + function rehide() { + display.inputDiv.style.position = "relative"; + display.input.style.cssText = oldCSS; + if (ie && ie_version < 9) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos; + slowPoll(cm); + + // Try to detect the user choosing select-all + if (display.input.selectionStart != null) { + if (!ie || (ie && ie_version < 9)) prepareSelectAllHack(); + var i = 0, poll = function() { + if (display.selForContextMenu == cm.doc.sel && display.input.selectionStart == 0) + operation(cm, commands.selectAll)(cm); + else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500); + else resetInput(cm); + }; + display.detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && ie_version >= 9) prepareSelectAllHack(); + if (captureRightClick) { + e_stop(e); + var mouseup = function() { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false, signal); + } + + // UPDATING + + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). + var changeEnd = CodeMirror.changeEnd = function(change) { + if (!change.text) return change.to; + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); + }; + + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) return pos; + if (cmp(pos, change.to) <= 0) return changeEnd(change); + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch; + return Pos(line, ch); + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(out, doc.sel.primIndex); + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + return Pos(nw.line, pos.ch - old.ch + nw.ch); + else + return Pos(nw.line + (pos.line - old.line), pos.ch); + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex); + } + + // Allow "beforeChange" event handlers to influence a change + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function() { this.canceled = true; } + }; + if (update) obj.update = function(from, to, text, origin) { + if (from) this.from = clipPos(doc, from); + if (to) this.to = clipPos(doc, to); + if (text) this.text = text; + if (origin !== undefined) this.origin = origin; + }; + signal(doc, "beforeChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); + + if (obj.canceled) return null; + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; + } + + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly); + if (doc.cm.state.suppressEdits) return; + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) return; + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 0; --i) + makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}); + } else { + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return; + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { + if (doc.cm && doc.cm.state.suppressEdits) return; + + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + for (var i = 0; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + break; + } + if (i == source.length) return; + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return; + } + selAfter = event; + } + else break; + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + for (var i = event.changes.length - 1; i >= 0; --i) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + source.length = 0; + return; + } + + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change) : lst(source); + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); + var rebased = []; + + // Propagate to the linked documents + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + } + } + + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. + function shiftDoc(doc, distance) { + if (distance == 0) return; + doc.first += distance; + doc.sel = new Selection(map(doc.sel.ranges, function(range) { + return new Range(Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch)); + }), doc.sel.primIndex); + if (doc.cm) { + regChange(doc.cm, doc.first, doc.first - distance, distance); + for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) + regLineChange(doc.cm, l, "gutter"); + } + } + + // More lower-level change function, handling only a single document + // (not linked ones). + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return; + } + if (change.from.line > doc.lastLine()) return; + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) selAfter = computeSelAfterChange(doc, change); + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans); + else updateDoc(doc, change, spans); + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function(line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true; + } + }); + } + + if (doc.sel.contains(change.from, change.to) > -1) + signalCursorActivity(cm); + + updateDoc(doc, change, spans, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function(line) { + var len = lineLength(line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) cm.curOp.updateMaxLine = true; + } + + // Adjust frontier, schedule worker + doc.frontier = Math.min(doc.frontier, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + regLineChange(cm, from.line, "text"); + else + regChange(cm, from.line, to.line + 1, lendiff); + + var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); + if (changeHandler || changesHandler) { + var obj = { + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }; + if (changeHandler) signalLater(cm, "change", cm, obj); + if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); + } + cm.display.selForContextMenu = null; + } + + function replaceRange(doc, code, from, to, origin) { + if (!to) to = from; + if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } + if (typeof code == "string") code = splitLines(code); + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, coords) { + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; + if (coords.top + box.top < 0) doScroll = true; + else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " + + (coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " + + (coords.bottom - coords.top + scrollerCutOff) + "px; left: " + + coords.left + "px; width: 2px;"); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) margin = 0; + for (;;) { + var changed = false, coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), + Math.min(coords.top, endCoords.top) - margin, + Math.max(coords.left, endCoords.left), + Math.max(coords.bottom, endCoords.bottom) + margin); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + setScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; + } + if (!changed) return coords; + } + } + + // Scroll a given set of coordinates into view (immediately). + function scrollIntoView(cm, x1, y1, x2, y2) { + var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); + if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); + if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); + } + + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. + function calculateScrollPos(cm, x1, y1, x2, y2) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (y1 < 0) y1 = 0; + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = display.scroller.clientHeight - scrollerCutOff, result = {}; + if (y2 - y1 > screen) y2 = y1 + screen; + var docBottom = cm.doc.height + paddingVert(display); + var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; + if (y1 < screentop) { + result.scrollTop = atTop ? 0 : y1; + } else if (y2 > screentop + screen) { + var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); + if (newTop != screentop) result.scrollTop = newTop; + } + + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; + var screenw = display.scroller.clientWidth - scrollerCutOff - display.gutters.offsetWidth; + var tooWide = x2 - x1 > screenw; + if (tooWide) x2 = y1 + screen; + if (x1 < 10) + result.scrollLeft = 0; + else if (x1 < screenleft) + result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10)); + else if (x2 > screenw + screenleft - 3) + result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw; + + return result; + } + + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollPos(cm, left, top) { + if (left != null || top != null) resolveScrollToPos(cm); + if (left != null) + cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left; + if (top != null) + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(), from = cur, to = cur; + if (!cm.options.lineWrapping) { + from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur; + to = Pos(cur.line, cur.ch + 1); + } + cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), + Math.min(from.top, to.top) - range.margin, + Math.max(from.right, to.right), + Math.max(from.bottom, to.bottom) + range.margin); + cm.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + } + + // API UTILITIES + + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc, state; + if (how == null) how = "add"; + if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. + if (!doc.mode.indent) how = "prev"; + else state = getStateBefore(cm, n); + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) line.stateAfter = null; + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass || indentation > 150) { + if (!aggressive) return; + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); + else indentation = 0; + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} + if (pos < indentation) indentString += spaceStr(indentation - pos); + + if (indentString != curSpaceString) { + replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i, new Range(pos, pos)); + break; + } + } + } + line.stateAfter = null; + } + + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(doc, handle, changeType, op) { + var no = handle, line = handle; + if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType); + return line; + } + + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break; + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function() { + for (var i = kill.length - 1; i >= 0; i--) + replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); + ensureCursorVisible(cm); + }); + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "char", "column" (like char, but doesn't + // cross line boundaries), "word" (across next word), or "group" (to + // the start of next group of word or non-word-non-whitespace + // chars). The visually param controls whether, in right-to-left + // text, direction 1 means to move towards the next index in the + // string, or towards the character to the right of the current + // position. The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosH(doc, pos, dir, unit, visually) { + var line = pos.line, ch = pos.ch, origDir = dir; + var lineObj = getLine(doc, line); + var possible = true; + function findNextLine() { + var l = line + dir; + if (l < doc.first || l >= doc.first + doc.size) return (possible = false); + line = l; + return lineObj = getLine(doc, l); + } + function moveOnce(boundToLine) { + var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); + if (next == null) { + if (!boundToLine && findNextLine()) { + if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); + else ch = dir < 0 ? lineObj.text.length : 0; + } else return (possible = false); + } else ch = next; + return true; + } + + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) break; + var cur = lineObj.text.charAt(ch) || "\n"; + var type = isWordChar(cur, helper) ? "w" + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null + : "p"; + if (group && !first && !type) type = "s"; + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce();} + break; + } + + if (type) sawType = type; + if (dir > 0 && !moveOnce(!first)) break; + } + } + var result = skipAtomic(doc, Pos(line, ch), origDir, true); + if (!possible) result.hitSide = true; + return result; + } + + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + for (;;) { + var target = coordsChar(cm, x, y); + if (!target.outside) break; + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } + y += dir * 5; + } + return target; + } + + // EDITOR METHODS + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); focusInput(this); fastPoll(this);}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") return; + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + operation(this, optionHandlers[option])(this, value, old); + }, + + getOption: function(option) {return this.options[option];}, + getDoc: function() {return this.doc;}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](map); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + if (maps[i] == map || (typeof maps[i] != "string" && maps[i].name == map)) { + maps.splice(i, 1); + return true; + } + }, + + addOverlay: methodOp(function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) throw new Error("Overlays may not be stateful."); + this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: methodOp(function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return; + } + } + }), + + indentLine: methodOp(function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; + else dir = dir ? "add" : "subtract"; + } + if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); + }), + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var from = range.from(), to = range.to(); + var start = Math.max(end, from.line); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + indentLine(this, j, how); + var newRanges = this.doc.sel.ranges; + if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) + replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) ensureCursorVisible(this); + } + } + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + var doc = this.doc; + pos = clipPos(doc, pos); + var state = getStateBefore(this, pos.line, precise), mode = this.doc.mode; + var line = getLine(doc, pos.line); + var stream = new StringStream(line.text, this.options.tabSize); + while (stream.pos < pos.ch && !stream.eol()) { + stream.start = stream.pos; + var style = readToken(mode, stream, state); + } + return {start: stream.start, + end: stream.pos, + string: stream.current(), + type: style || null, + state: state}; + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + var type; + if (ch == 0) type = styles[2]; + else for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; + else if (styles[mid * 2 + 1] < ch) before = mid + 1; + else { type = styles[mid * 2 + 2]; break; } + } + var cut = type ? type.indexOf("cm-overlay ") : -1; + return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1); + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) return mode; + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0]; + }, + + getHelpers: function(pos, type) { + var found = []; + if (!helpers.hasOwnProperty(type)) return helpers; + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) found.push(help[mode[type]]); + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) found.push(val); + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i = 0; i < help._global.length; i++) { + var cur = help._global[i]; + if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) + found.push(cur.val); + } + return found; + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getStateBefore(this, line + 1, precise); + }, + + cursorCoords: function(start, mode) { + var pos, range = this.doc.sel.primary(); + if (start == null) pos = range.head; + else if (typeof start == "object") pos = clipPos(this.doc, start); + else pos = start ? range.from() : range.to(); + return cursorCoords(this, pos, mode || "page"); + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page"); + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top); + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset); + }, + heightAtLine: function(line, mode) { + var end = false, last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) line = this.doc.first; + else if (line > last) { line = last; end = true; } + var lineObj = getLine(this.doc, line); + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + + (end ? this.doc.height - heightAtLine(lineObj) : 0); + }, + + defaultTextHeight: function() { return textHeight(this.display); }, + defaultCharWidth: function() { return charWidth(this.display); }, + + setGutterMarker: methodOp(function(line, gutterID, value) { + return changeLine(this.doc, line, "gutter", function(line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) line.gutterMarkers = null; + return true; + }); + }), + + clearGutter: methodOp(function(gutterID) { + var cm = this, doc = cm.doc, i = doc.first; + doc.iter(function(line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + line.gutterMarkers[gutterID] = null; + regLineChange(cm, i, "gutter"); + if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; + } + ++i; + }); + }), + + addLineWidget: methodOp(function(handle, node, options) { + return addLineWidget(this, handle, node, options); + }), + + removeLineWidget: function(widget) { widget.clear(); }, + + lineInfo: function(line) { + if (typeof line == "number") { + if (!isLine(this.doc, line)) return null; + var n = line; + line = getLine(this.doc, line); + if (!line) return null; + } else { + var n = lineNo(line); + if (n == null) return null; + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets}; + }, + + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + top = pos.top - node.offsetHeight; + else if (pos.bottom + node.offsetHeight <= vspace) + top = pos.bottom; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; + node.style.left = left + "px"; + } + if (scroll) + scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: onKeyUp, + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + return commands[cmd](this); + }, + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) break; + } + return cur; + }, + + moveH: methodOp(function(dir, unit) { + var cm = this; + cm.extendSelectionsBy(function(range) { + if (cm.display.shift || cm.doc.extend || range.empty()) + return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually); + else + return dir < 0 ? range.from() : range.to(); + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + doc.replaceSelection("", null, "+delete"); + else + deleteNearSelection(this, function(range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other}; + }); + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) x = coords.left; + else coords.left = x; + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) break; + } + return cur; + }, + + moveV: methodOp(function(dir, unit) { + var cm = this, doc = this.doc, goals = []; + var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function(range) { + if (collapse) + return dir < 0 ? range.from() : range.to(); + var headPos = cursorCoords(cm, range.head, "div"); + if (range.goalColumn != null) headPos.left = range.goalColumn; + goals.push(headPos.left); + var pos = findPosV(cm, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top); + return pos; + }, sel_move); + if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++) + doc.sel.ranges[i].goalColumn = goals[i]; + }), + + // Find the word at the given position (as returned by coordsChar). + findWordAt: function(pos) { + var doc = this.doc, line = getLine(doc, pos.line).text; + var start = pos.ch, end = pos.ch; + if (line) { + var helper = this.getHelper(pos, "wordChars"); + if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; + var startChar = line.charAt(start); + var check = isWordChar(startChar, helper) + ? function(ch) { return isWordChar(ch, helper); } + : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} + : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + } + return new Range(Pos(pos.line, start), Pos(pos.line, end)); + }, + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) return; + if (this.state.overwrite = !this.state.overwrite) + addClass(this.display.cursorDiv, "CodeMirror-overwrite"); + else + rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); + + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return activeElt() == this.display.input; }, + + scrollTo: methodOp(function(x, y) { + if (x != null || y != null) resolveScrollToPos(this); + if (x != null) this.curOp.scrollLeft = x; + if (y != null) this.curOp.scrollTop = y; + }), + getScrollInfo: function() { + var scroller = this.display.scroller, co = scrollerCutOff; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - co, width: scroller.scrollWidth - co, + clientHeight: scroller.clientHeight - co, clientWidth: scroller.clientWidth - co}; + }, + + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) margin = this.options.cursorScrollMargin; + } else if (typeof range == "number") { + range = {from: Pos(range, 0), to: null}; + } else if (range.from == null) { + range = {from: range, to: null}; + } + if (!range.to) range.to = range.from; + range.margin = margin || 0; + + if (range.from.line != null) { + resolveScrollToPos(this); + this.curOp.scrollToPos = range; + } else { + var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left), + Math.min(range.from.top, range.to.top) - range.margin, + Math.max(range.from.right, range.to.right), + Math.max(range.from.bottom, range.to.bottom) + range.margin); + this.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + }), + + setSize: methodOp(function(width, height) { + var cm = this; + function interpret(val) { + return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; + } + if (width != null) cm.display.wrapper.style.width = interpret(width); + if (height != null) cm.display.wrapper.style.height = interpret(height); + if (cm.options.lineWrapping) clearLineMeasurementCache(this); + var lineNo = cm.display.viewFrom; + cm.doc.iter(lineNo, cm.display.viewTo, function(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) + if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; } + ++lineNo; + }); + cm.curOp.forceUpdate = true; + signal(cm, "refresh", this); + }), + + operation: function(f){return runInOp(this, f);}, + + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; + regChange(this); + this.curOp.forceUpdate = true; + clearCaches(this); + this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop); + updateGutterSpace(this); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) + estimateLineHeights(this); + signal(this, "refresh", this); + }), + + swapDoc: methodOp(function(doc) { + var old = this.doc; + old.cm = null; + attachDoc(this, doc); + clearCaches(this); + resetInput(this); + this.scrollTo(doc.scrollLeft, doc.scrollTop); + this.curOp.forceScroll = true; + signalLater(this, "swapDoc", this, old); + return old; + }), + + getInputField: function(){return this.display.input;}, + getWrapperElement: function(){return this.display.wrapper;}, + getScrollerElement: function(){return this.display.scroller;}, + getGutterElement: function(){return this.display.gutters;} + }; + eventMixin(CodeMirror); + + // OPTION DEFAULTS + + // The default configuration options. + var defaults = CodeMirror.defaults = {}; + // Functions to run when options are changed. + var optionHandlers = CodeMirror.optionHandlers = {}; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) optionHandlers[name] = + notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; + } + + // Passed to option handlers when there is no old value. + var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function(cm, val) { + cm.setValue(val); + }, true); + option("mode", null, function(cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function(cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val) { + cm.options.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + cm.refresh(); + }, true); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); + option("electricChars", true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function(cm) { + themeChanged(cm); + guttersChanged(cm); + }, true); + option("keyMap", "default", keyMapChanged); + option("extraKeys", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("fixedGutter", true, function(cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, updateScrollbars, true); + option("lineNumbers", false, function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("firstLineNumber", 1, guttersChanged, true); + option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + + option("readOnly", false, function(cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + cm.display.disabled = true; + } else { + cm.display.disabled = false; + if (!val) resetInput(cm); + } + }); + option("disableInput", false, function(cm, val) {if (!val) resetInput(cm);}, true); + option("dragDrop", true); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1, updateSelection, true); + option("singleCursorHeightPerLine", true, updateSelection, true); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 1250); + option("viewportMargin", 10, function(cm){cm.refresh();}, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("moveInputWithCursor", true, function(cm, val) { + if (!val) cm.display.inputDiv.style.top = cm.display.inputDiv.style.left = 0; + }); + + option("tabindex", null, function(cm, val) { + cm.display.input.tabIndex = val || ""; + }); + option("autofocus", null); + + // MODE DEFINITION AND QUERYING + + // Known modes, by name and by MIME + var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + if (arguments.length > 2) { + mode.dependencies = []; + for (var i = 2; i < arguments.length; ++i) mode.dependencies.push(arguments[i]); + } + modes[name] = mode; + }; + + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. + CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + if (typeof found == "string") found = {name: found}; + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return CodeMirror.resolveMode("application/xml"); + } + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; + }; + + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. + CodeMirror.getMode = function(options, spec) { + var spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) return CodeMirror.getMode(options, "text/plain"); + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) continue; + if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) modeObj.helperType = spec.helperType; + if (spec.modeProps) for (var prop in spec.modeProps) + modeObj[prop] = spec.modeProps[prop]; + + return modeObj; + }; + + // Minimal default mode. + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + // This can be used to attach properties to mode objects from + // outside the actual mode definition. + var modeExtensions = CodeMirror.modeExtensions = {}; + CodeMirror.extendMode = function(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + }; + + // EXTENSIONS + + CodeMirror.defineExtension = function(name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function(name, func) { + Doc.prototype[name] = func; + }; + CodeMirror.defineOption = option; + + var initHooks = []; + CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + + var helpers = CodeMirror.helpers = {}; + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []}; + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + + // MODE STATE HANDLING + + // Utility functions for working with state. Exported because nested + // modes need to do this for their inner modes. + + var copyState = CodeMirror.copyState = function(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + }; + + var startState = CodeMirror.startState = function(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + }; + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. + CodeMirror.innerMode = function(mode, state) { + while (mode.innerMode) { + var info = mode.innerMode(state); + if (!info || info.mode == mode) break; + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state}; + }; + + // STANDARD COMMANDS + + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);}, + singleSelection: function(cm) { + cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); + }, + killLine: function(cm) { + deleteNearSelection(cm, function(range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + return {from: range.head, to: Pos(range.head.line + 1, 0)}; + else + return {from: range.head, to: Pos(range.head.line, len)}; + } else { + return {from: range.from(), to: range.to()}; + } + }); + }, + deleteLine: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0))}; + }); + }, + delLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), to: range.from()}; + }); + }, + delWrappedLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var leftPos = cm.coordsChar({left: 0, top: top}, "div"); + return {from: leftPos, to: range.from()}; + }); + }, + delWrappedLineRight: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + return {from: range.from(), to: rightPos }; + }); + }, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + undoSelection: function(cm) {cm.undoSelection();}, + redoSelection: function(cm) {cm.redoSelection();}, + goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, + goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, + goLineStart: function(cm) { + cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, + {origin: "+move", bias: 1}); + }, + goLineStartSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + return lineStartSmart(cm, range.head); + }, {origin: "+move", bias: 1}); + }, + goLineEnd: function(cm) { + cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, + {origin: "+move", bias: -1}); + }, + goLineRight: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + }, sel_move); + }, + goLineLeft: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div"); + }, sel_move); + }, + goLineLeftSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var pos = cm.coordsChar({left: 0, top: top}, "div"); + if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head); + return pos; + }, sel_move); + }, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goGroupRight: function(cm) {cm.moveH(1, "group");}, + goGroupLeft: function(cm) {cm.moveH(-1, "group");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharBefore: function(cm) {cm.deleteH(-1, "char");}, + delCharAfter: function(cm) {cm.deleteH(1, "char");}, + delWordBefore: function(cm) {cm.deleteH(-1, "word");}, + delWordAfter: function(cm) {cm.deleteH(1, "word");}, + delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, + delGroupAfter: function(cm) {cm.deleteH(1, "group");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t");}, + insertSoftTab: function(cm) { + var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].from(); + var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); + spaces.push(new Array(tabSize - col % tabSize + 1).join(" ")); + } + cm.replaceSelections(spaces); + }, + defaultTab: function(cm) { + if (cm.somethingSelected()) cm.indentSelection("add"); + else cm.execCommand("insertTab"); + }, + transposeChars: function(cm) { + runInOp(cm, function() { + var ranges = cm.listSelections(), newSel = []; + for (var i = 0; i < ranges.length; i++) { + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (line) { + if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1); + if (cur.ch > 0) { + cur = new Pos(cur.line, cur.ch + 1); + cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), + Pos(cur.line, cur.ch - 2), cur, "+transpose"); + } else if (cur.line > cm.doc.first) { + var prev = getLine(cm.doc, cur.line - 1).text; + if (prev) + cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1), + Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose"); + } + } + newSel.push(new Range(cur, cur)); + } + cm.setSelections(newSel); + }); + }, + newlineAndIndent: function(cm) { + runInOp(cm, function() { + var len = cm.listSelections().length; + for (var i = 0; i < len; i++) { + var range = cm.listSelections()[i]; + cm.replaceRange("\n", range.anchor, range.head, "+input"); + cm.indentLine(range.from().line + 1, null, true); + ensureCursorVisible(cm); + } + }); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + // STANDARD KEYMAPS + + var keyMap = CodeMirror.keyMap = {}; + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" + }; + // Note that the save and find-related commands aren't defined by + // default. User code or addons can define them. Unknown commands + // are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Ctrl-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", + fallthrough: "basic" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", + fallthrough: ["basic", "emacsy"] + }; + // Very basic readline/emacs-style bindings, which are standard on Mac. + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + + // KEYMAP DISPATCH + + function getKeyMap(val) { + if (typeof val == "string") return keyMap[val]; + else return val; + } + + // Given an array of keymaps and a key name, call handle on any + // bindings found, until that returns a truthy value, at which point + // we consider the key handled. Implements things like binding a key + // to false stopping further handling and keymap fallthrough. + var lookupKey = CodeMirror.lookupKey = function(name, maps, handle) { + function lookup(map) { + map = getKeyMap(map); + var found = map[name]; + if (found === false) return "stop"; + if (found != null && handle(found)) return true; + if (map.nofallthrough) return "stop"; + + var fallthrough = map.fallthrough; + if (fallthrough == null) return false; + if (Object.prototype.toString.call(fallthrough) != "[object Array]") + return lookup(fallthrough); + for (var i = 0; i < fallthrough.length; ++i) { + var done = lookup(fallthrough[i]); + if (done) return done; + } + return false; + } + + for (var i = 0; i < maps.length; ++i) { + var done = lookup(maps[i]); + if (done) return done != "stop"; + } + }; + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + var isModifierKey = CodeMirror.isModifierKey = function(event) { + var name = keyNames[event.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + }; + + // Look up the name of a key as indicated by an event object. + var keyName = CodeMirror.keyName = function(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) return false; + var name = keyNames[event.keyCode]; + if (name == null || event.altGraphKey) return false; + if (event.altKey) name = "Alt-" + name; + if (flipCtrlCmd ? event.metaKey : event.ctrlKey) name = "Ctrl-" + name; + if (flipCtrlCmd ? event.ctrlKey : event.metaKey) name = "Cmd-" + name; + if (!noShift && event.shiftKey) name = "Shift-" + name; + return name; + }; + + // FROMTEXTAREA + + CodeMirror.fromTextArea = function(textarea, options) { + if (!options) options = {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabindex) + options.tabindex = textarea.tabindex; + if (!options.placeholder && textarea.placeholder) + options.placeholder = textarea.placeholder; + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = activeElt(); + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form, realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function() { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + textarea.style.display = "none"; + var cm = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + cm.save = save; + cm.getTextArea = function() { return textarea; }; + cm.toTextArea = function() { + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + return cm; + }; + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + var StringStream = CodeMirror.StringStream = function(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + }; + + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == this.lineStart;}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + indentation: function() { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);}, + hideFirstChars: function(n, inner) { + this.lineStart += n; + try { return inner(); } + finally { this.lineStart -= n; } + } + }; + + // TEXTMARKERS + + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + var TextMarker = CodeMirror.TextMarker = function(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + }; + eventMixin(TextMarker); + + // Clear the marker. + TextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) startOperation(cm); + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) signalLater(this, "clear", found.from, found.to); + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text"); + else if (cm) { + if (span.to != null) max = lineNo(line); + if (span.from != null) min = lineNo(line); + } + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) + updateLineHeight(line, textHeight(cm.display)); + } + if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { + var visual = visualLine(this.lines[i]), len = lineLength(visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } + + if (min != null && cm && this.collapsed) regChange(cm, min, max + 1); + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) reCheckSelection(cm.doc); + } + if (cm) signalLater(cm, "markerCleared", cm, this); + if (withOp) endOperation(cm); + if (this.parent) this.parent.clear(); + }; + + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function(side, lineObj) { + if (side == null && this.type == "bookmark") side = 1; + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) return from; + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) return to; + } + } + return from && {from: from, to: to}; + }; + + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. + TextMarker.prototype.changed = function() { + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; + if (!pos || !cm) return; + runInOp(cm, function() { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + updateLineHeight(line, line.height + dHeight); + } + }); + }; + + TextMarker.prototype.attachLine = function(line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); + } + this.lines.push(line); + }; + TextMarker.prototype.detachLine = function(line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). + var nextMarkerId = 0; + + // Create a marker, wire it up to the right lines, and + function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). + if (options && options.shared) return markTextShared(doc, from, to, options, type); + // Ensure we are in an operation. + if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); + + var marker = new TextMarker(doc, type), diff = cmp(from, to); + if (options) copyObj(options, marker, false); + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) + return marker; + if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) + marker.collapsed = true; + marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.widgetNode.ignoreEvents = true; + if (options.insertLeft) marker.widgetNode.insertLeft = true; + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + throw new Error("Inserting collapsed marker partially overlapping an existing one"); + sawCollapsedSpans = true; + } + + if (marker.addToHistory) + addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function(line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) + updateMaxLine = true; + if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0); + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); + ++curLine; + }); + // lineIsHidden depends on the presence of the spans, so needs a second pass + if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { + if (lineIsHidden(doc, line)) updateLineHeight(line, 0); + }); + + if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); + + if (marker.readOnly) { + sawReadOnlySpans = true; + if (doc.history.done.length || doc.history.undone.length) + doc.clearHistory(); + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + // Sync editor state + if (updateMaxLine) cm.curOp.updateMaxLine = true; + if (marker.collapsed) + regChange(cm, from.line, to.line + 1); + else if (marker.className || marker.title || marker.startStyle || marker.endStyle) + for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text"); + if (marker.atomic) reCheckSelection(cm.doc); + signalLater(cm, "markerAdded", cm, marker); + } + return marker; + } + + // SHARED TEXTMARKERS + + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0; i < markers.length; ++i) + markers[i].parent = this; + }; + eventMixin(SharedTextMarker); + + SharedTextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + this.markers[i].clear(); + signalLater(this, "clear"); + }; + SharedTextMarker.prototype.find = function(side, lineObj) { + return this.primary.find(side, lineObj); + }; + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.widgetNode; + linkedDocs(doc, function(doc) { + if (widget) options.widgetNode = widget.cloneNode(true); + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + if (doc.linked[i].isParent) return; + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary); + } + + function findSharedMarkers(doc) { + return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), + function(m) { return m.parent; }); + } + + function copySharedMarkers(doc, markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], pos = marker.find(); + var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); + if (cmp(mFrom, mTo)) { + var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); + marker.markers.push(subMark); + subMark.parent = marker; + } + } + } + + function detachSharedMarkers(markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], linked = [marker.primary.doc];; + linkedDocs(marker.primary.doc, function(d) { linked.push(d); }); + for (var j = 0; j < marker.markers.length; j++) { + var subMarker = marker.markers[j]; + if (indexOf(linked, subMarker.doc) == -1) { + subMarker.parent = null; + marker.markers.splice(j--, 1); + } + } + } + } + + // TEXTMARKER SPANS + + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. + function getMarkedSpanFor(spans, marker) { + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) return span; + } + } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). + function removeMarkedSpan(spans, span) { + for (var r, i = 0; i < spans.length; ++i) + if (spans[i] != span) (r || (r = [])).push(spans[i]); + return r; + } + // Add a span to a line. + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). + function markedSpansBefore(old, startCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); + (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); + } + } + return nw; + } + function markedSpansAfter(old, endCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); + (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); + } + } + return nw; + } + + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. + function stretchSpansOverChange(doc, change) { + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) return null; + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) span.to = startCh; + else if (sameLine) span.to = found.to == null ? null : found.to + offset; + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i = 0; i < last.length; ++i) { + var span = last[i]; + if (span.to != null) span.to += offset; + if (span.from == null) { + var found = getMarkedSpanFor(first, span.marker); + if (!found) { + span.from = offset; + if (sameLine) (first || (first = [])).push(span); + } + } else { + span.from += offset; + if (sameLine) (first || (first = [])).push(span); + } + } + } + // Make sure we didn't create any zero-length spans + if (first) first = clearEmptySpans(first); + if (last && last != first) last = clearEmptySpans(last); + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + for (var i = 0; i < first.length; ++i) + if (first[i].to == null) + (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)); + for (var i = 0; i < gap; ++i) + newMarkers.push(gapMarkers); + newMarkers.push(last); + } + return newMarkers; + } + + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + spans.splice(i--, 1); + } + if (!spans.length) return null; + return spans; + } + + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) return stretched; + if (!stretched) return old; + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + if (oldCur[k].marker == span.marker) continue spans; + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old; + } + + // Used to 'clip' out readOnly ranges when making a change. + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function(line) { + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + (markers || (markers = [])).push(mark); + } + }); + if (!markers) return null; + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(0); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue; + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) + newParts.push({from: p.from, to: m.from}); + if (dto > 0 || !mk.inclusiveRight && !dto) + newParts.push({from: m.to, to: p.to}); + parts.splice.apply(parts, newParts); + j += newParts.length - 1; + } + } + return parts; + } + + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.detachLine(line); + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.attachLine(line); + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } + + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) return lenDiff; + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) return -fromCmp; + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) return toCmp; + return b.id - a.id; + } + + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + found = sp.marker; + } + return found; + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); } + + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. + function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) continue; + var found = sp.marker.find(0); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; + if (fromCmp <= 0 && (cmp(found.to, from) > 0 || (sp.marker.inclusiveRight && marker.inclusiveLeft)) || + fromCmp >= 0 && (cmp(found.from, to) < 0 || (sp.marker.inclusiveLeft && marker.inclusiveRight))) + return true; + } + } + + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + line = merged.find(-1, true).line; + return line; + } + + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + (lines || (lines = [])).push(line); + } + return lines; + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) return lineN; + return lineNo(vis); + } + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) return lineN; + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) return lineN; + while (merged = collapsedSpanAtEnd(line)) + line = merged.find(1, true).line; + return lineNo(line) + 1; + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if (sp.from == null) return true; + if (sp.marker.widgetNode) continue; + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + return true; + } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)); + } + if (span.marker.inclusiveRight && span.to == line.text.length) + return true; + for (var sp, i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) return true; + } + } + + // LINE WIDGETS + + // Line widgets are block elements displayed above or below a line. + + var LineWidget = CodeMirror.LineWidget = function(cm, node, options) { + if (options) for (var opt in options) if (options.hasOwnProperty(opt)) + this[opt] = options[opt]; + this.cm = cm; + this.node = node; + }; + eventMixin(LineWidget); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + addToScrollPos(cm, null, diff); + } + + LineWidget.prototype.clear = function() { + var cm = this.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); + if (no == null || !ws) return; + for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); + if (!ws.length) line.widgets = null; + var height = widgetHeight(this); + runInOp(cm, function() { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + updateLineHeight(line, Math.max(0, line.height - height)); + }); + }; + LineWidget.prototype.changed = function() { + var oldH = this.height, cm = this.cm, line = this.line; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) return; + runInOp(cm, function() { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + updateLineHeight(line, line.height + diff); + }); + }; + + function widgetHeight(widget) { + if (widget.height != null) return widget.height; + if (!contains(document.body, widget.node)) { + var parentStyle = "position: relative;"; + if (widget.coverGutter) + parentStyle += "margin-left: -" + widget.cm.getGutterElement().offsetWidth + "px;"; + removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, parentStyle)); + } + return widget.height = widget.node.offsetHeight; + } + + function addLineWidget(cm, handle, node, options) { + var widget = new LineWidget(cm, node, options); + if (widget.noHScroll) cm.display.alignWidgets = true; + changeLine(cm.doc, handle, "widget", function(line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) widgets.push(widget); + else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); + widget.line = line; + if (!lineIsHidden(cm.doc, line)) { + var aboveVisible = heightAtLine(line) < cm.doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) addToScrollPos(cm, null, widget.height); + cm.curOp.forceUpdate = true; + } + return true; + }); + return widget; + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + eventMixin(Line); + Line.prototype.lineNo = function() { return lineNo(this); }; + + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + if (line.order != null) line.order = null; + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) updateLineHeight(line, estHeight); + } + + // Detach a line from the document tree and its markers. + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + function extractLineClasses(type, output) { + if (type) for (;;) { + var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); + if (!lineClass) break; + type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (output[prop] == null) + output[prop] = lineClass[2]; + else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) + output[prop] += " " + lineClass[2]; + } + return type; + } + + function callBlankLine(mode, state) { + if (mode.blankLine) return mode.blankLine(state); + if (!mode.innerMode) return; + var inner = CodeMirror.innerMode(mode, state); + if (inner.mode.blankLine) return inner.mode.blankLine(inner.state); + } + + function readToken(mode, stream, state) { + for (var i = 0; i < 10; i++) { + var style = mode.token(stream, state); + if (stream.pos > stream.start) return style; + } + throw new Error("Mode " + mode.name + " failed to advance stream."); + } + + // Run the given mode's parser over a line, calling f for each token. + function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize), style; + if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses); + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) processLine(cm, text, state, stream.pos); + stream.pos = text.length; + style = null; + } else { + style = extractLineClasses(readToken(mode, stream, state), lineClasses); + } + if (cm.options.addModeClass) { + var mName = CodeMirror.innerMode(mode, state).mode.name; + if (mName) style = "m-" + (style ? mName + " " + style : mName); + } + if (!flattenSpans || curStyle != style) { + if (curStart < stream.start) f(stream.start, curStyle); + curStart = stream.start; curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 characters + var pos = Math.min(stream.pos, curStart + 50000); + f(pos, curStyle); + curStart = pos; + } + } + + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. + function highlightLine(cm, line, state, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen], lineClasses = {}; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, state, function(end, style) { + st.push(end, style); + }, lineClasses, forceToEnd); + + // Run overlays, adjust style array. + for (var o = 0; o < cm.state.overlays.length; ++o) { + var overlay = cm.state.overlays[o], i = 1, at = 0; + runMode(cm, line.text, overlay.mode, true, function(end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + st.splice(i, 1, end, st[i+1], i_end); + i += 2; + at = Math.min(end, i_end); + } + if (!style) return; + if (overlay.opaque) { + st.splice(start, i - start, end, "cm-overlay " + style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style; + } + } + }, lineClasses); + } + + return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}; + } + + function getLineStyles(cm, line) { + if (!line.styles || line.styles[0] != cm.state.modeGen) { + var result = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); + line.styles = result.styles; + if (result.classes) line.styleClasses = result.classes; + else if (line.styleClasses) line.styleClasses = null; + } + return line.styles; + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. Used for lines that + // aren't currently visible. + function processLine(cm, text, state, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize); + stream.start = stream.pos = startAt || 0; + if (text == "") callBlankLine(mode, state); + while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { + readToken(mode, stream, state); + stream.start = stream.pos; + } + } + + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, options) { + if (!style || /^\s*$/.test(style)) return null; + var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = style.replace(/\S+/g, "cm-$&")); + } + + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: elt("pre", [content]), content: content, col: 0, pos: 0, cm: cm}; + lineView.measure = {}; + + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order; + builder.pos = 0; + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if ((ie || webkit) && cm.getOption("lineWrapping")) + builder.addToken = buildTokenSplitSpaces(builder.addToken); + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) + builder.addToken = buildTokenBadBidi(builder.addToken, order); + builder.map = []; + insertLineContent(line, builder, getLineStyles(cm, line)); + if (line.styleClasses) { + if (line.styleClasses.bgClass) + builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); + if (line.styleClasses.textClass) + builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); + } + + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); + + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map); + (lineView.measure.caches || (lineView.measure.caches = [])).push({}); + } + } + + signal(cm, "renderLine", cm, lineView.line, builder.pre); + if (builder.pre.className) + builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); + return builder; + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + return token; + } + + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. + function buildToken(builder, text, style, startStyle, endStyle, title) { + if (!text) return; + var special = builder.cm.options.specialChars, mustWrap = false; + if (!special.test(text)) { + builder.col += text.length; + var content = document.createTextNode(text); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie && ie_version < 9) mustWrap = true; + builder.pos += text.length; + } else { + var content = document.createDocumentFragment(), pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + var txt = document.createTextNode(text.slice(pos, pos + skipped)); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.map.push(builder.pos, builder.pos + skipped, txt); + builder.col += skipped; + builder.pos += skipped; + } + if (!m) break; + pos += skipped + 1; + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + builder.col += tabWidth; + } else { + var txt = builder.cm.options.specialCharPlaceholder(m[0]); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.col += 1; + } + builder.map.push(builder.pos, builder.pos + 1, txt); + builder.pos++; + } + } + if (style || startStyle || endStyle || mustWrap) { + var fullStyle = style || ""; + if (startStyle) fullStyle += startStyle; + if (endStyle) fullStyle += endStyle; + var token = elt("span", [content], fullStyle); + if (title) token.title = title; + return builder.content.appendChild(token); + } + builder.content.appendChild(content); + } + + function buildTokenSplitSpaces(inner) { + function split(old) { + var out = " "; + for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; + out += " "; + return out; + } + return function(builder, text, style, startStyle, endStyle, title) { + inner(builder, text.replace(/ {3,}/g, split), style, startStyle, endStyle, title); + }; + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function(builder, text, style, startStyle, endStyle, title) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + for (var i = 0; i < order.length; i++) { + var part = order[i]; + if (part.to > start && part.from <= start) break; + } + if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title); + inner(builder, text.slice(0, part.to - start), style, startStyle, null, title); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } + }; + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.widgetNode; + if (widget) { + builder.map.push(builder.pos, builder.pos + size, widget); + builder.content.appendChild(widget); + } + builder.pos += size; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i = 1; i < styles.length; i+=2) + builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options)); + return; + } + + var len = allText.length, pos = 0, i = 1, text = "", style; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = title = ""; + collapsed = null; nextChange = Infinity; + var foundBookmarks = []; + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (sp.from <= pos && (sp.to == null || sp.to > pos)) { + if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; } + if (m.className) spanStyle += " " + m.className; + if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; + if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; + if (m.title && !title) title = m.title; + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + collapsed = sp; + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) foundBookmarks.push(m); + } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) return; + } + if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) + buildCollapsedSpan(builder, 0, foundBookmarks[j]); + } + if (pos >= len) break; + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder.cm.options); + } + } + } + + // DOCUMENT DATA STRUCTURE + + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore); + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null;} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // Adjust the line structure + if (isWholeLineUpdate(doc, change)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + for (var i = 0, added = []; i < text.length - 1; ++i) + added.push(new Line(text[i], spansFor(i), estimateHeight)); + update(lastLine, lastLine.text, lastSpans); + if (nlines) doc.remove(from.line, nlines); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + for (var added = [], i = 1; i < text.length - 1; ++i) + added.push(new Line(text[i], spansFor(i), estimateHeight)); + added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + for (var i = 1, added = []; i < text.length - 1; ++i) + added.push(new Line(text[i], spansFor(i), estimateHeight)); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1); + doc.insert(from.line + 1, added); + } + + signalLater(doc, "change", doc, change); + } + + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, height = 0; i < lines.length; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + // Remove the n lines at offset 'at'. + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + // Helper used to collapse a small branch into a single leaf. + collapse: function(lines) { + lines.push.apply(lines, this.lines); + }, + // Insert the given array of lines at offset 'at', count them as + // having the given height. + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0; i < lines.length; ++i) lines[i].parent = this; + }, + // Used to iterate over a part of the tree. + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0; i < children.length; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + collapse: function(lines) { + for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines); + }, + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + // When a node has grown, check whether it should be split. + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iterN: function(at, n, op) { + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + var nextDocId = 0; + var Doc = CodeMirror.Doc = function(text, mode, firstLine) { + if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); + if (firstLine == null) firstLine = 0; + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.cleanGeneration = 1; + this.frontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = simpleSelection(start); + this.history = new History(null); + this.id = ++nextDocId; + this.modeOption = mode; + + if (typeof text == "string") text = splitLines(text); + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). + iter: function(from, to, op) { + if (op) this.iterN(from - this.first, to - from, op); + else this.iterN(this.first, this.first + this.size, from); + }, + + // Non-public interface for adding and removing lines. + insert: function(at, lines) { + var height = 0; + for (var i = 0; i < lines.length; ++i) height += lines[i].height; + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + setValue: docMethodOp(function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: splitLines(code), origin: "setValue"}, true); + setSelection(this, simpleSelection(top)); + }), + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, + + getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, + getLineNumber: function(line) {return lineNo(line);}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") line = getLine(this, line); + return visualLine(line); + }, + + lineCount: function() {return this.size;}, + firstLine: function() {return this.first;}, + lastLine: function() {return this.first + this.size - 1;}, + + clipPos: function(pos) {return clipPos(this, pos);}, + + getCursor: function(start) { + var range = this.sel.primary(), pos; + if (start == null || start == "head") pos = range.head; + else if (start == "anchor") pos = range.anchor; + else if (start == "end" || start == "to" || start === false) pos = range.to(); + else pos = range.from(); + return pos; + }, + listSelections: function() { return this.sel.ranges; }, + somethingSelected: function() {return this.sel.somethingSelected();}, + + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); + }), + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); + }), + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads, options)); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + extendSelections(this, map(this.sel.ranges, f), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + if (!ranges.length) return; + for (var i = 0, out = []; i < ranges.length; i++) + out[i] = new Range(clipPos(this, ranges[i].anchor), + clipPos(this, ranges[i].head)); + if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex); + setSelection(this, normalizeSelection(out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(ranges, ranges.length - 1), options); + }), + + getSelection: function(lineSep) { + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) return lines; + else return lines.join(lineSep || "\n"); + }, + getSelections: function(lineSep) { + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) sel = sel.join(lineSep || "\n"); + parts[i] = sel; + } + return parts; + }, + replaceSelection: function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + dup[i] = code; + this.replaceSelections(dup, collapse, origin || "+input"); + }, + replaceSelections: docMethodOp(function(code, collapse, origin) { + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i = changes.length - 1; i >= 0; i--) + makeChange(this, changes[i]); + if (newSel) setSelectionReplaceHistory(this, newSel); + else if (this.cm) ensureCursorVisible(this.cm); + }), + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), + + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend;}, + + historySize: function() { + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done; + for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone; + return {undo: done, redo: undone}; + }, + clearHistory: function() {this.history = new History(this.history.maxGeneration);}, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; + return this.history.generation; + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration); + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)}; + }, + setHistory: function(histData) { + var hist = this.history = new History(this.history.maxGeneration); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); + }, + + addLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, "class", function(line) { + var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass"; + if (!line[prop]) line[prop] = cls; + else if (new RegExp("(?:^|\\s)" + cls + "(?:$|\\s)").test(line[prop])) return false; + else line[prop] += " " + cls; + return true; + }); + }), + removeLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, "class", function(line) { + var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) return false; + else if (cls == null) line[prop] = null; + else { + var found = cur.match(new RegExp("(?:^|\\s+)" + cls + "(?:$|\\s+)")); + if (!found) return false; + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true; + }); + }), + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false, shared: options && options.shared}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark"); + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + markers.push(span.marker.parent || span.marker); + } + return markers; + }, + findMarks: function(from, to, filter) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo = from.line; + this.iter(from.line, to.line + 1, function(line) { + var spans = line.markedSpans; + if (spans) for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(lineNo == from.line && from.ch > span.to || + span.from == null && lineNo != from.line|| + lineNo == to.line && span.from > to.ch) && + (!filter || filter(span.marker))) + found.push(span.marker.parent || span.marker); + } + ++lineNo; + }); + return found; + }, + getAllMarks: function() { + var markers = []; + this.iter(function(line) { + var sps = line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) + if (sps[i].from != null) markers.push(sps[i].marker); + }); + return markers; + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first; + this.iter(function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)); + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) return 0; + this.iter(this.first, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = this.sel; + doc.extend = false; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc; + }, + + linkedDoc: function(options) { + if (!options) options = {}; + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) from = options.from; + if (options.to != null && options.to < to) to = options.to; + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); + if (options.sharedHist) copy.history = this.history; + (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + copySharedMarkers(copy, findSharedMarkers(this)); + return copy; + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) other = other.doc; + if (this.linked) for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) continue; + this.linked.splice(i, 1); + other.unlinkDoc(this); + detachSharedMarkers(findSharedMarkers(this)); + break; + } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); + other.history = new History(null); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode;}, + getEditor: function() {return this.cm;} + }); + + // Public alias. + Doc.prototype.eachLine = Doc.prototype.iter; + + // Set up methods on CodeMirror's prototype to redirect to the editor's document. + var dontDelegate = "iter insert remove copy getEditor".split(" "); + for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments);}; + })(Doc.prototype[prop]); + + eventMixin(Doc); + + // Call f for all linked documents. + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) continue; + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) continue; + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } + } + propagate(doc, null, true); + } + + // Attach a document to an editor. + function attachDoc(cm, doc) { + if (doc.cm) throw new Error("This document is already in use."); + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + if (!cm.options.lineWrapping) findMaxLine(cm); + cm.options.mode = doc.modeOption; + regChange(cm); + } + + // LINE UTILITIES + + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document."); + for (var chunk = doc; !chunk.lines;) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + + // Get the part of a document between two positions, as an array of + // strings. + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function(line) { + var text = line.text; + if (n == end.line) text = text.slice(0, end.ch); + if (n == start.line) text = text.slice(start.ch); + out.push(text); + ++n; + }); + return out; + } + // Get the lines between from and to, as array of strings. + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function(line) { out.push(line.text); }); + return out; + } + + // Update the height of a line, propagating the height change + // upwards to parent nodes. + function updateLineHeight(line, height) { + var diff = height - line.height; + if (diff) for (var n = line; n; n = n.parent) n.height += diff; + } + + // Given a line object, find its line number by walking up through + // its parent links. + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first; + } + + // Find the line at the given vertical position, using the height + // information in the document tree. + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i = 0; i < chunk.children.length; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) break; + else h += line.height; + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i = 0; i < p.children.length; ++i) { + var cur = p.children[i]; + if (cur == chunk) break; + else h += cur.height; + } + } + return h; + } + + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. + function getOrder(line) { + var order = line.order; + if (order == null) order = line.order = bidiOrdering(line.text); + return order; + } + + // HISTORY + + function History(startGen) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = this.lastSelOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = startGen || 1; + } + + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); + return histChange; + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) array.pop(); + else break; + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done); + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done); + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done); + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, ore are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + var last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + pushSelectionToHistory(doc.sel, hist.done); + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) hist.done.shift(); + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = hist.lastSelOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) signal(doc, "historyAdded"); + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500); + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastSelOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + hist.done[hist.done.length - 1] = sel; + else + pushSelectionToHistory(sel, hist.done); + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastSelOp = opId; + if (options && options.clearRedo !== false) + clearSelectionEvents(hist.undone); + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + dest.push(sel); + } + + // Used to store marked span information in the history. + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { + if (line.markedSpans) + (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; + ++n; + }); + } + + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. + function removeClearedSpans(spans) { + if (!spans) return null; + for (var i = 0, out; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } + else if (out) out.push(spans[i]); + } + return !out ? spans : out.length ? out : null; + } + + // Retrieve and filter the old marked spans stored in a change event. + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) return null; + for (var i = 0, nw = []; i < change.text.length; ++i) + nw.push(removeClearedSpans(found[i])); + return nw; + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup, instantiateSel) { + for (var i = 0, copy = []; i < events.length; ++i) { + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue; + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m; + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } + } + } + return copy; + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSelSingle(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue; + } + for (var j = 0; j < sub.changes.length; ++j) { + var cur = sub.changes[j]; + if (to < cur.from.line) { + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); + } else if (from <= cur.to.line) { + ok = false; + break; + } + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // EVENT UTILITIES + + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. + + var e_preventDefault = CodeMirror.e_preventDefault = function(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + }; + var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + }; + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; + } + var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);}; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) b = 1; + else if (e.button & 2) b = 3; + else if (e.button & 4) b = 2; + } + if (mac && e.ctrlKey && b == 1) b = 3; + return b; + } + + // EVENT HANDLING + + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var on = CodeMirror.on = function(emitter, type, f) { + if (emitter.addEventListener) + emitter.addEventListener(type, f, false); + else if (emitter.attachEvent) + emitter.attachEvent("on" + type, f); + else { + var map = emitter._handlers || (emitter._handlers = {}); + var arr = map[type] || (map[type] = []); + arr.push(f); + } + }; + + var off = CodeMirror.off = function(emitter, type, f) { + if (emitter.removeEventListener) + emitter.removeEventListener(type, f, false); + else if (emitter.detachEvent) + emitter.detachEvent("on" + type, f); + else { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + for (var i = 0; i < arr.length; ++i) + if (arr[i] == f) { arr.splice(i, 1); break; } + } + }; + + var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); + }; + + var orphanDelayedCallbacks = null; + + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. + function signalLater(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2), list; + if (operationGroup) { + list = operationGroup.delayedCallbacks; + } else if (orphanDelayedCallbacks) { + list = orphanDelayedCallbacks; + } else { + list = orphanDelayedCallbacks = []; + setTimeout(fireOrphanDelayed, 0); + } + function bnd(f) {return function(){f.apply(null, args);};}; + for (var i = 0; i < arr.length; ++i) + list.push(bnd(arr[i])); + } + + function fireOrphanDelayed() { + var delayed = orphanDelayedCallbacks; + orphanDelayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore; + } + + function signalCursorActivity(cm) { + var arr = cm._handlers && cm._handlers.cursorActivity; + if (!arr) return; + var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); + for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1) + set.push(arr[i]); + } + + function hasHandler(emitter, type) { + var arr = emitter._handlers && emitter._handlers[type]; + return arr && arr.length > 0; + } + + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // MISC UTILITIES + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerCutOff = 30; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + + function Delayed() {this.id = null;} + Delayed.prototype.set = function(ms, f) { + clearTimeout(this.id); + this.id = setTimeout(f, ms); + }; + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + return n + (end - i); + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + }; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + function findColumn(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) nextTab = string.length; + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + return pos + Math.min(skipped, goal - col); + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) return pos; + } + } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + spaceStrs.push(lst(spaceStrs) + " "); + return spaceStrs[n]; + } + + function lst(arr) { return arr[arr.length-1]; } + + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; + else if (ie) // Suppress mysterious IE10 errors + selectInput = function(node) { try { node.select(); } catch(_e) {} }; + + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + if (array[i] == elt) return i; + return -1; + } + if ([].indexOf) indexOf = function(array, elt) { return array.indexOf(elt); }; + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) out[i] = f(array[i], i); + return out; + } + if ([].map) map = function(array, f) { return array.map(f); }; + + function createObj(base, props) { + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + var ctor = function() {}; + ctor.prototype = base; + inst = new ctor(); + } + if (props) copyObj(props, inst); + return inst; + }; + + function copyObj(obj, target, overwrite) { + if (!target) target = {}; + for (var prop in obj) + if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + target[prop] = obj[prop]; + return target; + } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args);}; + } + + var nonASCIISingleCaseWordChar = /[\u00df\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + var isWordCharBasic = CodeMirror.isWordChar = function(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); + }; + function isWordChar(ch, helper) { + if (!helper) return isWordCharBasic(ch); + if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true; + return helper.test(ch); + } + + function isEmpty(obj) { + for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; + return true; + } + + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } + + // DOM UTILITIES + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") e.appendChild(document.createTextNode(content)); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; + } + + var range; + if (document.createRange) range = function(node, start, end) { + var r = document.createRange(); + r.setEnd(node, end); + r.setStart(node, start); + return r; + }; + else range = function(node, start, end) { + var r = document.body.createTextRange(); + r.moveToElementText(node.parentNode); + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r; + }; + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + e.removeChild(e.firstChild); + return e; + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e); + } + + function contains(parent, child) { + if (parent.contains) + return parent.contains(child); + while (child = child.parentNode) + if (child == parent) return true; + } + + function activeElt() { return document.activeElement; } + // Older versions of IE throws unspecified error when touching + // document.activeElement in some cases (during loading, in iframe) + if (ie && ie_version < 11) activeElt = function() { + try { return document.activeElement; } + catch(e) { return document.body; } + }; + + function classTest(cls) { return new RegExp("\\b" + cls + "\\b\\s*"); } + function rmClass(node, cls) { + var test = classTest(cls); + if (test.test(node.className)) node.className = node.className.replace(test, ""); + } + function addClass(node, cls) { + if (!classTest(cls).test(node.className)) node.className += " " + cls; + } + function joinClasses(a, b) { + var as = a.split(" "); + for (var i = 0; i < as.length; i++) + if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i]; + return b; + } + + // WINDOW-WIDE EVENTS + + // These must be handled carefully, because naively registering a + // handler for each editor will cause the editors to never be + // garbage collected. + + function forEachCodeMirror(f) { + if (!document.body.getElementsByClassName) return; + var byClass = document.body.getElementsByClassName("CodeMirror"); + for (var i = 0; i < byClass.length; i++) { + var cm = byClass[i].CodeMirror; + if (cm) f(cm); + } + } + + var globalsRegistered = false; + function ensureGlobalHandlers() { + if (globalsRegistered) return; + registerGlobalHandlers(); + globalsRegistered = true; + } + function registerGlobalHandlers() { + // When the window resizes, we need to refresh active editors. + var resizeTimer; + on(window, "resize", function() { + if (resizeTimer == null) resizeTimer = setTimeout(function() { + resizeTimer = null; + knownScrollbarWidth = null; + forEachCodeMirror(onResize); + }, 100); + }); + // When the window loses focus, we want to show the editor as blurred + on(window, "blur", function() { + forEachCodeMirror(onBlur); + }); + } + + // FEATURE DETECTION + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie && ie_version < 9) return false; + var div = elt('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + var knownScrollbarWidth; + function scrollbarWidth(measure) { + if (knownScrollbarWidth != null) return knownScrollbarWidth; + var test = elt("div", null, null, "width: 50px; height: 50px; overflow-x: scroll"); + removeChildrenAndAdd(measure, test); + if (test.offsetWidth) + knownScrollbarWidth = test.offsetHeight - test.clientHeight; + return knownScrollbarWidth || 0; + } + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); + } + if (zwspSupported) return elt("span", "\u200b"); + else return elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + } + + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) return badBidiRects; + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780) + var r1 = range(txt, 1, 2).getBoundingClientRect(); + return badBidiRects = (r1.right - r0.right < 3); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) nl = string.length; + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result; + } : function(string){return string.split(/\r\n?|\n/);}; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + var hasCopyEvent = (function() { + var e = elt("div"); + if ("oncopy" in e) return true; + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == "function"; + })(); + + var badZoomedRects = null; + function hasBadZoomedRects(measure) { + if (badZoomedRects != null) return badZoomedRects; + var node = removeChildrenAndAdd(measure, elt("span", "x")); + var normal = node.getBoundingClientRect(); + var fromRange = range(node, 0, 1).getBoundingClientRect(); + return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1; + } + + // KEY NAMES + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) return f(from, to, "ltr"); + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); + found = true; + } + } + if (!found) f(from, to, "ltr"); + } + + function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } + function bidiRight(part) { return part.level % 2 ? part.from : part.to; } + + function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } + function lineRight(line) { + var order = getOrder(line); + if (!order) return line.text.length; + return bidiRight(lst(order)); + } + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(line); + if (visual != line) lineN = lineNo(visual); + var order = getOrder(visual); + var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); + return Pos(lineN, ch); + } + function lineEnd(cm, lineN) { + var merged, line = getLine(cm.doc, lineN); + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + lineN = null; + } + var order = getOrder(line); + var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); + return Pos(lineN == null ? lineNo(line) : lineN, ch); + } + function lineStartSmart(cm, pos) { + var start = lineStart(cm, pos.line); + var line = getLine(cm.doc, start.line); + var order = getOrder(line); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; + return Pos(start.line, inWS ? 0 : firstNonWS); + } + return start; + } + + function compareBidiLevel(order, a, b) { + var linedir = order[0].level; + if (a == linedir) return true; + if (b == linedir) return false; + return a < b; + } + var bidiOther; + function getBidiPartAt(order, pos) { + bidiOther = null; + for (var i = 0, found; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < pos && cur.to > pos) return i; + if ((cur.from == pos || cur.to == pos)) { + if (found == null) { + found = i; + } else if (compareBidiLevel(order, cur.level, order[found].level)) { + if (cur.from != cur.to) bidiOther = found; + return i; + } else { + if (cur.from != cur.to) bidiOther = i; + return found; + } + } + } + return found; + } + + function moveInLine(line, pos, dir, byUnit) { + if (!byUnit) return pos + dir; + do pos += dir; + while (pos > 0 && isExtendingChar(line.text.charAt(pos))); + return pos; + } + + // This is needed in order to move 'visually' through bi-directional + // text -- i.e., pressing left should make the cursor go left, even + // when in RTL text. The tricky part is the 'jumps', where RTL and + // LTR text touch each other. This often requires the cursor offset + // to move more than one unit, in order to visually move one unit. + function moveVisually(line, start, dir, byUnit) { + var bidi = getOrder(line); + if (!bidi) return moveLogically(line, start, dir, byUnit); + var pos = getBidiPartAt(bidi, start), part = bidi[pos]; + var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); + + for (;;) { + if (target > part.from && target < part.to) return target; + if (target == part.from || target == part.to) { + if (getBidiPartAt(bidi, target) == pos) return target; + part = bidi[pos += dir]; + return (dir > 0) == part.level % 2 ? part.to : part.from; + } else { + part = bidi[pos += dir]; + if (!part) return null; + if ((dir > 0) == part.level % 2) + target = moveInLine(line, part.to, -1, byUnit); + else + target = moveInLine(line, part.from, 1, byUnit); + } + } + } + + function moveLogically(line, start, dir, byUnit) { + var target = start + dir; + if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir; + return target < 0 || target > line.text.length ? null : target; + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; + // Character types for codepoints 0x600 to 0x6ff + var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm"; + function charType(code) { + if (code <= 0xf7) return lowTypes.charAt(code); + else if (0x590 <= code && code <= 0x5f4) return "R"; + else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600); + else if (0x6ee <= code && code <= 0x8ac) return "r"; + else if (0x2000 <= code && code <= 0x200b) return "w"; + else if (code == 0x200c) return "b"; + else return "L"; + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + // Browsers seem to always treat the boundaries of block elements as being L. + var outerType = "L"; + + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + + return function(str) { + if (!bidiRE.test(str)) return false; + var len = str.length, types = []; + for (var i = 0, type; i < len; ++i) + types.push(type = charType(str.charCodeAt(i))); + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i = 0, prev = outerType; i < len; ++i) { + var type = types[i]; + if (type == "m") types[i] = prev; + else prev = type; + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (type == "1" && cur == "r") types[i] = "n"; + else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i = 1, prev = types[0]; i < len - 1; ++i) { + var type = types[i]; + if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; + else if (type == "," && prev == types[i+1] && + (prev == "1" || prev == "n")) types[i] = prev; + prev = type; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i = 0; i < len; ++i) { + var type = types[i]; + if (type == ",") types[i] = "N"; + else if (type == "%") { + for (var end = i + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (cur == "L" && type == "1") types[i] = "L"; + else if (isStrong.test(type)) cur = type; + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i = 0; i < len; ++i) { + if (isNeutral.test(types[i])) { + for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} + var before = (i ? types[i-1] : outerType) == "L"; + var after = (end < len ? types[end] : outerType) == "L"; + var replace = before || after ? "L" : "R"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i = 0; i < len;) { + if (countsAsLeft.test(types[i])) { + var start = i; + for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} + order.push(new BidiSpan(0, start, i)); + } else { + var pos = i, at = order.length; + for (++i; i < len && types[i] != "L"; ++i) {} + for (var j = pos; j < i;) { + if (countsAsNum.test(types[j])) { + if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j)); + var nstart = j; + for (++j; j < i && countsAsNum.test(types[j]); ++j) {} + order.splice(at, 0, new BidiSpan(2, nstart, j)); + pos = j; + } else ++j; + } + if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i)); + } + } + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift(new BidiSpan(0, 0, m[0].length)); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push(new BidiSpan(0, len - m[0].length, len)); + } + if (order[0].level != lst(order).level) + order.push(new BidiSpan(order[0].level, len, len)); + + return order; + }; + })(); + + // THE END + + CodeMirror.version = "4.6.0"; + + return CodeMirror; +}); +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("xml", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var multilineTagIndentFactor = parserConfig.multilineTagIndentFactor || 1; + var multilineTagIndentPastTag = parserConfig.multilineTagIndentPastTag; + if (multilineTagIndentPastTag == null) multilineTagIndentPastTag = true; + + var Kludges = parserConfig.htmlMode ? { + autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, + 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, + 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, + 'track': true, 'wbr': true, 'menuitem': true}, + implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, + 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, + 'th': true, 'tr': true}, + contextGrabbers: { + 'dd': {'dd': true, 'dt': true}, + 'dt': {'dd': true, 'dt': true}, + 'li': {'li': true}, + 'option': {'option': true, 'optgroup': true}, + 'optgroup': {'optgroup': true}, + 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, + 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, + 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, + 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, + 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, + 'rp': {'rp': true, 'rt': true}, + 'rt': {'rp': true, 'rt': true}, + 'tbody': {'tbody': true, 'tfoot': true}, + 'td': {'td': true, 'th': true}, + 'tfoot': {'tbody': true}, + 'th': {'td': true, 'th': true}, + 'thead': {'tbody': true, 'tfoot': true}, + 'tr': {'tr': true} + }, + doNotIndent: {"pre": true}, + allowUnquoted: true, + allowMissing: true, + caseFold: true + } : { + autoSelfClosers: {}, + implicitlyClosed: {}, + contextGrabbers: {}, + doNotIndent: {}, + allowUnquoted: false, + allowMissing: false, + caseFold: false + }; + var alignCDATA = parserConfig.alignCDATA; + + // Return variables for tokenizers + var type, setStyle; + + function inText(stream, state) { + function chain(parser) { + state.tokenize = parser; + return parser(stream, state); + } + + var ch = stream.next(); + if (ch == "<") { + if (stream.eat("!")) { + if (stream.eat("[")) { + if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); + else return null; + } else if (stream.match("--")) { + return chain(inBlock("comment", "-->")); + } else if (stream.match("DOCTYPE", true, true)) { + stream.eatWhile(/[\w\._\-]/); + return chain(doctype(1)); + } else { + return null; + } + } else if (stream.eat("?")) { + stream.eatWhile(/[\w\._\-]/); + state.tokenize = inBlock("meta", "?>"); + return "meta"; + } else { + type = stream.eat("/") ? "closeTag" : "openTag"; + state.tokenize = inTag; + return "tag bracket"; + } + } else if (ch == "&") { + var ok; + if (stream.eat("#")) { + if (stream.eat("x")) { + ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";"); + } else { + ok = stream.eatWhile(/[\d]/) && stream.eat(";"); + } + } else { + ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";"); + } + return ok ? "atom" : "error"; + } else { + stream.eatWhile(/[^&<]/); + return null; + } + } + + function inTag(stream, state) { + var ch = stream.next(); + if (ch == ">" || (ch == "/" && stream.eat(">"))) { + state.tokenize = inText; + type = ch == ">" ? "endTag" : "selfcloseTag"; + return "tag bracket"; + } else if (ch == "=") { + type = "equals"; + return null; + } else if (ch == "<") { + state.tokenize = inText; + state.state = baseState; + state.tagName = state.tagStart = null; + var next = state.tokenize(stream, state); + return next ? next + " tag error" : "tag error"; + } else if (/[\'\"]/.test(ch)) { + state.tokenize = inAttribute(ch); + state.stringStartCol = stream.column(); + return state.tokenize(stream, state); + } else { + stream.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/); + return "word"; + } + } + + function inAttribute(quote) { + var closure = function(stream, state) { + while (!stream.eol()) { + if (stream.next() == quote) { + state.tokenize = inTag; + break; + } + } + return "string"; + }; + closure.isInAttribute = true; + return closure; + } + + function inBlock(style, terminator) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.match(terminator)) { + state.tokenize = inText; + break; + } + stream.next(); + } + return style; + }; + } + function doctype(depth) { + return function(stream, state) { + var ch; + while ((ch = stream.next()) != null) { + if (ch == "<") { + state.tokenize = doctype(depth + 1); + return state.tokenize(stream, state); + } else if (ch == ">") { + if (depth == 1) { + state.tokenize = inText; + break; + } else { + state.tokenize = doctype(depth - 1); + return state.tokenize(stream, state); + } + } + } + return "meta"; + }; + } + + function Context(state, tagName, startOfLine) { + this.prev = state.context; + this.tagName = tagName; + this.indent = state.indented; + this.startOfLine = startOfLine; + if (Kludges.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent)) + this.noIndent = true; + } + function popContext(state) { + if (state.context) state.context = state.context.prev; + } + function maybePopContext(state, nextTagName) { + var parentTagName; + while (true) { + if (!state.context) { + return; + } + parentTagName = state.context.tagName; + if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) || + !Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { + return; + } + popContext(state); + } + } + + function baseState(type, stream, state) { + if (type == "openTag") { + state.tagStart = stream.column(); + return tagNameState; + } else if (type == "closeTag") { + return closeTagNameState; + } else { + return baseState; + } + } + function tagNameState(type, stream, state) { + if (type == "word") { + state.tagName = stream.current(); + setStyle = "tag"; + return attrState; + } else { + setStyle = "error"; + return tagNameState; + } + } + function closeTagNameState(type, stream, state) { + if (type == "word") { + var tagName = stream.current(); + if (state.context && state.context.tagName != tagName && + Kludges.implicitlyClosed.hasOwnProperty(state.context.tagName)) + popContext(state); + if (state.context && state.context.tagName == tagName) { + setStyle = "tag"; + return closeState; + } else { + setStyle = "tag error"; + return closeStateErr; + } + } else { + setStyle = "error"; + return closeStateErr; + } + } + + function closeState(type, _stream, state) { + if (type != "endTag") { + setStyle = "error"; + return closeState; + } + popContext(state); + return baseState; + } + function closeStateErr(type, stream, state) { + setStyle = "error"; + return closeState(type, stream, state); + } + + function attrState(type, _stream, state) { + if (type == "word") { + setStyle = "attribute"; + return attrEqState; + } else if (type == "endTag" || type == "selfcloseTag") { + var tagName = state.tagName, tagStart = state.tagStart; + state.tagName = state.tagStart = null; + if (type == "selfcloseTag" || + Kludges.autoSelfClosers.hasOwnProperty(tagName)) { + maybePopContext(state, tagName); + } else { + maybePopContext(state, tagName); + state.context = new Context(state, tagName, tagStart == state.indented); + } + return baseState; + } + setStyle = "error"; + return attrState; + } + function attrEqState(type, stream, state) { + if (type == "equals") return attrValueState; + if (!Kludges.allowMissing) setStyle = "error"; + return attrState(type, stream, state); + } + function attrValueState(type, stream, state) { + if (type == "string") return attrContinuedState; + if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return attrState;} + setStyle = "error"; + return attrState(type, stream, state); + } + function attrContinuedState(type, stream, state) { + if (type == "string") return attrContinuedState; + return attrState(type, stream, state); + } + + return { + startState: function() { + return {tokenize: inText, + state: baseState, + indented: 0, + tagName: null, tagStart: null, + context: null}; + }, + + token: function(stream, state) { + if (!state.tagName && stream.sol()) + state.indented = stream.indentation(); + + if (stream.eatSpace()) return null; + type = null; + var style = state.tokenize(stream, state); + if ((style || type) && style != "comment") { + setStyle = null; + state.state = state.state(type || style, stream, state); + if (setStyle) + style = setStyle == "error" ? style + " error" : setStyle; + } + return style; + }, + + indent: function(state, textAfter, fullLine) { + var context = state.context; + // Indent multi-line strings (e.g. css). + if (state.tokenize.isInAttribute) { + if (state.tagStart == state.indented) + return state.stringStartCol + 1; + else + return state.indented + indentUnit; + } + if (context && context.noIndent) return CodeMirror.Pass; + if (state.tokenize != inTag && state.tokenize != inText) + return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; + // Indent the starts of attribute names. + if (state.tagName) { + if (multilineTagIndentPastTag) + return state.tagStart + state.tagName.length + 2; + else + return state.tagStart + indentUnit * multilineTagIndentFactor; + } + if (alignCDATA && /<!\[CDATA\[/.test(textAfter)) return 0; + var tagAfter = textAfter && /^<(\/)?([\w_:\.-]*)/.exec(textAfter); + if (tagAfter && tagAfter[1]) { // Closing tag spotted + while (context) { + if (context.tagName == tagAfter[2]) { + context = context.prev; + break; + } else if (Kludges.implicitlyClosed.hasOwnProperty(context.tagName)) { + context = context.prev; + } else { + break; + } + } + } else if (tagAfter) { // Opening tag spotted + while (context) { + var grabbers = Kludges.contextGrabbers[context.tagName]; + if (grabbers && grabbers.hasOwnProperty(tagAfter[2])) + context = context.prev; + else + break; + } + } + while (context && !context.startOfLine) + context = context.prev; + if (context) return context.indent + indentUnit; + else return 0; + }, + + electricInput: /<\/[\s\w:]+>$/, + blockCommentStart: "<!--", + blockCommentEnd: "-->", + + configuration: parserConfig.htmlMode ? "html" : "xml", + helperType: parserConfig.htmlMode ? "html" : "xml" + }; +}); + +CodeMirror.defineMIME("text/xml", "xml"); +CodeMirror.defineMIME("application/xml", "xml"); +if (!CodeMirror.mimeModes.hasOwnProperty("text/html")) + CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); + +}); /*! Chosen, a Select Box Enhancer for jQuery and Prototype by Patrick Filler for Harvest, http://getharvest.com Version 1.1.0 @@ -26312,10 +28892,5349 @@ return Chosen; })(AbstractChosen); }).call(this); +/** + * Super simple wysiwyg editor on Bootstrap v0.5.9 + * http://hackerwins.github.io/summernote/ + * + * summernote.js + * Copyright 2013-2014 Alan Hong. and other contributors + * summernote may be freely distributed under the MIT license./ + * + * Date: 2014-09-24T15:46Z + */ + +(function (factory) { + /* global define */ + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals: jQuery + factory(window.jQuery); + } +}(function ($) { + + + + if ('function' !== typeof Array.prototype.reduce) { + /** + * Array.prototype.reduce fallback + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce + */ + Array.prototype.reduce = function (callback, optInitialValue) { + var idx, value, length = this.length >>> 0, isValueSet = false; + if (1 < arguments.length) { + value = optInitialValue; + isValueSet = true; + } + for (idx = 0; length > idx; ++idx) { + if (this.hasOwnProperty(idx)) { + if (isValueSet) { + value = callback(value, this[idx], idx, this); + } else { + value = this[idx]; + isValueSet = true; + } + } + } + if (!isValueSet) { + throw new TypeError('Reduce of empty array with no initial value'); + } + return value; + }; + } + + if ('function' !== typeof Array.prototype.filter) { + Array.prototype.filter = function (fun/*, thisArg*/) { + if (this === void 0 || this === null) { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fun !== 'function') { + throw new TypeError(); + } + + var res = []; + var thisArg = arguments.length >= 2 ? arguments[1] : void 0; + for (var i = 0; i < len; i++) { + if (i in t) { + var val = t[i]; + if (fun.call(thisArg, val, i, t)) { + res.push(val); + } + } + } + + return res; + }; + } + + var isSupportAmd = typeof define === 'function' && define.amd; + + /** + * returns whether font is installed or not. + * @param {String} fontName + * @return {Boolean} + */ + var isFontInstalled = function (fontName) { + var testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS'; + var $tester = $('<div>').css({ + position: 'absolute', + left: '-9999px', + top: '-9999px', + fontSize: '200px' + }).text('mmmmmmmmmwwwwwww').appendTo(document.body); + + var originalWidth = $tester.css('fontFamily', testFontName).width(); + var width = $tester.css('fontFamily', fontName + ',' + testFontName).width(); + + $tester.remove(); + + return originalWidth !== width; + }; + + /** + * Object which check platform and agent + */ + var agent = { + isMac: navigator.appVersion.indexOf('Mac') > -1, + isMSIE: navigator.userAgent.indexOf('MSIE') > -1 || navigator.userAgent.indexOf('Trident') > -1, + isFF: navigator.userAgent.indexOf('Firefox') > -1, + jqueryVersion: parseFloat($.fn.jquery), + isSupportAmd: isSupportAmd, + hasCodeMirror: isSupportAmd ? require.specified('CodeMirror') : !!window.CodeMirror, + isFontInstalled: isFontInstalled, + isW3CRangeSupport: !!document.createRange + }; + + /** + * func utils (for high-order func's arg) + */ + var func = (function () { + var eq = function (itemA) { + return function (itemB) { + return itemA === itemB; + }; + }; + + var eq2 = function (itemA, itemB) { + return itemA === itemB; + }; + + var peq2 = function (propName) { + return function (itemA, itemB) { + return itemA[propName] === itemB[propName]; + }; + }; + + var ok = function () { + return true; + }; + + var fail = function () { + return false; + }; + + var not = function (f) { + return function () { + return !f.apply(f, arguments); + }; + }; + + var and = function (fA, fB) { + return function (item) { + return fA(item) && fB(item); + }; + }; + + var self = function (a) { + return a; + }; + + var idCounter = 0; + + /** + * generate a globally-unique id + * + * @param {String} [prefix] + */ + var uniqueId = function (prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + /** + * returns bnd (bounds) from rect + * + * - IE Compatability Issue: http://goo.gl/sRLOAo + * - Scroll Issue: http://goo.gl/sNjUc + * + * @param {Rect} rect + * @return {Object} bounds + * @return {Number} bounds.top + * @return {Number} bounds.left + * @return {Number} bounds.width + * @return {Number} bounds.height + */ + var rect2bnd = function (rect) { + var $document = $(document); + return { + top: rect.top + $document.scrollTop(), + left: rect.left + $document.scrollLeft(), + width: rect.right - rect.left, + height: rect.bottom - rect.top + }; + }; + + /** + * returns a copy of the object where the keys have become the values and the values the keys. + * @param {Object} obj + * @return {Object} + */ + var invertObject = function (obj) { + var inverted = {}; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + inverted[obj[key]] = key; + } + } + return inverted; + }; + + return { + eq: eq, + eq2: eq2, + peq2: peq2, + ok: ok, + fail: fail, + self: self, + not: not, + and: and, + uniqueId: uniqueId, + rect2bnd: rect2bnd, + invertObject: invertObject + }; + })(); + + /** + * list utils + */ + var list = (function () { + /** + * returns the first item of an array. + * + * @param {Array} array + */ + var head = function (array) { + return array[0]; + }; + + /** + * returns the last item of an array. + * + * @param {Array} array + */ + var last = function (array) { + return array[array.length - 1]; + }; + + /** + * returns everything but the last entry of the array. + * + * @param {Array} array + */ + var initial = function (array) { + return array.slice(0, array.length - 1); + }; + + /** + * returns the rest of the items in an array. + * + * @param {Array} array + */ + var tail = function (array) { + return array.slice(1); + }; + + /** + * returns item of array + */ + var find = function (array, pred) { + for (var idx = 0, len = array.length; idx < len; idx ++) { + var item = array[idx]; + if (pred(item)) { + return item; + } + } + }; + + /** + * returns true if all of the values in the array pass the predicate truth test. + */ + var all = function (array, pred) { + for (var idx = 0, len = array.length; idx < len; idx ++) { + if (!pred(array[idx])) { + return false; + } + } + return true; + }; + + /** + * returns true if the value is present in the list. + */ + var contains = function (array, item) { + return array.indexOf(item) !== -1; + }; + + /** + * get sum from a list + * + * @param {Array} array - array + * @param {Function} fn - iterator + */ + var sum = function (array, fn) { + fn = fn || func.self; + return array.reduce(function (memo, v) { + return memo + fn(v); + }, 0); + }; + + /** + * returns a copy of the collection with array type. + * @param {Collection} collection - collection eg) node.childNodes, ... + */ + var from = function (collection) { + var result = [], idx = -1, length = collection.length; + while (++idx < length) { + result[idx] = collection[idx]; + } + return result; + }; + + /** + * cluster elements by predicate function. + * + * @param {Array} array - array + * @param {Function} fn - predicate function for cluster rule + * @param {Array[]} + */ + var clusterBy = function (array, fn) { + if (!array.length) { return []; } + var aTail = tail(array); + return aTail.reduce(function (memo, v) { + var aLast = last(memo); + if (fn(last(aLast), v)) { + aLast[aLast.length] = v; + } else { + memo[memo.length] = [v]; + } + return memo; + }, [[head(array)]]); + }; + + /** + * returns a copy of the array with all falsy values removed + * + * @param {Array} array - array + * @param {Function} fn - predicate function for cluster rule + */ + var compact = function (array) { + var aResult = []; + for (var idx = 0, len = array.length; idx < len; idx ++) { + if (array[idx]) { aResult.push(array[idx]); } + } + return aResult; + }; + + /** + * produces a duplicate-free version of the array + * + * @param {Array} array + */ + var unique = function (array) { + var results = []; + + for (var idx = 0, len = array.length; idx < len; idx ++) { + if (results.indexOf(array[idx]) === -1) { + results.push(array[idx]); + } + } + + return results; + }; + + return { head: head, last: last, initial: initial, tail: tail, + find: find, contains: contains, + all: all, sum: sum, from: from, + clusterBy: clusterBy, compact: compact, unique: unique }; + })(); + + + var NBSP_CHAR = String.fromCharCode(160); + var ZERO_WIDTH_NBSP_CHAR = '\ufeff'; + + /** + * Dom functions + */ + var dom = (function () { + /** + * returns whether node is `note-editable` or not. + * + * @param {Node} node + * @return {Boolean} + */ + var isEditable = function (node) { + return node && $(node).hasClass('note-editable'); + }; + + /** + * returns whether node is `note-control-sizing` or not. + * + * @param {Node} node + * @return {Boolean} + */ + var isControlSizing = function (node) { + return node && $(node).hasClass('note-control-sizing'); + }; + + /** + * build layoutInfo from $editor(.note-editor) + * + * @param {jQuery} $editor + * @return {Object} + */ + var buildLayoutInfo = function ($editor) { + var makeFinder; + + // air mode + if ($editor.hasClass('note-air-editor')) { + var id = list.last($editor.attr('id').split('-')); + makeFinder = function (sIdPrefix) { + return function () { return $(sIdPrefix + id); }; + }; + + return { + editor: function () { return $editor; }, + editable: function () { return $editor; }, + popover: makeFinder('#note-popover-'), + handle: makeFinder('#note-handle-'), + dialog: makeFinder('#note-dialog-') + }; + + // frame mode + } else { + makeFinder = function (sClassName) { + return function () { return $editor.find(sClassName); }; + }; + return { + editor: function () { return $editor; }, + dropzone: makeFinder('.note-dropzone'), + toolbar: makeFinder('.note-toolbar'), + editable: makeFinder('.note-editable'), + codable: makeFinder('.note-codable'), + statusbar: makeFinder('.note-statusbar'), + popover: makeFinder('.note-popover'), + handle: makeFinder('.note-handle'), + dialog: makeFinder('.note-dialog') + }; + } + }; + + /** + * returns predicate which judge whether nodeName is same + * + * @param {String} nodeName + * @return {String} + */ + var makePredByNodeName = function (nodeName) { + nodeName = nodeName.toUpperCase(); + return function (node) { + return node && node.nodeName.toUpperCase() === nodeName; + }; + }; + + var isText = function (node) { + return node && node.nodeType === 3; + }; + + /** + * ex) br, col, embed, hr, img, input, ... + * @see http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements + */ + var isVoid = function (node) { + return node && /^BR|^IMG|^HR/.test(node.nodeName.toUpperCase()); + }; + + var isPara = function (node) { + if (isEditable(node)) { + return false; + } + + // Chrome(v31.0), FF(v25.0.1) use DIV for paragraph + return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase()); + }; + + var isLi = makePredByNodeName('LI'); + + var isPurePara = function (node) { + return isPara(node) && !isLi(node); + }; + + var isInline = function (node) { + return !isBodyContainer(node) && !isList(node) && !isPara(node); + }; + + var isList = function (node) { + return node && /^UL|^OL/.test(node.nodeName.toUpperCase()); + }; + + var isCell = function (node) { + return node && /^TD|^TH/.test(node.nodeName.toUpperCase()); + }; + + var isBlockquote = makePredByNodeName('BLOCKQUOTE'); + + var isBodyContainer = function (node) { + return isCell(node) || isBlockquote(node) || isEditable(node); + }; + + var isAnchor = makePredByNodeName('A'); + + var isParaInline = function (node) { + return isInline(node) && !!ancestor(node, isPara); + }; + + var isBodyInline = function (node) { + return isInline(node) && !ancestor(node, isPara); + }; + + var isBody = makePredByNodeName('BODY'); + + /** + * blank HTML for cursor position + */ + var blankHTML = agent.isMSIE ? '&nbsp;' : '<br>'; + + /** + * returns #text's text size or element's childNodes size + * + * @param {Node} node + */ + var nodeLength = function (node) { + if (isText(node)) { + return node.nodeValue.length; + } + + return node.childNodes.length; + }; + + /** + * returns whether node is empty or not. + * + * @param {Node} node + * @return {Boolean} + */ + var isEmpty = function (node) { + var len = nodeLength(node); + + if (len === 0) { + return true; + } else if (!dom.isText(node) && len === 1 && node.innerHTML === blankHTML) { + // ex) <p><br></p>, <span><br></span> + return true; + } + + return false; + }; + + /** + * padding blankHTML if node is empty (for cursor position) + */ + var paddingBlankHTML = function (node) { + if (!isVoid(node) && !nodeLength(node)) { + node.innerHTML = blankHTML; + } + }; + + /** + * find nearest ancestor predicate hit + * + * @param {Node} node + * @param {Function} pred - predicate function + */ + var ancestor = function (node, pred) { + while (node) { + if (pred(node)) { return node; } + if (isEditable(node)) { break; } + + node = node.parentNode; + } + return null; + }; + + /** + * returns new array of ancestor nodes (until predicate hit). + * + * @param {Node} node + * @param {Function} [optional] pred - predicate function + */ + var listAncestor = function (node, pred) { + pred = pred || func.fail; + + var ancestors = []; + ancestor(node, function (el) { + if (!isEditable(el)) { + ancestors.push(el); + } + + return pred(el); + }); + return ancestors; + }; + + /** + * find farthest ancestor predicate hit + */ + var lastAncestor = function (node, pred) { + var ancestors = listAncestor(node); + return list.last(ancestors.filter(pred)); + }; + + /** + * returns common ancestor node between two nodes. + * + * @param {Node} nodeA + * @param {Node} nodeB + */ + var commonAncestor = function (nodeA, nodeB) { + var ancestors = listAncestor(nodeA); + for (var n = nodeB; n; n = n.parentNode) { + if ($.inArray(n, ancestors) > -1) { return n; } + } + return null; // difference document area + }; + + /** + * listing all previous siblings (until predicate hit). + * + * @param {Node} node + * @param {Function} [optional] pred - predicate function + */ + var listPrev = function (node, pred) { + pred = pred || func.fail; + + var nodes = []; + while (node) { + if (pred(node)) { break; } + nodes.push(node); + node = node.previousSibling; + } + return nodes; + }; + + /** + * listing next siblings (until predicate hit). + * + * @param {Node} node + * @param {Function} [pred] - predicate function + */ + var listNext = function (node, pred) { + pred = pred || func.fail; + + var nodes = []; + while (node) { + if (pred(node)) { break; } + nodes.push(node); + node = node.nextSibling; + } + return nodes; + }; + + /** + * listing descendant nodes + * + * @param {Node} node + * @param {Function} [pred] - predicate function + */ + var listDescendant = function (node, pred) { + var descendents = []; + pred = pred || func.ok; + + // start DFS(depth first search) with node + (function fnWalk(current) { + if (node !== current && pred(current)) { + descendents.push(current); + } + for (var idx = 0, len = current.childNodes.length; idx < len; idx++) { + fnWalk(current.childNodes[idx]); + } + })(node); + + return descendents; + }; + + /** + * wrap node with new tag. + * + * @param {Node} node + * @param {Node} tagName of wrapper + * @return {Node} - wrapper + */ + var wrap = function (node, wrapperName) { + var parent = node.parentNode; + var wrapper = $('<' + wrapperName + '>')[0]; + + parent.insertBefore(wrapper, node); + wrapper.appendChild(node); + + return wrapper; + }; + + /** + * insert node after preceding + * + * @param {Node} node + * @param {Node} preceding - predicate function + */ + var insertAfter = function (node, preceding) { + var next = preceding.nextSibling, parent = preceding.parentNode; + if (next) { + parent.insertBefore(node, next); + } else { + parent.appendChild(node); + } + return node; + }; + + /** + * append elements. + * + * @param {Node} node + * @param {Collection} aChild + */ + var appendChildNodes = function (node, aChild) { + $.each(aChild, function (idx, child) { + node.appendChild(child); + }); + return node; + }; + + /** + * returns whether boundaryPoint is left edge or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isLeftEdgePoint = function (point) { + return point.offset === 0; + }; + + /** + * returns whether boundaryPoint is right edge or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isRightEdgePoint = function (point) { + return point.offset === nodeLength(point.node); + }; + + /** + * returns whether boundaryPoint is edge or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isEdgePoint = function (point) { + return isLeftEdgePoint(point) || isRightEdgePoint(point); + }; + + /** + * returns wheter node is left edge of ancestor or not. + * + * @param {Node} node + * @param {Node} ancestor + * @return {Boolean} + */ + var isLeftEdgeOf = function (node, ancestor) { + while (node && node !== ancestor) { + if (position(node) !== 0) { + return false; + } + node = node.parentNode; + } + + return true; + }; + + /** + * returns whether node is right edge of ancestor or not. + * + * @param {Node} node + * @param {Node} ancestor + * @return {Boolean} + */ + var isRightEdgeOf = function (node, ancestor) { + while (node && node !== ancestor) { + if (position(node) !== nodeLength(node.parentNode) - 1) { + return false; + } + node = node.parentNode; + } + + return true; + }; + + /** + * returns offset from parent. + * + * @param {Node} node + */ + var position = function (node) { + var offset = 0; + while ((node = node.previousSibling)) { + offset += 1; + } + return offset; + }; + + var hasChildren = function (node) { + return !!(node && node.childNodes && node.childNodes.length); + }; + + /** + * returns previous boundaryPoint + * + * @param {BoundaryPoint} point + * @param {Boolean} isSkipInnerOffset + * @return {BoundaryPoint} + */ + var prevPoint = function (point, isSkipInnerOffset) { + var node, offset; + + if (point.offset === 0) { + if (isEditable(point.node)) { + return null; + } + + node = point.node.parentNode; + offset = position(point.node); + } else if (hasChildren(point.node)) { + node = point.node.childNodes[point.offset - 1]; + offset = nodeLength(node); + } else { + node = point.node; + offset = isSkipInnerOffset ? 0 : point.offset - 1; + } + + return { + node: node, + offset: offset + }; + }; + + /** + * returns next boundaryPoint + * + * @param {BoundaryPoint} point + * @param {Boolean} isSkipInnerOffset + * @return {BoundaryPoint} + */ + var nextPoint = function (point, isSkipInnerOffset) { + var node, offset; + + if (nodeLength(point.node) === point.offset) { + if (isEditable(point.node)) { + return null; + } + + node = point.node.parentNode; + offset = position(point.node) + 1; + } else if (hasChildren(point.node)) { + node = point.node.childNodes[point.offset]; + offset = 0; + } else { + node = point.node; + offset = isSkipInnerOffset ? nodeLength(point.node) : point.offset + 1; + } + + return { + node: node, + offset: offset + }; + }; + + /** + * returns whether pointA and pointB is same or not. + * + * @param {BoundaryPoint} pointA + * @param {BoundaryPoint} pointB + * @return {Boolean} + */ + var isSamePoint = function (pointA, pointB) { + return pointA.node === pointB.node && pointA.offset === pointB.offset; + }; + + /** + * returns whether point is visible (can set cursor) or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isVisiblePoint = function (point) { + if (isText(point.node) || !hasChildren(point.node) || isEmpty(point.node)) { + return true; + } + + var leftNode = point.node.childNodes[point.offset - 1]; + var rightNode = point.node.childNodes[point.offset]; + if ((!leftNode || isVoid(leftNode)) && (!rightNode || isVoid(rightNode))) { + return true; + } + + return false; + }; + + /** + * @param {BoundaryPoint} point + * @param {Function} pred + * @return {BoundaryPoint} + */ + var prevPointUntil = function (point, pred) { + while (point) { + if (pred(point)) { + return point; + } + + point = prevPoint(point); + } + + return null; + }; + + /** + * @param {BoundaryPoint} point + * @param {Function} pred + * @return {BoundaryPoint} + */ + var nextPointUntil = function (point, pred) { + while (point) { + if (pred(point)) { + return point; + } + + point = nextPoint(point); + } + + return null; + }; + + /** + * @param {BoundaryPoint} startPoint + * @param {BoundaryPoint} endPoint + * @param {Function} handler + * @param {Boolean} isSkipInnerOffset + */ + var walkPoint = function (startPoint, endPoint, handler, isSkipInnerOffset) { + var point = startPoint; + + while (point) { + handler(point); + + if (isSamePoint(point, endPoint)) { + break; + } + + var isSkipOffset = isSkipInnerOffset && + startPoint.node !== point.node && + endPoint.node !== point.node; + point = nextPoint(point, isSkipOffset); + } + }; + + /** + * return offsetPath(array of offset) from ancestor + * + * @param {Node} ancestor - ancestor node + * @param {Node} node + */ + var makeOffsetPath = function (ancestor, node) { + var ancestors = listAncestor(node, func.eq(ancestor)); + return $.map(ancestors, position).reverse(); + }; + + /** + * return element from offsetPath(array of offset) + * + * @param {Node} ancestor - ancestor node + * @param {array} aOffset - offsetPath + */ + var fromOffsetPath = function (ancestor, aOffset) { + var current = ancestor; + for (var i = 0, len = aOffset.length; i < len; i++) { + if (current.childNodes.length <= aOffset[i]) { + current = current.childNodes[current.childNodes.length - 1]; + } else { + current = current.childNodes[aOffset[i]]; + } + } + return current; + }; + + /** + * split element or #text + * + * @param {BoundaryPoint} point + * @param {Boolean} [isSkipPaddingBlankHTML] + * @return {Node} right node of boundaryPoint + */ + var splitNode = function (point, isSkipPaddingBlankHTML) { + // split #text + if (isText(point.node)) { + // edge case + if (isLeftEdgePoint(point)) { + return point.node; + } else if (isRightEdgePoint(point)) { + return point.node.nextSibling; + } + + return point.node.splitText(point.offset); + } + + // split element + var childNode = point.node.childNodes[point.offset]; + var clone = insertAfter(point.node.cloneNode(false), point.node); + appendChildNodes(clone, listNext(childNode)); + + if (!isSkipPaddingBlankHTML) { + paddingBlankHTML(point.node); + paddingBlankHTML(clone); + } + + return clone; + }; + + /** + * split tree by point + * + * @param {Node} root - split root + * @param {BoundaryPoint} point + * @param {Boolean} [isSkipPaddingBlankHTML] + * @return {Node} right node of boundaryPoint + */ + var splitTree = function (root, point, isSkipPaddingBlankHTML) { + // ex) [#text, <span>, <p>] + var ancestors = listAncestor(point.node, func.eq(root)); + + if (!ancestors.length) { + return null; + } else if (ancestors.length === 1) { + return splitNode(point, isSkipPaddingBlankHTML); + } + + return ancestors.reduce(function (node, parent) { + var clone = insertAfter(parent.cloneNode(false), parent); + + if (node === point.node) { + node = splitNode(point, isSkipPaddingBlankHTML); + } + + appendChildNodes(clone, listNext(node)); + + if (!isSkipPaddingBlankHTML) { + paddingBlankHTML(parent); + paddingBlankHTML(clone); + } + return clone; + }); + }; + + var create = function (nodeName) { + return document.createElement(nodeName); + }; + + var createText = function (text) { + return document.createTextNode(text); + }; + + /** + * remove node, (isRemoveChild: remove child or not) + * @param {Node} node + * @param {Boolean} isRemoveChild + */ + var remove = function (node, isRemoveChild) { + if (!node || !node.parentNode) { return; } + if (node.removeNode) { return node.removeNode(isRemoveChild); } + + var parent = node.parentNode; + if (!isRemoveChild) { + var nodes = []; + var i, len; + for (i = 0, len = node.childNodes.length; i < len; i++) { + nodes.push(node.childNodes[i]); + } + + for (i = 0, len = nodes.length; i < len; i++) { + parent.insertBefore(nodes[i], node); + } + } + + parent.removeChild(node); + }; + + /** + * @param {Node} node + * @param {Function} pred + */ + var removeWhile = function (node, pred) { + while (node) { + if (isEditable(node) || !pred(node)) { + break; + } + + var parent = node.parentNode; + remove(node); + node = parent; + } + }; + + /** + * replace node with provided nodeName + * + * @param {Node} node + * @param {String} nodeName + * @return {Node} - new node + */ + var replace = function (node, nodeName) { + if (node.nodeName.toUpperCase() === nodeName.toUpperCase()) { + return node; + } + + var newNode = create(nodeName); + + if (node.style.cssText) { + newNode.style.cssText = node.style.cssText; + } + + appendChildNodes(newNode, list.from(node.childNodes)); + insertAfter(newNode, node); + remove(node); + + return newNode; + }; + + var isTextarea = makePredByNodeName('TEXTAREA'); + + /** + * get the HTML contents of node + * + * @param {jQuery} $node + * @param {Boolean} [isNewlineOnBlock] + */ + var html = function ($node, isNewlineOnBlock) { + var markup = isTextarea($node[0]) ? $node.val() : $node.html(); + + if (isNewlineOnBlock) { + var regexTag = /<(\/?)(\b(?!!)[^>\s]*)(.*?)(\s*\/?>)/g; + markup = markup.replace(regexTag, function (match, endSlash, name) { + name = name.toUpperCase(); + var isEndOfInlineContainer = /^DIV|^TD|^TH|^P|^LI|^H[1-7]/.test(name) && + !!endSlash; + var isBlockNode = /^BLOCKQUOTE|^TABLE|^TBODY|^TR|^HR|^UL|^OL/.test(name); + + return match + ((isEndOfInlineContainer || isBlockNode) ? '\n' : ''); + }); + markup = $.trim(markup); + } + + return markup; + }; + + var value = function ($textarea) { + var val = $textarea.val(); + // strip line breaks + return val.replace(/[\n\r]/g, ''); + }; + + return { + NBSP_CHAR: NBSP_CHAR, + ZERO_WIDTH_NBSP_CHAR: ZERO_WIDTH_NBSP_CHAR, + blank: blankHTML, + emptyPara: '<p>' + blankHTML + '</p>', + isEditable: isEditable, + isControlSizing: isControlSizing, + buildLayoutInfo: buildLayoutInfo, + isText: isText, + isPara: isPara, + isPurePara: isPurePara, + isInline: isInline, + isBodyInline: isBodyInline, + isBody: isBody, + isParaInline: isParaInline, + isList: isList, + isTable: makePredByNodeName('TABLE'), + isCell: isCell, + isBlockquote: isBlockquote, + isBodyContainer: isBodyContainer, + isAnchor: isAnchor, + isDiv: makePredByNodeName('DIV'), + isLi: isLi, + isSpan: makePredByNodeName('SPAN'), + isB: makePredByNodeName('B'), + isU: makePredByNodeName('U'), + isS: makePredByNodeName('S'), + isI: makePredByNodeName('I'), + isImg: makePredByNodeName('IMG'), + isTextarea: isTextarea, + isEmpty: isEmpty, + isEmptyAnchor: func.and(isAnchor, isEmpty), + nodeLength: nodeLength, + isLeftEdgePoint: isLeftEdgePoint, + isRightEdgePoint: isRightEdgePoint, + isEdgePoint: isEdgePoint, + isLeftEdgeOf: isLeftEdgeOf, + isRightEdgeOf: isRightEdgeOf, + prevPoint: prevPoint, + nextPoint: nextPoint, + isSamePoint: isSamePoint, + isVisiblePoint: isVisiblePoint, + prevPointUntil: prevPointUntil, + nextPointUntil: nextPointUntil, + walkPoint: walkPoint, + ancestor: ancestor, + listAncestor: listAncestor, + lastAncestor: lastAncestor, + listNext: listNext, + listPrev: listPrev, + listDescendant: listDescendant, + commonAncestor: commonAncestor, + wrap: wrap, + insertAfter: insertAfter, + appendChildNodes: appendChildNodes, + position: position, + hasChildren: hasChildren, + makeOffsetPath: makeOffsetPath, + fromOffsetPath: fromOffsetPath, + splitTree: splitTree, + create: create, + createText: createText, + remove: remove, + removeWhile: removeWhile, + replace: replace, + html: html, + value: value + }; + })(); + + var settings = { + // version + version: '0.5.9', + + /** + * options + */ + options: { + width: null, // set editor width + height: null, // set editor height, ex) 300 + + minHeight: null, // set minimum height of editor + maxHeight: null, // set maximum height of editor + + focus: false, // set focus to editable area after initializing summernote + + tabsize: 4, // size of tab ex) 2 or 4 + styleWithSpan: true, // style with span (Chrome and FF only) + + disableLinkTarget: false, // hide link Target Checkbox + disableDragAndDrop: false, // disable drag and drop event + disableResizeEditor: false, // disable resizing editor + + codemirror: { // codemirror options + mode: 'text/html', + htmlMode: true, + lineNumbers: true + }, + + // language + lang: 'en-US', // language 'en-US', 'ko-KR', ... + direction: null, // text direction, ex) 'rtl' + + // toolbar + toolbar: [ + ['style', ['style']], + ['font', ['bold', 'italic', 'underline', 'superscript', 'subscript', 'strikethrough', 'clear']], + ['fontname', ['fontname']], + // ['fontsize', ['fontsize']], // Still buggy + ['color', ['color']], + ['para', ['ul', 'ol', 'paragraph']], + ['height', ['height']], + ['table', ['table']], + ['insert', ['link', 'picture', 'video', 'hr']], + ['view', ['fullscreen', 'codeview']], + ['help', ['help']] + ], + + // air mode: inline editor + airMode: false, + // airPopover: [ + // ['style', ['style']], + // ['font', ['bold', 'italic', 'underline', 'clear']], + // ['fontname', ['fontname']], + // ['fontsize', ['fontsize']], // Still buggy + // ['color', ['color']], + // ['para', ['ul', 'ol', 'paragraph']], + // ['height', ['height']], + // ['table', ['table']], + // ['insert', ['link', 'picture', 'video']], + // ['help', ['help']] + // ], + airPopover: [ + ['color', ['color']], + ['font', ['bold', 'underline', 'clear']], + ['para', ['ul', 'paragraph']], + ['table', ['table']], + ['insert', ['link', 'picture']] + ], + + // style tag + styleTags: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + + // default fontName + defaultFontName: 'Helvetica Neue', + + // fontName + fontNames: [ + 'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', + 'Helvetica Neue', 'Impact', 'Lucida Grande', + 'Tahoma', 'Times New Roman', 'Verdana' + ], + + // pallete colors(n x n) + colors: [ + ['#000000', '#424242', '#636363', '#9C9C94', '#CEC6CE', '#EFEFEF', '#F7F7F7', '#FFFFFF'], + ['#FF0000', '#FF9C00', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF', '#9C00FF', '#FF00FF'], + ['#F7C6CE', '#FFE7CE', '#FFEFC6', '#D6EFD6', '#CEDEE7', '#CEE7F7', '#D6D6E7', '#E7D6DE'], + ['#E79C9C', '#FFC69C', '#FFE79C', '#B5D6A5', '#A5C6CE', '#9CC6EF', '#B5A5D6', '#D6A5BD'], + ['#E76363', '#F7AD6B', '#FFD663', '#94BD7B', '#73A5AD', '#6BADDE', '#8C7BC6', '#C67BA5'], + ['#CE0000', '#E79439', '#EFC631', '#6BA54A', '#4A7B8C', '#3984C6', '#634AA5', '#A54A7B'], + ['#9C0000', '#B56308', '#BD9400', '#397B21', '#104A5A', '#085294', '#311873', '#731842'], + ['#630000', '#7B3900', '#846300', '#295218', '#083139', '#003163', '#21104A', '#4A1031'] + ], + + // fontSize + fontSizes: ['8', '9', '10', '11', '12', '14', '18', '24', '36'], + + // lineHeight + lineHeights: ['1.0', '1.2', '1.4', '1.5', '1.6', '1.8', '2.0', '3.0'], + + // insertTable max size + insertTableMaxSize: { + col: 10, + row: 10 + }, + + // callbacks + oninit: null, // initialize + onfocus: null, // editable has focus + onblur: null, // editable out of focus + onenter: null, // enter key pressed + onkeyup: null, // keyup + onkeydown: null, // keydown + onImageUpload: null, // imageUpload + onImageUploadError: null, // imageUploadError + onToolbarClick: null, + + /** + * manipulate link address when user create link + * @param {String} sLinkUrl + * @return {String} + */ + onCreateLink: function (sLinkUrl) { + if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) { + sLinkUrl = 'mailto:' + sLinkUrl; + } else if (sLinkUrl.indexOf('://') === -1) { + sLinkUrl = 'http://' + sLinkUrl; + } + + return sLinkUrl; + }, + + keyMap: { + pc: { + 'ENTER': 'insertParagraph', + 'CTRL+Z': 'undo', + 'CTRL+Y': 'redo', + 'TAB': 'tab', + 'SHIFT+TAB': 'untab', + 'CTRL+B': 'bold', + 'CTRL+I': 'italic', + 'CTRL+U': 'underline', + 'CTRL+SHIFT+S': 'strikethrough', + 'CTRL+BACKSLASH': 'removeFormat', + 'CTRL+SHIFT+L': 'justifyLeft', + 'CTRL+SHIFT+E': 'justifyCenter', + 'CTRL+SHIFT+R': 'justifyRight', + 'CTRL+SHIFT+J': 'justifyFull', + 'CTRL+SHIFT+NUM7': 'insertUnorderedList', + 'CTRL+SHIFT+NUM8': 'insertOrderedList', + 'CTRL+LEFTBRACKET': 'outdent', + 'CTRL+RIGHTBRACKET': 'indent', + 'CTRL+NUM0': 'formatPara', + 'CTRL+NUM1': 'formatH1', + 'CTRL+NUM2': 'formatH2', + 'CTRL+NUM3': 'formatH3', + 'CTRL+NUM4': 'formatH4', + 'CTRL+NUM5': 'formatH5', + 'CTRL+NUM6': 'formatH6', + 'CTRL+ENTER': 'insertHorizontalRule', + 'CTRL+K': 'showLinkDialog' + }, + + mac: { + 'ENTER': 'insertParagraph', + 'CMD+Z': 'undo', + 'CMD+SHIFT+Z': 'redo', + 'TAB': 'tab', + 'SHIFT+TAB': 'untab', + 'CMD+B': 'bold', + 'CMD+I': 'italic', + 'CMD+U': 'underline', + 'CMD+SHIFT+S': 'strikethrough', + 'CMD+BACKSLASH': 'removeFormat', + 'CMD+SHIFT+L': 'justifyLeft', + 'CMD+SHIFT+E': 'justifyCenter', + 'CMD+SHIFT+R': 'justifyRight', + 'CMD+SHIFT+J': 'justifyFull', + 'CMD+SHIFT+NUM7': 'insertUnorderedList', + 'CMD+SHIFT+NUM8': 'insertOrderedList', + 'CMD+LEFTBRACKET': 'outdent', + 'CMD+RIGHTBRACKET': 'indent', + 'CMD+NUM0': 'formatPara', + 'CMD+NUM1': 'formatH1', + 'CMD+NUM2': 'formatH2', + 'CMD+NUM3': 'formatH3', + 'CMD+NUM4': 'formatH4', + 'CMD+NUM5': 'formatH5', + 'CMD+NUM6': 'formatH6', + 'CMD+ENTER': 'insertHorizontalRule', + 'CMD+K': 'showLinkDialog' + } + } + }, + + // default language: en-US + lang: { + 'en-US': { + font: { + bold: 'Bold', + italic: 'Italic', + underline: 'Underline', + strikethrough: 'Strikethrough', + subscript: 'Subscript', + superscript: 'Superscript', + clear: 'Remove Font Style', + height: 'Line Height', + name: 'Font Family', + size: 'Font Size' + }, + image: { + image: 'Picture', + insert: 'Insert Image', + resizeFull: 'Resize Full', + resizeHalf: 'Resize Half', + resizeQuarter: 'Resize Quarter', + floatLeft: 'Float Left', + floatRight: 'Float Right', + floatNone: 'Float None', + dragImageHere: 'Drag an image here', + selectFromFiles: 'Select from files', + url: 'Image URL', + remove: 'Remove Image' + }, + link: { + link: 'Link', + insert: 'Insert Link', + unlink: 'Unlink', + edit: 'Edit', + textToDisplay: 'Text to display', + url: 'To what URL should this link go?', + openInNewWindow: 'Open in new window' + }, + video: { + video: 'Video', + videoLink: 'Video Link', + insert: 'Insert Video', + url: 'Video URL?', + providers: '(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)' + }, + table: { + table: 'Table' + }, + hr: { + insert: 'Insert Horizontal Rule' + }, + style: { + style: 'Style', + normal: 'Normal', + blockquote: 'Quote', + pre: 'Code', + h1: 'Header 1', + h2: 'Header 2', + h3: 'Header 3', + h4: 'Header 4', + h5: 'Header 5', + h6: 'Header 6' + }, + lists: { + unordered: 'Unordered list', + ordered: 'Ordered list' + }, + options: { + help: 'Help', + fullscreen: 'Full Screen', + codeview: 'Code View' + }, + paragraph: { + paragraph: 'Paragraph', + outdent: 'Outdent', + indent: 'Indent', + left: 'Align left', + center: 'Align center', + right: 'Align right', + justify: 'Justify full' + }, + color: { + recent: 'Recent Color', + more: 'More Color', + background: 'Background Color', + foreground: 'Foreground Color', + transparent: 'Transparent', + setTransparent: 'Set transparent', + reset: 'Reset', + resetToDefault: 'Reset to default' + }, + shortcut: { + shortcuts: 'Keyboard shortcuts', + close: 'Close', + textFormatting: 'Text formatting', + action: 'Action', + paragraphFormatting: 'Paragraph formatting', + documentStyle: 'Document Style' + }, + history: { + undo: 'Undo', + redo: 'Redo' + } + } + } + }; + + /** + * Async functions which returns `Promise` + */ + var async = (function () { + /** + * read contents of file as representing URL + * + * @param {File} file + * @return {Promise} - then: sDataUrl + */ + var readFileAsDataURL = function (file) { + return $.Deferred(function (deferred) { + $.extend(new FileReader(), { + onload: function (e) { + var sDataURL = e.target.result; + deferred.resolve(sDataURL); + }, + onerror: function () { + deferred.reject(this); + } + }).readAsDataURL(file); + }).promise(); + }; + + /** + * create `<image>` from url string + * + * @param {String} sUrl + * @return {Promise} - then: $image + */ + var createImage = function (sUrl, filename) { + return $.Deferred(function (deferred) { + $('<img>').one('load', function () { + deferred.resolve($(this)); + }).one('error abort', function () { + deferred.reject($(this)); + }).css({ + display: 'none' + }).appendTo(document.body) + .attr('src', sUrl) + .attr('data-filename', filename); + }).promise(); + }; + + return { + readFileAsDataURL: readFileAsDataURL, + createImage: createImage + }; + })(); + + /** + * Object for keycodes. + */ + var key = { + isEdit: function (keyCode) { + return [8, 9, 13, 32].indexOf(keyCode) !== -1; + }, + nameFromCode: { + '8': 'BACKSPACE', + '9': 'TAB', + '13': 'ENTER', + '32': 'SPACE', + + // Number: 0-9 + '48': 'NUM0', + '49': 'NUM1', + '50': 'NUM2', + '51': 'NUM3', + '52': 'NUM4', + '53': 'NUM5', + '54': 'NUM6', + '55': 'NUM7', + '56': 'NUM8', + + // Alphabet: a-z + '66': 'B', + '69': 'E', + '73': 'I', + '74': 'J', + '75': 'K', + '76': 'L', + '82': 'R', + '83': 'S', + '85': 'U', + '89': 'Y', + '90': 'Z', + + '191': 'SLASH', + '219': 'LEFTBRACKET', + '220': 'BACKSLASH', + '221': 'RIGHTBRACKET' + } + }; + + /** + * Style + * @class + */ + var Style = function () { + /** + * passing an array of style properties to .css() + * will result in an object of property-value pairs. + * (compability with version < 1.9) + * + * @param {jQuery} $obj + * @param {Array} propertyNames - An array of one or more CSS properties. + * @returns {Object} + */ + var jQueryCSS = function ($obj, propertyNames) { + if (agent.jqueryVersion < 1.9) { + var result = {}; + $.each(propertyNames, function (idx, propertyName) { + result[propertyName] = $obj.css(propertyName); + }); + return result; + } + return $obj.css.call($obj, propertyNames); + }; + + /** + * paragraph level style + * + * @param {WrappedRange} rng + * @param {Object} styleInfo + */ + this.stylePara = function (rng, styleInfo) { + $.each(rng.nodes(dom.isPara, { + includeAncestor: true + }), function (idx, para) { + $(para).css(styleInfo); + }); + }; + + /** + * get current style on cursor + * + * @param {WrappedRange} rng + * @param {Node} target - target element on event + * @return {Object} - object contains style properties. + */ + this.current = function (rng, target) { + var $cont = $(dom.isText(rng.sc) ? rng.sc.parentNode : rng.sc); + var properties = ['font-family', 'font-size', 'text-align', 'list-style-type', 'line-height']; + var styleInfo = jQueryCSS($cont, properties) || {}; + + styleInfo['font-size'] = parseInt(styleInfo['font-size'], 10); + + // document.queryCommandState for toggle state + styleInfo['font-bold'] = document.queryCommandState('bold') ? 'bold' : 'normal'; + styleInfo['font-italic'] = document.queryCommandState('italic') ? 'italic' : 'normal'; + styleInfo['font-underline'] = document.queryCommandState('underline') ? 'underline' : 'normal'; + styleInfo['font-strikethrough'] = document.queryCommandState('strikeThrough') ? 'strikethrough' : 'normal'; + styleInfo['font-superscript'] = document.queryCommandState('superscript') ? 'superscript' : 'normal'; + styleInfo['font-subscript'] = document.queryCommandState('subscript') ? 'subscript' : 'normal'; + + // list-style-type to list-style(unordered, ordered) + if (!rng.isOnList()) { + styleInfo['list-style'] = 'none'; + } else { + var aOrderedType = ['circle', 'disc', 'disc-leading-zero', 'square']; + var isUnordered = $.inArray(styleInfo['list-style-type'], aOrderedType) > -1; + styleInfo['list-style'] = isUnordered ? 'unordered' : 'ordered'; + } + + var para = dom.ancestor(rng.sc, dom.isPara); + if (para && para.style['line-height']) { + styleInfo['line-height'] = para.style.lineHeight; + } else { + var lineHeight = parseInt(styleInfo['line-height'], 10) / parseInt(styleInfo['font-size'], 10); + styleInfo['line-height'] = lineHeight.toFixed(1); + } + + styleInfo.image = dom.isImg(target) && target; + styleInfo.anchor = rng.isOnAnchor() && dom.ancestor(rng.sc, dom.isAnchor); + styleInfo.ancestors = dom.listAncestor(rng.sc, dom.isEditable); + styleInfo.range = rng; + + return styleInfo; + }; + }; + + + /** + * Data structure + * - {BoundaryPoint}: a point of dom tree + * - {BoundaryPoints}: two boundaryPoints corresponding to the start and the end of the Range + * + * @see http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position + */ + var range = (function () { + + /** + * return boundaryPoint from TextRange, inspired by Andy Na's HuskyRange.js + * + * @param {TextRange} textRange + * @param {Boolean} isStart + * @return {BoundaryPoint} + * + * @see http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx + */ + var textRangeToPoint = function (textRange, isStart) { + var container = textRange.parentElement(), offset; + + var tester = document.body.createTextRange(), prevContainer; + var childNodes = list.from(container.childNodes); + for (offset = 0; offset < childNodes.length; offset++) { + if (dom.isText(childNodes[offset])) { + continue; + } + tester.moveToElementText(childNodes[offset]); + if (tester.compareEndPoints('StartToStart', textRange) >= 0) { + break; + } + prevContainer = childNodes[offset]; + } + + if (offset !== 0 && dom.isText(childNodes[offset - 1])) { + var textRangeStart = document.body.createTextRange(), curTextNode = null; + textRangeStart.moveToElementText(prevContainer || container); + textRangeStart.collapse(!prevContainer); + curTextNode = prevContainer ? prevContainer.nextSibling : container.firstChild; + + var pointTester = textRange.duplicate(); + pointTester.setEndPoint('StartToStart', textRangeStart); + var textCount = pointTester.text.replace(/[\r\n]/g, '').length; + + while (textCount > curTextNode.nodeValue.length && curTextNode.nextSibling) { + textCount -= curTextNode.nodeValue.length; + curTextNode = curTextNode.nextSibling; + } + + /* jshint ignore:start */ + var dummy = curTextNode.nodeValue; // enforce IE to re-reference curTextNode, hack + /* jshint ignore:end */ + + if (isStart && curTextNode.nextSibling && dom.isText(curTextNode.nextSibling) && + textCount === curTextNode.nodeValue.length) { + textCount -= curTextNode.nodeValue.length; + curTextNode = curTextNode.nextSibling; + } + + container = curTextNode; + offset = textCount; + } + + return { + cont: container, + offset: offset + }; + }; + + /** + * return TextRange from boundary point (inspired by google closure-library) + * @param {BoundaryPoint} point + * @return {TextRange} + */ + var pointToTextRange = function (point) { + var textRangeInfo = function (container, offset) { + var node, isCollapseToStart; + + if (dom.isText(container)) { + var prevTextNodes = dom.listPrev(container, func.not(dom.isText)); + var prevContainer = list.last(prevTextNodes).previousSibling; + node = prevContainer || container.parentNode; + offset += list.sum(list.tail(prevTextNodes), dom.nodeLength); + isCollapseToStart = !prevContainer; + } else { + node = container.childNodes[offset] || container; + if (dom.isText(node)) { + return textRangeInfo(node, 0); + } + + offset = 0; + isCollapseToStart = false; + } + + return { + node: node, + collapseToStart: isCollapseToStart, + offset: offset + }; + }; + + var textRange = document.body.createTextRange(); + var info = textRangeInfo(point.node, point.offset); + + textRange.moveToElementText(info.node); + textRange.collapse(info.collapseToStart); + textRange.moveStart('character', info.offset); + return textRange; + }; + + /** + * Wrapped Range + * + * @param {Node} sc - start container + * @param {Number} so - start offset + * @param {Node} ec - end container + * @param {Number} eo - end offset + */ + var WrappedRange = function (sc, so, ec, eo) { + this.sc = sc; + this.so = so; + this.ec = ec; + this.eo = eo; + + // nativeRange: get nativeRange from sc, so, ec, eo + var nativeRange = function () { + if (agent.isW3CRangeSupport) { + var w3cRange = document.createRange(); + w3cRange.setStart(sc, so); + w3cRange.setEnd(ec, eo); + + return w3cRange; + } else { + var textRange = pointToTextRange({ + node: sc, + offset: so + }); + + textRange.setEndPoint('EndToEnd', pointToTextRange({ + node: ec, + offset: eo + })); + + return textRange; + } + }; + + this.getPoints = function () { + return { + sc: sc, + so: so, + ec: ec, + eo: eo + }; + }; + + this.getStartPoint = function () { + return { + node: sc, + offset: so + }; + }; + + this.getEndPoint = function () { + return { + node: ec, + offset: eo + }; + }; + + /** + * select update visible range + */ + this.select = function () { + var nativeRng = nativeRange(); + if (agent.isW3CRangeSupport) { + var selection = document.getSelection(); + if (selection.rangeCount > 0) { + selection.removeAllRanges(); + } + selection.addRange(nativeRng); + } else { + nativeRng.select(); + } + }; + + /** + * @return {WrappedRange} + */ + this.normalize = function () { + var getVisiblePoint = function (point) { + if (!dom.isVisiblePoint(point)) { + if (dom.isLeftEdgePoint(point)) { + point = dom.nextPointUntil(point, dom.isVisiblePoint); + } else if (dom.isRightEdgePoint(point)) { + point = dom.prevPointUntil(point, dom.isVisiblePoint); + } + } + return point; + }; + + var startPoint = getVisiblePoint(this.getStartPoint()); + var endPoint = getVisiblePoint(this.getStartPoint()); + + return new WrappedRange( + startPoint.node, + startPoint.offset, + endPoint.node, + endPoint.offset + ); + }; + + /** + * returns matched nodes on range + * + * @param {Function} [pred] - predicate function + * @param {Object} [options] + * @param {Boolean} [options.includeAncestor] + * @param {Boolean} [options.fullyContains] + * @return {Node[]} + */ + this.nodes = function (pred, options) { + pred = pred || func.ok; + + var includeAncestor = options && options.includeAncestor; + var fullyContains = options && options.fullyContains; + + // TODO compare points and sort + var startPoint = this.getStartPoint(); + var endPoint = this.getEndPoint(); + + var nodes = []; + var leftEdgeNodes = []; + + dom.walkPoint(startPoint, endPoint, function (point) { + if (dom.isEditable(point.node)) { + return; + } + + var node; + if (fullyContains) { + if (dom.isLeftEdgePoint(point)) { + leftEdgeNodes.push(point.node); + } + if (dom.isRightEdgePoint(point) && list.contains(leftEdgeNodes, point.node)) { + node = point.node; + } + } else if (includeAncestor) { + node = dom.ancestor(point.node, pred); + } else { + node = point.node; + } + + if (node && pred(node)) { + nodes.push(node); + } + }, true); + + return list.unique(nodes); + }; + + /** + * returns commonAncestor of range + * @return {Element} - commonAncestor + */ + this.commonAncestor = function () { + return dom.commonAncestor(sc, ec); + }; + + /** + * returns expanded range by pred + * + * @param {Function} pred - predicate function + * @return {WrappedRange} + */ + this.expand = function (pred) { + var startAncestor = dom.ancestor(sc, pred); + var endAncestor = dom.ancestor(ec, pred); + + if (!startAncestor && !endAncestor) { + return new WrappedRange(sc, so, ec, eo); + } + + var boundaryPoints = this.getPoints(); + + if (startAncestor) { + boundaryPoints.sc = startAncestor; + boundaryPoints.so = 0; + } + + if (endAncestor) { + boundaryPoints.ec = endAncestor; + boundaryPoints.eo = dom.nodeLength(endAncestor); + } + + return new WrappedRange( + boundaryPoints.sc, + boundaryPoints.so, + boundaryPoints.ec, + boundaryPoints.eo + ); + }; + + /** + * @param {Boolean} isCollapseToStart + * @return {WrappedRange} + */ + this.collapse = function (isCollapseToStart) { + if (isCollapseToStart) { + return new WrappedRange(sc, so, sc, so); + } else { + return new WrappedRange(ec, eo, ec, eo); + } + }; + + /** + * splitText on range + */ + this.splitText = function () { + var isSameContainer = sc === ec; + var boundaryPoints = this.getPoints(); + + if (dom.isText(ec) && !dom.isEdgePoint(this.getEndPoint())) { + ec.splitText(eo); + } + + if (dom.isText(sc) && !dom.isEdgePoint(this.getStartPoint())) { + boundaryPoints.sc = sc.splitText(so); + boundaryPoints.so = 0; + + if (isSameContainer) { + boundaryPoints.ec = boundaryPoints.sc; + boundaryPoints.eo = eo - so; + } + } + + return new WrappedRange( + boundaryPoints.sc, + boundaryPoints.so, + boundaryPoints.ec, + boundaryPoints.eo + ); + }; + + /** + * delete contents on range + * @return {WrappedRange} + */ + this.deleteContents = function () { + if (this.isCollapsed()) { + return this; + } + + var rng = this.splitText(); + var nodes = rng.nodes(null, { + fullyContains: true + }); + + var point = dom.prevPointUntil(rng.getStartPoint(), function (point) { + return !list.contains(nodes, point.node); + }); + + var emptyParents = []; + $.each(nodes, function (idx, node) { + // find empty parents + var parent = node.parentNode; + if (point.node !== parent && dom.nodeLength(parent) === 1) { + emptyParents.push(parent); + } + dom.remove(node, false); + }); + + // remove empty parents + $.each(emptyParents, function (idx, node) { + dom.remove(node, false); + }); + + return new WrappedRange( + point.node, + point.offset, + point.node, + point.offset + ); + }; + + /** + * makeIsOn: return isOn(pred) function + */ + var makeIsOn = function (pred) { + return function () { + var ancestor = dom.ancestor(sc, pred); + return !!ancestor && (ancestor === dom.ancestor(ec, pred)); + }; + }; + + // isOnEditable: judge whether range is on editable or not + this.isOnEditable = makeIsOn(dom.isEditable); + // isOnList: judge whether range is on list node or not + this.isOnList = makeIsOn(dom.isList); + // isOnAnchor: judge whether range is on anchor node or not + this.isOnAnchor = makeIsOn(dom.isAnchor); + // isOnAnchor: judge whether range is on cell node or not + this.isOnCell = makeIsOn(dom.isCell); + + /** + * @param {Function} pred + * @return {Boolean} + */ + this.isLeftEdgeOf = function (pred) { + if (!dom.isLeftEdgePoint(this.getStartPoint())) { + return false; + } + + var node = dom.ancestor(this.sc, pred); + return node && dom.isLeftEdgeOf(this.sc, node); + }; + + /** + * returns whether range was collapsed or not + */ + this.isCollapsed = function () { + return sc === ec && so === eo; + }; + + /** + * wrap inline nodes which children of body with paragraph + * + * @return {WrappedRange} + */ + this.wrapBodyInlineWithPara = function () { + if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) { + sc.innerHTML = dom.emptyPara; + return new WrappedRange(sc.firstChild, 0); + } else if (!dom.isInline(sc) || dom.isParaInline(sc)) { + return this; + } + + // find inline top ancestor + var ancestors = dom.listAncestor(sc, func.not(dom.isInline)); + var topAncestor = list.last(ancestors); + if (!dom.isInline(topAncestor)) { + topAncestor = ancestors[ancestors.length - 2] || sc.childNodes[so]; + } + + // siblings not in paragraph + var inlineSiblings = dom.listPrev(topAncestor, dom.isParaInline).reverse(); + inlineSiblings = inlineSiblings.concat(dom.listNext(topAncestor.nextSibling, dom.isParaInline)); + + // wrap with paragraph + if (inlineSiblings.length) { + var para = dom.wrap(list.head(inlineSiblings), 'p'); + dom.appendChildNodes(para, list.tail(inlineSiblings)); + } + + return this; + }; + + /** + * insert node at current cursor + * + * @param {Node} node + * @param {Boolean} [isInline] + * @return {Node} + */ + this.insertNode = function (node, isInline) { + var rng = this.wrapBodyInlineWithPara(); + var point = rng.getStartPoint(); + + var splitRoot, container, pivot; + if (isInline) { + container = dom.isPara(point.node) ? point.node : point.node.parentNode; + if (dom.isPara(point.node)) { + pivot = point.node.childNodes[point.offset]; + } else { + pivot = dom.splitTree(point.node, point); + } + } else { + // splitRoot will be childNode of container + var ancestors = dom.listAncestor(point.node, dom.isBodyContainer); + var topAncestor = list.last(ancestors) || point.node; + + if (dom.isBodyContainer(topAncestor)) { + splitRoot = ancestors[ancestors.length - 2]; + container = topAncestor; + } else { + splitRoot = topAncestor; + container = splitRoot.parentNode; + } + pivot = splitRoot && dom.splitTree(splitRoot, point); + } + + if (pivot) { + pivot.parentNode.insertBefore(node, pivot); + } else { + container.appendChild(node); + } + + return node; + }; + + this.toString = function () { + var nativeRng = nativeRange(); + return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text; + }; + + /** + * create offsetPath bookmark + * @param {Node} editable + */ + this.bookmark = function (editable) { + return { + s: { + path: dom.makeOffsetPath(editable, sc), + offset: so + }, + e: { + path: dom.makeOffsetPath(editable, ec), + offset: eo + } + }; + }; + + /** + * getClientRects + * @return {Rect[]} + */ + this.getClientRects = function () { + var nativeRng = nativeRange(); + return nativeRng.getClientRects(); + }; + }; + + return { + /** + * create Range Object From arguments or Browser Selection + * + * @param {Node} sc - start container + * @param {Number} so - start offset + * @param {Node} ec - end container + * @param {Number} eo - end offset + */ + create : function (sc, so, ec, eo) { + if (!arguments.length) { // from Browser Selection + if (agent.isW3CRangeSupport) { + var selection = document.getSelection(); + if (selection.rangeCount === 0) { + return null; + } else if (dom.isBody(selection.anchorNode)) { + // Firefox: returns entire body as range on initialization. We won't never need it. + return null; + } + + var nativeRng = selection.getRangeAt(0); + sc = nativeRng.startContainer; + so = nativeRng.startOffset; + ec = nativeRng.endContainer; + eo = nativeRng.endOffset; + } else { // IE8: TextRange + var textRange = document.selection.createRange(); + var textRangeEnd = textRange.duplicate(); + textRangeEnd.collapse(false); + var textRangeStart = textRange; + textRangeStart.collapse(true); + + var startPoint = textRangeToPoint(textRangeStart, true), + endPoint = textRangeToPoint(textRangeEnd, false); + + sc = startPoint.cont; + so = startPoint.offset; + ec = endPoint.cont; + eo = endPoint.offset; + } + } else if (arguments.length === 2) { //collapsed + ec = sc; + eo = so; + } + return new WrappedRange(sc, so, ec, eo); + }, + + /** + * create WrappedRange from node + * + * @param {Node} node + * @return {WrappedRange} + */ + createFromNode: function (node) { + return this.create(node, 0, node, 1); + }, + + /** + * create WrappedRange from Bookmark + * + * @param {Node} editable + * @param {Obkect} bookmark + * @return {WrappedRange} + */ + createFromBookmark : function (editable, bookmark) { + var sc = dom.fromOffsetPath(editable, bookmark.s.path); + var so = bookmark.s.offset; + var ec = dom.fromOffsetPath(editable, bookmark.e.path); + var eo = bookmark.e.offset; + return new WrappedRange(sc, so, ec, eo); + } + }; + })(); + + + var Typing = function () { + + /** + * @param {jQuery} $editable + * @param {WrappedRange} rng + * @param {Number} tabsize + */ + this.insertTab = function ($editable, rng, tabsize) { + var tab = dom.createText(new Array(tabsize + 1).join(dom.NBSP_CHAR)); + rng = rng.deleteContents(); + rng.insertNode(tab, true); + + rng = range.create(tab, tabsize); + rng.select(); + }; + + /** + * insert paragraph + */ + this.insertParagraph = function () { + var rng = range.create(); + + // deleteContents on range. + rng = rng.deleteContents(); + + // Wrap range if it needs to be wrapped by paragraph + rng = rng.wrapBodyInlineWithPara(); + + // finding paragraph + var splitRoot = dom.ancestor(rng.sc, dom.isPara); + + var nextPara; + // on paragraph: split paragraph + if (splitRoot) { + nextPara = dom.splitTree(splitRoot, rng.getStartPoint()); + + var emptyAnchors = dom.listDescendant(splitRoot, dom.isEmptyAnchor); + emptyAnchors = emptyAnchors.concat(dom.listDescendant(nextPara, dom.isEmptyAnchor)); + + $.each(emptyAnchors, function (idx, anchor) { + dom.remove(anchor); + }); + // no paragraph: insert empty paragraph + } else { + var next = rng.sc.childNodes[rng.so]; + nextPara = $(dom.emptyPara)[0]; + if (next) { + rng.sc.insertBefore(nextPara, next); + } else { + rng.sc.appendChild(nextPara); + } + } + + range.create(nextPara, 0).normalize().select(); + }; + + }; + + /** + * Table + * @class + */ + var Table = function () { + /** + * handle tab key + * + * @param {WrappedRange} rng + * @param {Boolean} isShift + */ + this.tab = function (rng, isShift) { + var cell = dom.ancestor(rng.commonAncestor(), dom.isCell); + var table = dom.ancestor(cell, dom.isTable); + var cells = dom.listDescendant(table, dom.isCell); + + var nextCell = list[isShift ? 'prev' : 'next'](cells, cell); + if (nextCell) { + range.create(nextCell, 0).select(); + } + }; + + /** + * create empty table element + * + * @param {Number} rowCount + * @param {Number} colCount + * @return {Node} + */ + this.createTable = function (colCount, rowCount) { + var tds = [], tdHTML; + for (var idxCol = 0; idxCol < colCount; idxCol++) { + tds.push('<td>' + dom.blank + '</td>'); + } + tdHTML = tds.join(''); + + var trs = [], trHTML; + for (var idxRow = 0; idxRow < rowCount; idxRow++) { + trs.push('<tr>' + tdHTML + '</tr>'); + } + trHTML = trs.join(''); + return $('<table class="table table-bordered">' + trHTML + '</table>')[0]; + }; + }; + + + var Bullet = function () { + /** + * toggle ordered list + * @type command + */ + this.insertOrderedList = function () { + this.toggleList('OL'); + }; + + /** + * toggle unordered list + * @type command + */ + this.insertUnorderedList = function () { + this.toggleList('UL'); + }; + + /** + * indent + * @type command + */ + this.indent = function () { + var self = this; + var rng = range.create().wrapBodyInlineWithPara(); + + var paras = rng.nodes(dom.isPara, { includeAncestor: true }); + var clustereds = list.clusterBy(paras, func.peq2('parentNode')); + + $.each(clustereds, function (idx, paras) { + var head = list.head(paras); + if (dom.isLi(head)) { + self.wrapList(paras, head.parentNode.nodeName); + } else { + $.each(paras, function (idx, para) { + $(para).css('marginLeft', function (idx, val) { + return (parseInt(val, 10) || 0) + 25; + }); + }); + } + }); + + rng.select(); + }; + + /** + * outdent + * @type command + */ + this.outdent = function () { + var self = this; + var rng = range.create().wrapBodyInlineWithPara(); + + var paras = rng.nodes(dom.isPara, { includeAncestor: true }); + var clustereds = list.clusterBy(paras, func.peq2('parentNode')); + + $.each(clustereds, function (idx, paras) { + var head = list.head(paras); + if (dom.isLi(head)) { + self.releaseList([paras]); + } else { + $.each(paras, function (idx, para) { + $(para).css('marginLeft', function (idx, val) { + val = (parseInt(val, 10) || 0); + return val > 25 ? val - 25 : ''; + }); + }); + } + }); + + rng.select(); + }; + + /** + * toggle list + * @param {String} listName - OL or UL + */ + this.toggleList = function (listName) { + var self = this; + var rng = range.create().wrapBodyInlineWithPara(); + + var paras = rng.nodes(dom.isPara, { includeAncestor: true }); + var clustereds = list.clusterBy(paras, func.peq2('parentNode')); + + // paragraph to list + if (list.find(paras, dom.isPurePara)) { + $.each(clustereds, function (idx, paras) { + self.wrapList(paras, listName); + }); + // list to paragraph or change list style + } else { + var diffLists = rng.nodes(dom.isList, { + includeAncestor: true + }).filter(function (listNode) { + return !$.nodeName(listNode, listName); + }); + + if (diffLists.length) { + $.each(diffLists, function (idx, listNode) { + dom.replace(listNode, listName); + }); + } else { + this.releaseList(clustereds, true); + } + } + + rng.select(); + }; + + /** + * @param {Node[]} paras + * @param {String} listName + */ + this.wrapList = function (paras, listName) { + var head = list.head(paras); + var last = list.last(paras); + + var prevList = dom.isList(head.previousSibling) && head.previousSibling; + var nextList = dom.isList(last.nextSibling) && last.nextSibling; + + var listNode = prevList || dom.insertAfter(dom.create(listName || 'UL'), last); + + // P to LI + paras = $.map(paras, function (para) { + return dom.isPurePara(para) ? dom.replace(para, 'LI') : para; + }); + + // append to list(<ul>, <ol>) + dom.appendChildNodes(listNode, paras); + + if (nextList) { + dom.appendChildNodes(listNode, list.from(nextList.childNodes)); + dom.remove(nextList); + } + }; + + /** + * @param {Array[]} clustereds + * @param {Boolean} isEscapseToBody + * @return {Node[]} + */ + this.releaseList = function (clustereds, isEscapseToBody) { + var releasedParas = []; + + $.each(clustereds, function (idx, paras) { + var head = list.head(paras); + var last = list.last(paras); + + var headList = isEscapseToBody ? dom.lastAncestor(head, dom.isList) : + head.parentNode; + var lastList = headList.childNodes.length > 1 ? dom.splitTree(headList, { + node: last.parentNode, + offset: dom.position(last) + 1 + }, true) : null; + + var middleList = dom.splitTree(headList, { + node: head.parentNode, + offset: dom.position(head) + }, true); + + paras = isEscapseToBody ? dom.listDescendant(middleList, dom.isLi) : + list.from(middleList.childNodes).filter(dom.isLi); + + // LI to P + if (isEscapseToBody || !dom.isList(headList.parentNode)) { + paras = $.map(paras, function (para) { + return dom.replace(para, 'P'); + }); + } + + $.each(list.from(paras).reverse(), function (idx, para) { + dom.insertAfter(para, headList); + }); + + // remove empty lists + var rootLists = list.compact([headList, middleList, lastList]); + $.each(rootLists, function (idx, rootList) { + var listNodes = [rootList].concat(dom.listDescendant(rootList, dom.isList)); + $.each(listNodes.reverse(), function (idx, listNode) { + if (!dom.nodeLength(listNode)) { + dom.remove(listNode, true); + } + }); + }); + + releasedParas = releasedParas.concat(paras); + }); + + return releasedParas; + }; + }; + + /** + * Editor + * @class + */ + var Editor = function () { + + var style = new Style(); + var table = new Table(); + var typing = new Typing(); + var bullet = new Bullet(); + + /** + * save current range + * + * @param {jQuery} $editable + */ + this.saveRange = function ($editable, thenCollapse) { + $editable.focus(); + $editable.data('range', range.create()); + if (thenCollapse) { + range.create().collapse().select(); + } + }; + + /** + * restore lately range + * + * @param {jQuery} $editable + */ + this.restoreRange = function ($editable) { + var rng = $editable.data('range'); + if (rng) { + rng.select(); + $editable.focus(); + } + }; + + /** + * current style + * @param {Node} target + */ + this.currentStyle = function (target) { + var rng = range.create(); + return rng ? rng.isOnEditable() && style.current(rng, target) : false; + }; + + var triggerOnChange = this.triggerOnChange = function ($editable) { + var onChange = $editable.data('callbacks').onChange; + if (onChange) { + onChange($editable.html(), $editable); + } + }; + + /** + * undo + * @param {jQuery} $editable + */ + this.undo = function ($editable) { + $editable.data('NoteHistory').undo(); + triggerOnChange($editable); + }; + + /** + * redo + * @param {jQuery} $editable + */ + this.redo = function ($editable) { + $editable.data('NoteHistory').redo(); + triggerOnChange($editable); + }; + + /** + * after command + * @param {jQuery} $editable + */ + var afterCommand = this.afterCommand = function ($editable) { + $editable.data('NoteHistory').recordUndo(); + triggerOnChange($editable); + }; + + /* jshint ignore:start */ + // native commands(with execCommand), generate function for execCommand + var commands = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', + 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull', + 'formatBlock', 'removeFormat', + 'backColor', 'foreColor', 'insertHorizontalRule', 'fontName']; + + for (var idx = 0, len = commands.length; idx < len; idx ++) { + this[commands[idx]] = (function (sCmd) { + return function ($editable, value) { + document.execCommand(sCmd, false, value); + + afterCommand($editable); + }; + })(commands[idx]); + } + /* jshint ignore:end */ + + /** + * handle tab key + * + * @param {jQuery} $editable + * @param {Object} options + */ + this.tab = function ($editable, options) { + var rng = range.create(); + if (rng.isCollapsed() && rng.isOnCell()) { + table.tab(rng); + } else { + typing.insertTab($editable, rng, options.tabsize); + afterCommand($editable); + } + }; + + /** + * handle shift+tab key + */ + this.untab = function () { + var rng = range.create(); + if (rng.isCollapsed() && rng.isOnCell()) { + table.tab(rng, true); + } + }; + + /** + * insert paragraph + * + * @param {Node} $editable + */ + this.insertParagraph = function ($editable) { + typing.insertParagraph($editable); + afterCommand($editable); + }; + + /** + * @param {jQuery} $editable + */ + this.insertOrderedList = function ($editable) { + bullet.insertOrderedList($editable); + afterCommand($editable); + }; + + /** + * @param {jQuery} $editable + */ + this.insertUnorderedList = function ($editable) { + bullet.insertUnorderedList($editable); + afterCommand($editable); + }; + + /** + * @param {jQuery} $editable + */ + this.indent = function ($editable) { + bullet.indent($editable); + afterCommand($editable); + }; + + /** + * @param {jQuery} $editable + */ + this.outdent = function ($editable) { + bullet.outdent($editable); + afterCommand($editable); + }; + + /** + * insert image + * + * @param {jQuery} $editable + * @param {String} sUrl + */ + this.insertImage = function ($editable, sUrl, filename) { + async.createImage(sUrl, filename).then(function ($image) { + $image.css({ + display: '', + width: Math.min($editable.width(), $image.width()) + }); + range.create().insertNode($image[0]); + afterCommand($editable); + }).fail(function () { + var callbacks = $editable.data('callbacks'); + if (callbacks.onImageUploadError) { + callbacks.onImageUploadError(); + } + }); + }; + + /** + * insert video + * @param {jQuery} $editable + * @param {String} sUrl + */ + this.insertVideo = function ($editable, sUrl) { + // video url patterns(youtube, instagram, vimeo, dailymotion, youku) + var ytRegExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; + var ytMatch = sUrl.match(ytRegExp); + + var igRegExp = /\/\/instagram.com\/p\/(.[a-zA-Z0-9]*)/; + var igMatch = sUrl.match(igRegExp); + + var vRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/; + var vMatch = sUrl.match(vRegExp); + + var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/; + var vimMatch = sUrl.match(vimRegExp); + + var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/; + var dmMatch = sUrl.match(dmRegExp); + + var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)\.html/; + var youkuMatch = sUrl.match(youkuRegExp); + + var $video; + if (ytMatch && ytMatch[2].length === 11) { + var youtubeId = ytMatch[2]; + $video = $('<iframe>') + .attr('src', '//www.youtube.com/embed/' + youtubeId) + .attr('width', '640').attr('height', '360'); + } else if (igMatch && igMatch[0].length) { + $video = $('<iframe>') + .attr('src', igMatch[0] + '/embed/') + .attr('width', '612').attr('height', '710') + .attr('scrolling', 'no') + .attr('allowtransparency', 'true'); + } else if (vMatch && vMatch[0].length) { + $video = $('<iframe>') + .attr('src', vMatch[0] + '/embed/simple') + .attr('width', '600').attr('height', '600') + .attr('class', 'vine-embed'); + } else if (vimMatch && vimMatch[3].length) { + $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>') + .attr('src', '//player.vimeo.com/video/' + vimMatch[3]) + .attr('width', '640').attr('height', '360'); + } else if (dmMatch && dmMatch[2].length) { + $video = $('<iframe>') + .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2]) + .attr('width', '640').attr('height', '360'); + } else if (youkuMatch && youkuMatch[1].length) { + $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>') + .attr('height', '498') + .attr('width', '510') + .attr('src', '//player.youku.com/embed/' + youkuMatch[1]); + } else { + // this is not a known video link. Now what, Cat? Now what? + } + + if ($video) { + $video.attr('frameborder', 0); + range.create().insertNode($video[0]); + afterCommand($editable); + } + }; + + /** + * formatBlock + * + * @param {jQuery} $editable + * @param {String} tagName + */ + this.formatBlock = function ($editable, tagName) { + tagName = agent.isMSIE ? '<' + tagName + '>' : tagName; + document.execCommand('FormatBlock', false, tagName); + afterCommand($editable); + }; + + this.formatPara = function ($editable) { + this.formatBlock($editable, 'P'); + afterCommand($editable); + }; + + /* jshint ignore:start */ + for (var idx = 1; idx <= 6; idx ++) { + this['formatH' + idx] = function (idx) { + return function ($editable) { + this.formatBlock($editable, 'H' + idx); + }; + }(idx); + }; + /* jshint ignore:end */ + + /** + * fontsize + * FIXME: Still buggy + * + * @param {jQuery} $editable + * @param {String} value - px + */ + this.fontSize = function ($editable, value) { + document.execCommand('fontSize', false, 3); + if (agent.isFF) { + // firefox: <font size="3"> to <span style='font-size={value}px;'>, buggy + $editable.find('font[size=3]').removeAttr('size').css('font-size', value + 'px'); + } else { + // chrome: <span style="font-size: medium"> to <span style='font-size={value}px;'> + $editable.find('span').filter(function () { + return this.style.fontSize === 'medium'; + }).css('font-size', value + 'px'); + } + + afterCommand($editable); + }; + + /** + * lineHeight + * @param {jQuery} $editable + * @param {String} value + */ + this.lineHeight = function ($editable, value) { + style.stylePara(range.create(), { + lineHeight: value + }); + afterCommand($editable); + }; + + /** + * unlink + * + * @type command + * + * @param {jQuery} $editable + */ + this.unlink = function ($editable) { + var rng = range.create(); + if (rng.isOnAnchor()) { + var anchor = dom.ancestor(rng.sc, dom.isAnchor); + rng = range.createFromNode(anchor); + rng.select(); + document.execCommand('unlink'); + + afterCommand($editable); + } + }; + + /** + * create link + * + * @type command + * + * @param {jQuery} $editable + * @param {Object} linkInfo + * @param {Object} options + */ + this.createLink = function ($editable, linkInfo, options) { + var linkUrl = linkInfo.url; + var linkText = linkInfo.text; + var isNewWindow = linkInfo.newWindow; + var rng = linkInfo.range; + + if (options.onCreateLink) { + linkUrl = options.onCreateLink(linkUrl); + } + + rng = rng.deleteContents(); + + // Create a new link when there is no anchor on range. + var anchor = rng.insertNode($('<A>' + linkText + '</A>')[0], true); + $(anchor).attr({ + href: linkUrl, + target: isNewWindow ? '_blank' : '' + }); + + range.createFromNode(anchor).select(); + afterCommand($editable); + }; + + /** + * returns link info + * + * @return {Object} + */ + this.getLinkInfo = function ($editable) { + $editable.focus(); + + var rng = range.create().expand(dom.isAnchor); + + // Get the first anchor on range(for edit). + var $anchor = $(list.head(rng.nodes(dom.isAnchor))); + + return { + range: rng, + text: rng.toString(), + isNewWindow: $anchor.length ? $anchor.attr('target') === '_blank' : true, + url: $anchor.length ? $anchor.attr('href') : '' + }; + }; + + /** + * get video info + * + * @param {jQuery} $editable + * @return {Object} + */ + this.getVideoInfo = function ($editable) { + $editable.focus(); + + var rng = range.create(); + + if (rng.isOnAnchor()) { + var anchor = dom.ancestor(rng.sc, dom.isAnchor); + rng = range.createFromNode(anchor); + } + + return { + text: rng.toString() + }; + }; + + this.color = function ($editable, sObjColor) { + var oColor = JSON.parse(sObjColor); + var foreColor = oColor.foreColor, backColor = oColor.backColor; + + if (foreColor) { document.execCommand('foreColor', false, foreColor); } + if (backColor) { document.execCommand('backColor', false, backColor); } + + afterCommand($editable); + }; + + this.insertTable = function ($editable, sDim) { + var dimension = sDim.split('x'); + var rng = range.create(); + rng = rng.deleteContents(); + rng.insertNode(table.createTable(dimension[0], dimension[1])); + afterCommand($editable); + }; + + /** + * @param {jQuery} $editable + * @param {String} value + * @param {jQuery} $target + */ + this.floatMe = function ($editable, value, $target) { + $target.css('float', value); + afterCommand($editable); + }; + + /** + * resize overlay element + * @param {jQuery} $editable + * @param {String} value + * @param {jQuery} $target - target element + */ + this.resize = function ($editable, value, $target) { + $target.css({ + width: $editable.width() * value + 'px', + height: '' + }); + + afterCommand($editable); + }; + + /** + * @param {Position} pos + * @param {jQuery} $target - target element + * @param {Boolean} [bKeepRatio] - keep ratio + */ + this.resizeTo = function (pos, $target, bKeepRatio) { + var imageSize; + if (bKeepRatio) { + var newRatio = pos.y / pos.x; + var ratio = $target.data('ratio'); + imageSize = { + width: ratio > newRatio ? pos.x : pos.y / ratio, + height: ratio > newRatio ? pos.x * ratio : pos.y + }; + } else { + imageSize = { + width: pos.x, + height: pos.y + }; + } + + $target.css(imageSize); + }; + + /** + * remove media object + * + * @param {jQuery} $editable + * @param {String} value - dummy argument (for keep interface) + * @param {jQuery} $target - target element + */ + this.removeMedia = function ($editable, value, $target) { + $target.detach(); + + afterCommand($editable); + }; + }; + + /** + * History + * @class + */ + var History = function ($editable) { + var stack = [], stackOffset = 0; + var editable = $editable[0]; + + var makeSnapshot = function () { + var rng = range.create(); + var emptyBookmark = {s: {path: [0], offset: 0}, e: {path: [0], offset: 0}}; + + return { + contents: $editable.html(), + bookmark: (rng ? rng.bookmark(editable) : emptyBookmark) + }; + }; + + var applySnapshot = function (snapshot) { + if (snapshot.contents !== null) { + $editable.html(snapshot.contents); + } + if (snapshot.bookmark !== null) { + range.createFromBookmark(editable, snapshot.bookmark).select(); + } + }; + + this.undo = function () { + if (0 < stackOffset) { + stackOffset--; + applySnapshot(stack[stackOffset]); + } + }; + + this.redo = function () { + if (stack.length - 1 > stackOffset) { + stackOffset++; + applySnapshot(stack[stackOffset]); + } + }; + + this.recordUndo = function () { + // Wash out stack after stackOffset + if (stack.length > stackOffset) { + stack = stack.slice(0, stackOffset); + } + + // Create new snapshot and push it to the end + stack.push(makeSnapshot()); + stackOffset++; + }; + + // Create first undo stack + this.recordUndo(); + }; + + /** + * Button + */ + var Button = function () { + /** + * update button status + * + * @param {jQuery} $container + * @param {Object} styleInfo + */ + this.update = function ($container, styleInfo) { + /** + * handle dropdown's check mark (for fontname, fontsize, lineHeight). + * @param {jQuery} $btn + * @param {Number} value + */ + var checkDropdownMenu = function ($btn, value) { + $btn.find('.dropdown-menu li a').each(function () { + // always compare string to avoid creating another func. + var isChecked = ($(this).data('value') + '') === (value + ''); + this.className = isChecked ? 'checked' : ''; + }); + }; + + /** + * update button state(active or not). + * + * @param {String} selector + * @param {Function} pred + */ + var btnState = function (selector, pred) { + var $btn = $container.find(selector); + $btn.toggleClass('active', pred()); + }; + + // fontname + var $fontname = $container.find('.note-fontname'); + if ($fontname.length) { + var selectedFont = styleInfo['font-family']; + if (!!selectedFont) { + selectedFont = list.head(selectedFont.split(',')); + selectedFont = selectedFont.replace(/\'/g, ''); + $fontname.find('.note-current-fontname').text(selectedFont); + checkDropdownMenu($fontname, selectedFont); + } + } + + // fontsize + var $fontsize = $container.find('.note-fontsize'); + $fontsize.find('.note-current-fontsize').text(styleInfo['font-size']); + checkDropdownMenu($fontsize, parseFloat(styleInfo['font-size'])); + + // lineheight + var $lineHeight = $container.find('.note-height'); + checkDropdownMenu($lineHeight, parseFloat(styleInfo['line-height'])); + + btnState('button[data-event="bold"]', function () { + return styleInfo['font-bold'] === 'bold'; + }); + btnState('button[data-event="italic"]', function () { + return styleInfo['font-italic'] === 'italic'; + }); + btnState('button[data-event="underline"]', function () { + return styleInfo['font-underline'] === 'underline'; + }); + btnState('button[data-event="strikethrough"]', function () { + return styleInfo['font-strikethrough'] === 'strikethrough'; + }); + btnState('button[data-event="superscript"]', function () { + return styleInfo['font-superscript'] === 'superscript'; + }); + btnState('button[data-event="subscript"]', function () { + return styleInfo['font-subscript'] === 'subscript'; + }); + btnState('button[data-event="justifyLeft"]', function () { + return styleInfo['text-align'] === 'left' || styleInfo['text-align'] === 'start'; + }); + btnState('button[data-event="justifyCenter"]', function () { + return styleInfo['text-align'] === 'center'; + }); + btnState('button[data-event="justifyRight"]', function () { + return styleInfo['text-align'] === 'right'; + }); + btnState('button[data-event="justifyFull"]', function () { + return styleInfo['text-align'] === 'justify'; + }); + btnState('button[data-event="insertUnorderedList"]', function () { + return styleInfo['list-style'] === 'unordered'; + }); + btnState('button[data-event="insertOrderedList"]', function () { + return styleInfo['list-style'] === 'ordered'; + }); + }; + + /** + * update recent color + * + * @param {Node} button + * @param {String} eventName + * @param {value} value + */ + this.updateRecentColor = function (button, eventName, value) { + var $color = $(button).closest('.note-color'); + var $recentColor = $color.find('.note-recent-color'); + var colorInfo = JSON.parse($recentColor.attr('data-value')); + colorInfo[eventName] = value; + $recentColor.attr('data-value', JSON.stringify(colorInfo)); + var sKey = eventName === 'backColor' ? 'background-color' : 'color'; + $recentColor.find('i').css(sKey, value); + }; + }; + + /** + * Toolbar + */ + var Toolbar = function () { + var button = new Button(); + + this.update = function ($toolbar, styleInfo) { + button.update($toolbar, styleInfo); + }; + + /** + * @param {Node} button + * @param {String} eventName + * @param {String} value + */ + this.updateRecentColor = function (buttonNode, eventName, value) { + button.updateRecentColor(buttonNode, eventName, value); + }; + + /** + * activate buttons exclude codeview + * @param {jQuery} $toolbar + */ + this.activate = function ($toolbar) { + $toolbar.find('button') + .not('button[data-event="codeview"]') + .removeClass('disabled'); + }; + + /** + * deactivate buttons exclude codeview + * @param {jQuery} $toolbar + */ + this.deactivate = function ($toolbar) { + $toolbar.find('button') + .not('button[data-event="codeview"]') + .addClass('disabled'); + }; + + this.updateFullscreen = function ($container, bFullscreen) { + var $btn = $container.find('button[data-event="fullscreen"]'); + $btn.toggleClass('active', bFullscreen); + }; + + this.updateCodeview = function ($container, isCodeview) { + var $btn = $container.find('button[data-event="codeview"]'); + $btn.toggleClass('active', isCodeview); + }; + }; + + /** + * Popover (http://getbootstrap.com/javascript/#popovers) + */ + var Popover = function () { + var button = new Button(); + + /** + * returns position from placeholder + * @param {Node} placeholder + * @param {Boolean} isAirMode + */ + var posFromPlaceholder = function (placeholder, isAirMode) { + var $placeholder = $(placeholder); + var pos = isAirMode ? $placeholder.offset() : $placeholder.position(); + var height = $placeholder.outerHeight(true); // include margin + + // popover below placeholder. + return { + left: pos.left, + top: pos.top + height + }; + }; + + /** + * show popover + * @param {jQuery} popover + * @param {Position} pos + */ + var showPopover = function ($popover, pos) { + $popover.css({ + display: 'block', + left: pos.left, + top: pos.top + }); + }; + + var PX_POPOVER_ARROW_OFFSET_X = 20; + + /** + * update current state + * @param {jQuery} $popover - popover container + * @param {Object} styleInfo - style object + * @param {Boolean} isAirMode + */ + this.update = function ($popover, styleInfo, isAirMode) { + button.update($popover, styleInfo); + + var $linkPopover = $popover.find('.note-link-popover'); + if (styleInfo.anchor) { + var $anchor = $linkPopover.find('a'); + var href = $(styleInfo.anchor).attr('href'); + $anchor.attr('href', href).html(href); + showPopover($linkPopover, posFromPlaceholder(styleInfo.anchor, isAirMode)); + } else { + $linkPopover.hide(); + } + + var $imagePopover = $popover.find('.note-image-popover'); + if (styleInfo.image) { + showPopover($imagePopover, posFromPlaceholder(styleInfo.image, isAirMode)); + } else { + $imagePopover.hide(); + } + + var $airPopover = $popover.find('.note-air-popover'); + if (isAirMode && !styleInfo.range.isCollapsed()) { + var bnd = func.rect2bnd(list.last(styleInfo.range.getClientRects())); + showPopover($airPopover, { + left: Math.max(bnd.left + bnd.width / 2 - PX_POPOVER_ARROW_OFFSET_X, 0), + top: bnd.top + bnd.height + }); + } else { + $airPopover.hide(); + } + }; + + /** + * @param {Node} button + * @param {String} eventName + * @param {String} value + */ + this.updateRecentColor = function (button, eventName, value) { + button.updateRecentColor(button, eventName, value); + }; + + /** + * hide all popovers + * @param {jQuery} $popover - popover contaienr + */ + this.hide = function ($popover) { + $popover.children().hide(); + }; + }; + + /** + * Handle + */ + var Handle = function () { + /** + * update handle + * @param {jQuery} $handle + * @param {Object} styleInfo + * @param {Boolean} isAirMode + */ + this.update = function ($handle, styleInfo, isAirMode) { + var $selection = $handle.find('.note-control-selection'); + if (styleInfo.image) { + var $image = $(styleInfo.image); + var pos = isAirMode ? $image.offset() : $image.position(); + + // include margin + var imageSize = { + w: $image.outerWidth(true), + h: $image.outerHeight(true) + }; + + $selection.css({ + display: 'block', + left: pos.left, + top: pos.top, + width: imageSize.w, + height: imageSize.h + }).data('target', styleInfo.image); // save current image element. + var sizingText = imageSize.w + 'x' + imageSize.h; + $selection.find('.note-control-selection-info').text(sizingText); + } else { + $selection.hide(); + } + }; + + this.hide = function ($handle) { + $handle.children().hide(); + }; + }; + + /** + * Dialog + * + * @class + */ + var Dialog = function () { + + /** + * toggle button status + * + * @param {jQuery} $btn + * @param {Boolean} isEnable + */ + var toggleBtn = function ($btn, isEnable) { + $btn.toggleClass('disabled', !isEnable); + $btn.attr('disabled', !isEnable); + }; + + /** + * show image dialog + * + * @param {jQuery} $editable + * @param {jQuery} $dialog + * @return {Promise} + */ + this.showImageDialog = function ($editable, $dialog) { + return $.Deferred(function (deferred) { + var $imageDialog = $dialog.find('.note-image-dialog'); + + var $imageInput = $dialog.find('.note-image-input'), + $imageUrl = $dialog.find('.note-image-url'), + $imageBtn = $dialog.find('.note-image-btn'); + + $imageDialog.one('shown.bs.modal', function () { + // Cloning imageInput to clear element. + $imageInput.replaceWith($imageInput.clone() + .on('change', function () { + deferred.resolve(this.files); + $imageDialog.modal('hide'); + }) + .val('') + ); + + $imageBtn.click(function (event) { + event.preventDefault(); + + deferred.resolve($imageUrl.val()); + $imageDialog.modal('hide'); + }); + + $imageUrl.on('keyup paste', function (event) { + var url; + + if (event.type === 'paste') { + url = event.originalEvent.clipboardData.getData('text'); + } else { + url = $imageUrl.val(); + } + + toggleBtn($imageBtn, url); + }).val('').trigger('focus'); + }).one('hidden.bs.modal', function () { + $imageInput.off('change'); + $imageUrl.off('keyup paste'); + $imageBtn.off('click'); + + if (deferred.state() === 'pending') { + deferred.reject(); + } + }).modal('show'); + }); + }; + + /** + * Show video dialog and set event handlers on dialog controls. + * + * @param {jQuery} $dialog + * @param {Object} videoInfo + * @return {Promise} + */ + this.showVideoDialog = function ($editable, $dialog, videoInfo) { + return $.Deferred(function (deferred) { + var $videoDialog = $dialog.find('.note-video-dialog'); + var $videoUrl = $videoDialog.find('.note-video-url'), + $videoBtn = $videoDialog.find('.note-video-btn'); + + $videoDialog.one('shown.bs.modal', function () { + $videoUrl.val(videoInfo.text).keyup(function () { + toggleBtn($videoBtn, $videoUrl.val()); + }).trigger('keyup').trigger('focus'); + + $videoBtn.click(function (event) { + event.preventDefault(); + + deferred.resolve($videoUrl.val()); + $videoDialog.modal('hide'); + }); + }).one('hidden.bs.modal', function () { + // dettach events + $videoUrl.off('keyup'); + $videoBtn.off('click'); + + if (deferred.state() === 'pending') { + deferred.reject(); + } + }).modal('show'); + }); + }; + + /** + * Show link dialog and set event handlers on dialog controls. + * + * @param {jQuery} $dialog + * @param {Object} linkInfo + * @return {Promise} + */ + this.showLinkDialog = function ($editable, $dialog, linkInfo) { + return $.Deferred(function (deferred) { + var $linkDialog = $dialog.find('.note-link-dialog'); + + var $linkText = $linkDialog.find('.note-link-text'), + $linkUrl = $linkDialog.find('.note-link-url'), + $linkBtn = $linkDialog.find('.note-link-btn'), + $openInNewWindow = $linkDialog.find('input[type=checkbox]'); + + $linkDialog.one('shown.bs.modal', function () { + $linkText.val(linkInfo.text); + + $linkText.keyup(function () { + // if linktext was modified by keyup, + // stop cloning text from linkUrl + linkInfo.text = $linkText.val(); + }); + + // if no url was given, copy text to url + if (!linkInfo.url) { + linkInfo.url = linkInfo.text; + toggleBtn($linkBtn, linkInfo.text); + } + + $linkUrl.keyup(function () { + toggleBtn($linkBtn, $linkUrl.val()); + // display same link on `Text to display` input + // when create a new link + if (!linkInfo.text) { + $linkText.val($linkUrl.val()); + } + }).val(linkInfo.url).trigger('focus').trigger('select'); + + $openInNewWindow.prop('checked', linkInfo.newWindow); + + $linkBtn.one('click', function (event) { + event.preventDefault(); + + deferred.resolve({ + range: linkInfo.range, + url: $linkUrl.val(), + text: $linkText.val(), + newWindow: $openInNewWindow.is(':checked') + }); + $linkDialog.modal('hide'); + }); + }).one('hidden.bs.modal', function () { + // dettach events + $linkText.off('keyup'); + $linkUrl.off('keyup'); + $linkBtn.off('click'); + + if (deferred.state() === 'pending') { + deferred.reject(); + } + }).modal('show'); + }).promise(); + }; + + /** + * show help dialog + * + * @param {jQuery} $dialog + */ + this.showHelpDialog = function ($editable, $dialog) { + return $.Deferred(function (deferred) { + var $helpDialog = $dialog.find('.note-help-dialog'); + + $helpDialog.one('hidden.bs.modal', function () { + deferred.resolve(); + }).modal('show'); + }).promise(); + }; + }; + + + var CodeMirror; + if (agent.hasCodeMirror) { + if (agent.isSupportAmd) { + require(['CodeMirror'], function (cm) { + CodeMirror = cm; + }); + } else { + CodeMirror = window.CodeMirror; + } + } + + /** + * EventHandler + */ + var EventHandler = function () { + var $window = $(window); + var $document = $(document); + var $scrollbar = $('html, body'); + + var editor = new Editor(); + var toolbar = new Toolbar(), popover = new Popover(); + var handle = new Handle(), dialog = new Dialog(); + + /** + * returns makeLayoutInfo from editor's descendant node. + * + * @param {Node} descendant + * @returns {Object} + */ + var makeLayoutInfo = function (descendant) { + var $target = $(descendant).closest('.note-editor, .note-air-editor, .note-air-layout'); + + if (!$target.length) { return null; } + + var $editor; + if ($target.is('.note-editor, .note-air-editor')) { + $editor = $target; + } else { + $editor = $('#note-editor-' + list.last($target.attr('id').split('-'))); + } + + return dom.buildLayoutInfo($editor); + }; + + /** + * insert Images from file array. + * + * @param {jQuery} $editable + * @param {File[]} files + */ + var insertImages = function ($editable, files) { + var callbacks = $editable.data('callbacks'); + + // If onImageUpload options setted + if (callbacks.onImageUpload) { + callbacks.onImageUpload(files, editor, $editable); + // else insert Image as dataURL + } else { + $.each(files, function (idx, file) { + var filename = file.name; + async.readFileAsDataURL(file).then(function (sDataURL) { + editor.insertImage($editable, sDataURL, filename); + }).fail(function () { + if (callbacks.onImageUploadError) { + callbacks.onImageUploadError(); + } + }); + }); + } + }; + + var commands = { + /** + * @param {Object} layoutInfo + */ + showLinkDialog: function (layoutInfo) { + var $editor = layoutInfo.editor(), + $dialog = layoutInfo.dialog(), + $editable = layoutInfo.editable(), + linkInfo = editor.getLinkInfo($editable); + + var options = $editor.data('options'); + + editor.saveRange($editable); + dialog.showLinkDialog($editable, $dialog, linkInfo).then(function (linkInfo) { + editor.restoreRange($editable); + editor.createLink($editable, linkInfo, options); + // hide popover after creating link + popover.hide(layoutInfo.popover()); + }).fail(function () { + editor.restoreRange($editable); + }); + }, + + /** + * @param {Object} layoutInfo + */ + showImageDialog: function (layoutInfo) { + var $dialog = layoutInfo.dialog(), + $editable = layoutInfo.editable(); + + editor.saveRange($editable); + dialog.showImageDialog($editable, $dialog).then(function (data) { + editor.restoreRange($editable); + + if (typeof data === 'string') { + // image url + editor.insertImage($editable, data); + } else { + // array of files + insertImages($editable, data); + } + }).fail(function () { + editor.restoreRange($editable); + }); + }, + + /** + * @param {Object} layoutInfo + */ + showVideoDialog: function (layoutInfo) { + var $dialog = layoutInfo.dialog(), + $editable = layoutInfo.editable(), + videoInfo = editor.getVideoInfo($editable); + + editor.saveRange($editable); + dialog.showVideoDialog($editable, $dialog, videoInfo).then(function (sUrl) { + editor.restoreRange($editable); + editor.insertVideo($editable, sUrl); + }).fail(function () { + editor.restoreRange($editable); + }); + }, + + /** + * @param {Object} layoutInfo + */ + showHelpDialog: function (layoutInfo) { + var $dialog = layoutInfo.dialog(), + $editable = layoutInfo.editable(); + + editor.saveRange($editable, true); + dialog.showHelpDialog($editable, $dialog).then(function () { + editor.restoreRange($editable); + }); + }, + + fullscreen: function (layoutInfo) { + var $editor = layoutInfo.editor(), + $toolbar = layoutInfo.toolbar(), + $editable = layoutInfo.editable(), + $codable = layoutInfo.codable(); + + var options = $editor.data('options'); + + var resize = function (size) { + $editor.css('width', size.w); + $editable.css('height', size.h); + $codable.css('height', size.h); + if ($codable.data('cmeditor')) { + $codable.data('cmeditor').setsize(null, size.h); + } + }; + + $editor.toggleClass('fullscreen'); + var isFullscreen = $editor.hasClass('fullscreen'); + if (isFullscreen) { + $editable.data('orgheight', $editable.css('height')); + + $window.on('resize', function () { + resize({ + w: $window.width(), + h: $window.height() - $toolbar.outerHeight() + }); + }).trigger('resize'); + + $scrollbar.css('overflow', 'hidden'); + } else { + $window.off('resize'); + resize({ + w: options.width || '', + h: $editable.data('orgheight') + }); + $scrollbar.css('overflow', 'visible'); + } + + toolbar.updateFullscreen($toolbar, isFullscreen); + }, + + codeview: function (layoutInfo) { + var $editor = layoutInfo.editor(), + $toolbar = layoutInfo.toolbar(), + $editable = layoutInfo.editable(), + $codable = layoutInfo.codable(), + $popover = layoutInfo.popover(); + + var options = $editor.data('options'); + + var cmEditor, server; + + $editor.toggleClass('codeview'); + + var isCodeview = $editor.hasClass('codeview'); + if (isCodeview) { + $codable.val(dom.html($editable, true)); + $codable.height($editable.height()); + toolbar.deactivate($toolbar); + popover.hide($popover); + $codable.focus(); + + // activate CodeMirror as codable + if (agent.hasCodeMirror) { + cmEditor = CodeMirror.fromTextArea($codable[0], options.codemirror); + + // CodeMirror TernServer + if (options.codemirror.tern) { + server = new CodeMirror.TernServer(options.codemirror.tern); + cmEditor.ternServer = server; + cmEditor.on('cursorActivity', function (cm) { + server.updateArgHints(cm); + }); + } + + // CodeMirror hasn't Padding. + cmEditor.setSize(null, $editable.outerHeight()); + $codable.data('cmEditor', cmEditor); + } + } else { + // deactivate CodeMirror as codable + if (agent.hasCodeMirror) { + cmEditor = $codable.data('cmEditor'); + $codable.val(cmEditor.getValue()); + cmEditor.toTextArea(); + } + + $editable.html(dom.value($codable) || dom.emptyPara); + $editable.height(options.height ? $codable.height() : 'auto'); + + toolbar.activate($toolbar); + $editable.focus(); + } + + toolbar.updateCodeview(layoutInfo.toolbar(), isCodeview); + } + }; + + var hMousedown = function (event) { + //preventDefault Selection for FF, IE8+ + if (dom.isImg(event.target)) { + event.preventDefault(); + } + }; + + var hToolbarAndPopoverUpdate = function (event) { + // delay for range after mouseup + setTimeout(function () { + var layoutInfo = makeLayoutInfo(event.currentTarget || event.target); + var styleInfo = editor.currentStyle(event.target); + if (!styleInfo) { return; } + + var isAirMode = layoutInfo.editor().data('options').airMode; + if (!isAirMode) { + toolbar.update(layoutInfo.toolbar(), styleInfo); + } + + popover.update(layoutInfo.popover(), styleInfo, isAirMode); + handle.update(layoutInfo.handle(), styleInfo, isAirMode); + }, 0); + }; + + var hScroll = function (event) { + var layoutInfo = makeLayoutInfo(event.currentTarget || event.target); + //hide popover and handle when scrolled + popover.hide(layoutInfo.popover()); + handle.hide(layoutInfo.handle()); + }; + + /** + * paste clipboard image + * + * @param {Event} event + */ + var hPasteClipboardImage = function (event) { + var clipboardData = event.originalEvent.clipboardData; + if (!clipboardData || !clipboardData.items || !clipboardData.items.length) { + return; + } + + var layoutInfo = makeLayoutInfo(event.currentTarget || event.target), + $editable = layoutInfo.editable(); + + var item = list.head(clipboardData.items); + var isClipboardImage = item.kind === 'file' && item.type.indexOf('image/') !== -1; + + if (isClipboardImage) { + insertImages($editable, [item.getAsFile()]); + } + + editor.afterCommand($editable); + }; + + /** + * `mousedown` event handler on $handle + * - controlSizing: resize image + * + * @param {MouseEvent} event + */ + var hHandleMousedown = function (event) { + if (dom.isControlSizing(event.target)) { + event.preventDefault(); + event.stopPropagation(); + + var layoutInfo = makeLayoutInfo(event.target), + $handle = layoutInfo.handle(), $popover = layoutInfo.popover(), + $editable = layoutInfo.editable(), + $editor = layoutInfo.editor(); + + var target = $handle.find('.note-control-selection').data('target'), + $target = $(target), posStart = $target.offset(), + scrollTop = $document.scrollTop(); + + var isAirMode = $editor.data('options').airMode; + + $document.on('mousemove', function (event) { + editor.resizeTo({ + x: event.clientX - posStart.left, + y: event.clientY - (posStart.top - scrollTop) + }, $target, !event.shiftKey); + + handle.update($handle, {image: target}, isAirMode); + popover.update($popover, {image: target}, isAirMode); + }).one('mouseup', function () { + $document.off('mousemove'); + }); + + if (!$target.data('ratio')) { // original ratio. + $target.data('ratio', $target.height() / $target.width()); + } + + editor.afterCommand($editable); + } + }; + + var hToolbarAndPopoverMousedown = function (event) { + // prevent default event when insertTable (FF, Webkit) + var $btn = $(event.target).closest('[data-event]'); + if ($btn.length) { + event.preventDefault(); + } + }; + + var hToolbarAndPopoverClick = function (event) { + var $btn = $(event.target).closest('[data-event]'); + + if ($btn.length) { + var eventName = $btn.attr('data-event'), + value = $btn.attr('data-value'), + hide = $btn.attr('data-hide'); + + var layoutInfo = makeLayoutInfo(event.target); + + event.preventDefault(); + + // before command: detect control selection element($target) + var $target; + if ($.inArray(eventName, ['resize', 'floatMe', 'removeMedia']) !== -1) { + var $selection = layoutInfo.handle().find('.note-control-selection'); + $target = $($selection.data('target')); + } + + // If requested, hide the popover when the button is clicked. + // Useful for things like showHelpDialog. + if (hide) { + $btn.parents('.popover').hide(); + } + + if (editor[eventName]) { // on command + var $editable = layoutInfo.editable(); + $editable.trigger('focus'); + editor[eventName]($editable, value, $target); + } else if (commands[eventName]) { + commands[eventName].call(this, layoutInfo); + } + + // after command + if ($.inArray(eventName, ['backColor', 'foreColor']) !== -1) { + var options = layoutInfo.editor().data('options', options); + var module = options.airMode ? popover : toolbar; + module.updateRecentColor(list.head($btn), eventName, value); + } + + hToolbarAndPopoverUpdate(event); + } + }; + + var EDITABLE_PADDING = 24; + /** + * `mousedown` event handler on statusbar + * + * @param {MouseEvent} event + */ + var hStatusbarMousedown = function (event) { + event.preventDefault(); + event.stopPropagation(); + + var $editable = makeLayoutInfo(event.target).editable(); + var nEditableTop = $editable.offset().top - $document.scrollTop(); + + var layoutInfo = makeLayoutInfo(event.currentTarget || event.target); + var options = layoutInfo.editor().data('options'); + + $document.on('mousemove', function (event) { + var nHeight = event.clientY - (nEditableTop + EDITABLE_PADDING); + + nHeight = (options.minHeight > 0) ? Math.max(nHeight, options.minHeight) : nHeight; + nHeight = (options.maxHeight > 0) ? Math.min(nHeight, options.maxHeight) : nHeight; + + $editable.height(nHeight); + }).one('mouseup', function () { + $document.off('mousemove'); + }); + }; + + var PX_PER_EM = 18; + var hDimensionPickerMove = function (event, options) { + var $picker = $(event.target.parentNode); // target is mousecatcher + var $dimensionDisplay = $picker.next(); + var $catcher = $picker.find('.note-dimension-picker-mousecatcher'); + var $highlighted = $picker.find('.note-dimension-picker-highlighted'); + var $unhighlighted = $picker.find('.note-dimension-picker-unhighlighted'); + + var posOffset; + // HTML5 with jQuery - e.offsetX is undefined in Firefox + if (event.offsetX === undefined) { + var posCatcher = $(event.target).offset(); + posOffset = { + x: event.pageX - posCatcher.left, + y: event.pageY - posCatcher.top + }; + } else { + posOffset = { + x: event.offsetX, + y: event.offsetY + }; + } + + var dim = { + c: Math.ceil(posOffset.x / PX_PER_EM) || 1, + r: Math.ceil(posOffset.y / PX_PER_EM) || 1 + }; + + $highlighted.css({ width: dim.c + 'em', height: dim.r + 'em' }); + $catcher.attr('data-value', dim.c + 'x' + dim.r); + + if (3 < dim.c && dim.c < options.insertTableMaxSize.col) { + $unhighlighted.css({ width: dim.c + 1 + 'em'}); + } + + if (3 < dim.r && dim.r < options.insertTableMaxSize.row) { + $unhighlighted.css({ height: dim.r + 1 + 'em'}); + } + + $dimensionDisplay.html(dim.c + ' x ' + dim.r); + }; + + /** + * Drag and Drop Events + * + * @param {Object} layoutInfo - layout Informations + * @param {Boolean} disableDragAndDrop + */ + var handleDragAndDropEvent = function (layoutInfo, disableDragAndDrop) { + if (disableDragAndDrop) { + // prevent default drop event + $document.on('drop', function (e) { + e.preventDefault(); + }); + } else { + attachDragAndDropEvent(layoutInfo); + } + }; + + /** + * attach Drag and Drop Events + * + * @param {Object} layoutInfo - layout Informations + */ + var attachDragAndDropEvent = function (layoutInfo) { + var collection = $(), + $dropzone = layoutInfo.dropzone, + $dropzoneMessage = layoutInfo.dropzone.find('.note-dropzone-message'); + + // show dropzone on dragenter when dragging a object to document. + $document.on('dragenter', function (e) { + var isCodeview = layoutInfo.editor.hasClass('codeview'); + if (!isCodeview && !collection.length) { + layoutInfo.editor.addClass('dragover'); + $dropzone.width(layoutInfo.editor.width()); + $dropzone.height(layoutInfo.editor.height()); + $dropzoneMessage.text('Drag Image Here'); + } + collection = collection.add(e.target); + }).on('dragleave', function (e) { + collection = collection.not(e.target); + if (!collection.length) { + layoutInfo.editor.removeClass('dragover'); + } + }).on('drop', function () { + collection = $(); + layoutInfo.editor.removeClass('dragover'); + }); + + // change dropzone's message on hover. + $dropzone.on('dragenter', function () { + $dropzone.addClass('hover'); + $dropzoneMessage.text('Drop Image'); + }).on('dragleave', function () { + $dropzone.removeClass('hover'); + $dropzoneMessage.text('Drag Image Here'); + }); + + // attach dropImage + $dropzone.on('drop', function (event) { + event.preventDefault(); + + var dataTransfer = event.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.files) { + var layoutInfo = makeLayoutInfo(event.currentTarget || event.target); + layoutInfo.editable().focus(); + insertImages(layoutInfo.editable(), dataTransfer.files); + } + }).on('dragover', false); // prevent default dragover event + }; + + + /** + * bind KeyMap on keydown + * + * @param {Object} layoutInfo + * @param {Object} keyMap + */ + this.bindKeyMap = function (layoutInfo, keyMap) { + var $editor = layoutInfo.editor; + var $editable = layoutInfo.editable; + + layoutInfo = makeLayoutInfo($editable); + + $editable.on('keydown', function (event) { + var aKey = []; + + // modifier + if (event.metaKey) { aKey.push('CMD'); } + if (event.ctrlKey && !event.altKey) { aKey.push('CTRL'); } + if (event.shiftKey) { aKey.push('SHIFT'); } + + // keycode + var keyName = key.nameFromCode[event.keyCode]; + if (keyName) { aKey.push(keyName); } + + var eventName = keyMap[aKey.join('+')]; + if (eventName) { + event.preventDefault(); + + if (editor[eventName]) { + editor[eventName]($editable, $editor.data('options')); + } else if (commands[eventName]) { + commands[eventName].call(this, layoutInfo); + } + } else if (key.isEdit(event.keyCode)) { + editor.afterCommand($editable); + } + }); + }; + + /** + * attach eventhandler + * + * @param {Object} layoutInfo - layout Informations + * @param {Object} options - user options include custom event handlers + * @param {Function} options.enter - enter key handler + */ + this.attach = function (layoutInfo, options) { + // handlers for editable + this.bindKeyMap(layoutInfo, options.keyMap[agent.isMac ? 'mac' : 'pc']); + layoutInfo.editable.on('mousedown', hMousedown); + layoutInfo.editable.on('keyup mouseup', hToolbarAndPopoverUpdate); + layoutInfo.editable.on('scroll', hScroll); + layoutInfo.editable.on('paste', hPasteClipboardImage); + + // handler for handle and popover + layoutInfo.handle.on('mousedown', hHandleMousedown); + layoutInfo.popover.on('click', hToolbarAndPopoverClick); + layoutInfo.popover.on('mousedown', hToolbarAndPopoverMousedown); + + // handlers for frame mode (toolbar, statusbar) + if (!options.airMode) { + // handler for drag and drop + handleDragAndDropEvent(layoutInfo, options.disableDragAndDrop); + + // handler for toolbar + layoutInfo.toolbar.on('click', hToolbarAndPopoverClick); + layoutInfo.toolbar.on('mousedown', hToolbarAndPopoverMousedown); + + // handler for statusbar + if (!options.disableResizeEditor) { + layoutInfo.statusbar.on('mousedown', hStatusbarMousedown); + } + } + + // handler for table dimension + var $catcherContainer = options.airMode ? layoutInfo.popover : + layoutInfo.toolbar; + var $catcher = $catcherContainer.find('.note-dimension-picker-mousecatcher'); + $catcher.css({ + width: options.insertTableMaxSize.col + 'em', + height: options.insertTableMaxSize.row + 'em' + }).on('mousemove', function (event) { + hDimensionPickerMove(event, options); + }); + + // save options on editor + layoutInfo.editor.data('options', options); + + // ret styleWithCSS for backColor / foreColor clearing with 'inherit'. + if (options.styleWithSpan && !agent.isMSIE) { + // protect FF Error: NS_ERROR_FAILURE: Failure + setTimeout(function () { + document.execCommand('styleWithCSS', 0, true); + }, 0); + } + + // History + var history = new History(layoutInfo.editable); + layoutInfo.editable.data('NoteHistory', history); + + // basic event callbacks (lowercase) + // enter, focus, blur, keyup, keydown + if (options.onenter) { + layoutInfo.editable.keypress(function (event) { + if (event.keyCode === key.ENTER) { options.onenter(event); } + }); + } + + if (options.onfocus) { layoutInfo.editable.focus(options.onfocus); } + if (options.onblur) { layoutInfo.editable.blur(options.onblur); } + if (options.onkeyup) { layoutInfo.editable.keyup(options.onkeyup); } + if (options.onkeydown) { layoutInfo.editable.keydown(options.onkeydown); } + if (options.onpaste) { layoutInfo.editable.on('paste', options.onpaste); } + + // callbacks for advanced features (camel) + if (options.onToolbarClick) { layoutInfo.toolbar.click(options.onToolbarClick); } + if (options.onChange) { + var hChange = function () { + editor.triggerOnChange(layoutInfo.editable); + }; + + if (agent.isMSIE) { + var sDomEvents = 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted'; + layoutInfo.editable.on(sDomEvents, hChange); + } else { + layoutInfo.editable.on('input', hChange); + } + } + + // All editor status will be saved on editable with jquery's data + // for support multiple editor with singleton object. + layoutInfo.editable.data('callbacks', { + onChange: options.onChange, + onAutoSave: options.onAutoSave, + onImageUpload: options.onImageUpload, + onImageUploadError: options.onImageUploadError, + onFileUpload: options.onFileUpload, + onFileUploadError: options.onFileUpload + }); + }; + + this.dettach = function (layoutInfo, options) { + layoutInfo.editable.off(); + + layoutInfo.popover.off(); + layoutInfo.handle.off(); + layoutInfo.dialog.off(); + + if (!options.airMode) { + layoutInfo.dropzone.off(); + layoutInfo.toolbar.off(); + layoutInfo.statusbar.off(); + } + }; + }; + + /** + * renderer + * + * rendering toolbar and editable + */ + var Renderer = function () { + + /** + * bootstrap button template + * + * @param {String} label + * @param {Object} [options] + * @param {String} [options.event] + * @param {String} [options.value] + * @param {String} [options.title] + * @param {String} [options.dropdown] + * @param {String} [options.hide] + */ + var tplButton = function (label, options) { + var event = options.event; + var value = options.value; + var title = options.title; + var className = options.className; + var dropdown = options.dropdown; + var hide = options.hide; + + return '<button type="button"' + + ' class="btn btn-default btn-sm btn-small' + + (className ? ' ' + className : '') + + (dropdown ? ' dropdown-toggle' : '') + + '"' + + (dropdown ? ' data-toggle="dropdown"' : '') + + (title ? ' title="' + title + '"' : '') + + (event ? ' data-event="' + event + '"' : '') + + (value ? ' data-value=\'' + value + '\'' : '') + + (hide ? ' data-hide=\'' + hide + '\'' : '') + + ' tabindex="-1">' + + label + + (dropdown ? ' <span class="caret"></span>' : '') + + '</button>' + + (dropdown || ''); + }; + + /** + * bootstrap icon button template + * + * @param {String} iconClassName + * @param {Object} [options] + * @param {String} [options.event] + * @param {String} [options.value] + * @param {String} [options.title] + * @param {String} [options.dropdown] + */ + var tplIconButton = function (iconClassName, options) { + var label = '<i class="' + iconClassName + '"></i>'; + return tplButton(label, options); + }; + + /** + * bootstrap popover template + * + * @param {String} className + * @param {String} content + */ + var tplPopover = function (className, content) { + return '<div class="' + className + ' popover bottom in" style="display: none;">' + + '<div class="arrow"></div>' + + '<div class="popover-content">' + + content + + '</div>' + + '</div>'; + }; + + /** + * bootstrap dialog template + * + * @param {String} className + * @param {String} [title] + * @param {String} body + * @param {String} [footer] + */ + var tplDialog = function (className, title, body, footer) { + return '<div class="' + className + ' modal" aria-hidden="false">' + + '<div class="modal-dialog">' + + '<div class="modal-content">' + + (title ? + '<div class="modal-header">' + + '<button type="button" class="close" aria-hidden="true" tabindex="-1">&times;</button>' + + '<h4 class="modal-title">' + title + '</h4>' + + '</div>' : '' + ) + + '<form class="note-modal-form">' + + '<div class="modal-body">' + + '<div class="row-fluid">' + body + '</div>' + + '</div>' + + (footer ? + '<div class="modal-footer">' + footer + '</div>' : '' + ) + + '</form>' + + '</div>' + + '</div>' + + '</div>'; + }; + + var tplButtonInfo = { + picture: function (lang) { + return tplIconButton('fa fa-picture-o icon-picture', { + event: 'showImageDialog', + title: lang.image.image, + hide: true + }); + }, + link: function (lang) { + return tplIconButton('fa fa-link icon-link', { + event: 'showLinkDialog', + title: lang.link.link, + hide: true + }); + }, + video: function (lang) { + return tplIconButton('fa fa-youtube-play icon-play', { + event: 'showVideoDialog', + title: lang.video.video, + hide: true + }); + }, + table: function (lang) { + var dropdown = '<ul class="note-table dropdown-menu">' + + '<div class="note-dimension-picker">' + + '<div class="note-dimension-picker-mousecatcher" data-event="insertTable" data-value="1x1"></div>' + + '<div class="note-dimension-picker-highlighted"></div>' + + '<div class="note-dimension-picker-unhighlighted"></div>' + + '</div>' + + '<div class="note-dimension-display"> 1 x 1 </div>' + + '</ul>'; + return tplIconButton('fa fa-table icon-table', { + title: lang.table.table, + dropdown: dropdown + }); + }, + style: function (lang, options) { + var items = options.styleTags.reduce(function (memo, v) { + var label = lang.style[v === 'p' ? 'normal' : v]; + return memo + '<li><a data-event="formatBlock" href="#" data-value="' + v + '">' + + ( + (v === 'p' || v === 'pre') ? label : + '<' + v + '>' + label + '</' + v + '>' + ) + + '</a></li>'; + }, ''); + + return tplIconButton('fa fa-magic icon-magic', { + title: lang.style.style, + dropdown: '<ul class="dropdown-menu">' + items + '</ul>' + }); + }, + fontname: function (lang, options) { + var items = options.fontNames.reduce(function (memo, v) { + if (!agent.isFontInstalled(v)) { return memo; } + return memo + '<li><a data-event="fontName" href="#" data-value="' + v + '">' + + '<i class="fa fa-check icon-ok"></i> ' + v + + '</a></li>'; + }, ''); + var label = '<span class="note-current-fontname">' + + options.defaultFontName + + '</span>'; + return tplButton(label, { + title: lang.font.name, + dropdown: '<ul class="dropdown-menu">' + items + '</ul>' + }); + }, + fontsize: function (lang, options) { + var items = options.fontSizes.reduce(function (memo, v) { + return memo + '<li><a data-event="fontSize" href="#" data-value="' + v + '">' + + '<i class="fa fa-check icon-ok"></i> ' + v + + '</a></li>'; + }, ''); + + var label = '<span class="note-current-fontsize">11</span>'; + return tplButton(label, { + title: lang.font.size, + dropdown: '<ul class="dropdown-menu">' + items + '</ul>' + }); + }, + + color: function (lang) { + var colorButtonLabel = '<i class="fa fa-font icon-font" style="color:black;background-color:yellow;"></i>'; + var colorButton = tplButton(colorButtonLabel, { + className: 'note-recent-color', + title: lang.color.recent, + event: 'color', + value: '{"backColor":"yellow"}' + }); + + var dropdown = '<ul class="dropdown-menu">' + + '<li>' + + '<div class="btn-group">' + + '<div class="note-palette-title">' + lang.color.background + '</div>' + + '<div class="note-color-reset" data-event="backColor"' + + ' data-value="inherit" title="' + lang.color.transparent + '">' + + lang.color.setTransparent + + '</div>' + + '<div class="note-color-palette" data-target-event="backColor"></div>' + + '</div>' + + '<div class="btn-group">' + + '<div class="note-palette-title">' + lang.color.foreground + '</div>' + + '<div class="note-color-reset" data-event="foreColor" data-value="inherit" title="' + lang.color.reset + '">' + + lang.color.resetToDefault + + '</div>' + + '<div class="note-color-palette" data-target-event="foreColor"></div>' + + '</div>' + + '</li>' + + '</ul>'; + + var moreButton = tplButton('', { + title: lang.color.more, + dropdown: dropdown + }); + + return colorButton + moreButton; + }, + bold: function (lang) { + return tplIconButton('fa fa-bold icon-bold', { + event: 'bold', + title: lang.font.bold + }); + }, + italic: function (lang) { + return tplIconButton('fa fa-italic icon-italic', { + event: 'italic', + title: lang.font.italic + }); + }, + underline: function (lang) { + return tplIconButton('fa fa-underline icon-underline', { + event: 'underline', + title: lang.font.underline + }); + }, + strikethrough: function (lang) { + return tplIconButton('fa fa-strikethrough icon-strikethrough', { + event: 'strikethrough', + title: lang.font.strikethrough + }); + }, + superscript: function (lang) { + return tplIconButton('fa fa-superscript icon-superscript', { + event: 'superscript', + title: lang.font.superscript + }); + }, + subscript: function (lang) { + return tplIconButton('fa fa-subscript icon-subscript', { + event: 'subscript', + title: lang.font.subscript + }); + }, + clear: function (lang) { + return tplIconButton('fa fa-eraser icon-eraser', { + event: 'removeFormat', + title: lang.font.clear + }); + }, + ul: function (lang) { + return tplIconButton('fa fa-list-ul icon-list-ul', { + event: 'insertUnorderedList', + title: lang.lists.unordered + }); + }, + ol: function (lang) { + return tplIconButton('fa fa-list-ol icon-list-ol', { + event: 'insertOrderedList', + title: lang.lists.ordered + }); + }, + paragraph: function (lang) { + var leftButton = tplIconButton('fa fa-align-left icon-align-left', { + title: lang.paragraph.left, + event: 'justifyLeft' + }); + var centerButton = tplIconButton('fa fa-align-center icon-align-center', { + title: lang.paragraph.center, + event: 'justifyCenter' + }); + var rightButton = tplIconButton('fa fa-align-right icon-align-right', { + title: lang.paragraph.right, + event: 'justifyRight' + }); + var justifyButton = tplIconButton('fa fa-align-justify icon-align-justify', { + title: lang.paragraph.justify, + event: 'justifyFull' + }); + + var outdentButton = tplIconButton('fa fa-outdent icon-indent-left', { + title: lang.paragraph.outdent, + event: 'outdent' + }); + var indentButton = tplIconButton('fa fa-indent icon-indent-right', { + title: lang.paragraph.indent, + event: 'indent' + }); + + var dropdown = '<div class="dropdown-menu">' + + '<div class="note-align btn-group">' + + leftButton + centerButton + rightButton + justifyButton + + '</div>' + + '<div class="note-list btn-group">' + + indentButton + outdentButton + + '</div>' + + '</div>'; + + return tplIconButton('fa fa-align-left icon-align-left', { + title: lang.paragraph.paragraph, + dropdown: dropdown + }); + }, + height: function (lang, options) { + var items = options.lineHeights.reduce(function (memo, v) { + return memo + '<li><a data-event="lineHeight" href="#" data-value="' + parseFloat(v) + '">' + + '<i class="fa fa-check icon-ok"></i> ' + v + + '</a></li>'; + }, ''); + + return tplIconButton('fa fa-text-height icon-text-height', { + title: lang.font.height, + dropdown: '<ul class="dropdown-menu">' + items + '</ul>' + }); + + }, + help: function (lang) { + return tplIconButton('fa fa-question icon-question', { + event: 'showHelpDialog', + title: lang.options.help, + hide: true + }); + }, + fullscreen: function (lang) { + return tplIconButton('fa fa-arrows-alt icon-fullscreen', { + event: 'fullscreen', + title: lang.options.fullscreen + }); + }, + codeview: function (lang) { + return tplIconButton('fa fa-code icon-code', { + event: 'codeview', + title: lang.options.codeview + }); + }, + undo: function (lang) { + return tplIconButton('fa fa-undo icon-undo', { + event: 'undo', + title: lang.history.undo + }); + }, + redo: function (lang) { + return tplIconButton('fa fa-repeat icon-repeat', { + event: 'redo', + title: lang.history.redo + }); + }, + hr: function (lang) { + return tplIconButton('fa fa-minus icon-hr', { + event: 'insertHorizontalRule', + title: lang.hr.insert + }); + } + }; + + var tplPopovers = function (lang, options) { + var tplLinkPopover = function () { + var linkButton = tplIconButton('fa fa-edit icon-edit', { + title: lang.link.edit, + event: 'showLinkDialog', + hide: true + }); + var unlinkButton = tplIconButton('fa fa-unlink icon-unlink', { + title: lang.link.unlink, + event: 'unlink' + }); + var content = '<a href="http://www.google.com" target="_blank">www.google.com</a>&nbsp;&nbsp;' + + '<div class="note-insert btn-group">' + + linkButton + unlinkButton + + '</div>'; + return tplPopover('note-link-popover', content); + }; + + var tplImagePopover = function () { + var fullButton = tplButton('<span class="note-fontsize-10">100%</span>', { + title: lang.image.resizeFull, + event: 'resize', + value: '1' + }); + var halfButton = tplButton('<span class="note-fontsize-10">50%</span>', { + title: lang.image.resizeHalf, + event: 'resize', + value: '0.5' + }); + var quarterButton = tplButton('<span class="note-fontsize-10">25%</span>', { + title: lang.image.resizeQuarter, + event: 'resize', + value: '0.25' + }); + + var leftButton = tplIconButton('fa fa-align-left icon-align-left', { + title: lang.image.floatLeft, + event: 'floatMe', + value: 'left' + }); + var rightButton = tplIconButton('fa fa-align-right icon-align-right', { + title: lang.image.floatRight, + event: 'floatMe', + value: 'right' + }); + var justifyButton = tplIconButton('fa fa-align-justify icon-align-justify', { + title: lang.image.floatNone, + event: 'floatMe', + value: 'none' + }); + + var removeButton = tplIconButton('fa fa-trash-o icon-trash', { + title: lang.image.remove, + event: 'removeMedia', + value: 'none' + }); + + var content = '<div class="btn-group">' + fullButton + halfButton + quarterButton + '</div>' + + '<div class="btn-group">' + leftButton + rightButton + justifyButton + '</div>' + + '<div class="btn-group">' + removeButton + '</div>'; + return tplPopover('note-image-popover', content); + }; + + var tplAirPopover = function () { + var content = ''; + for (var idx = 0, len = options.airPopover.length; idx < len; idx ++) { + var group = options.airPopover[idx]; + content += '<div class="note-' + group[0] + ' btn-group">'; + for (var i = 0, lenGroup = group[1].length; i < lenGroup; i++) { + content += tplButtonInfo[group[1][i]](lang, options); + } + content += '</div>'; + } + + return tplPopover('note-air-popover', content); + }; + + return '<div class="note-popover">' + + tplLinkPopover() + + tplImagePopover() + + (options.airMode ? tplAirPopover() : '') + + '</div>'; + }; + + var tplHandles = function () { + return '<div class="note-handle">' + + '<div class="note-control-selection">' + + '<div class="note-control-selection-bg"></div>' + + '<div class="note-control-holder note-control-nw"></div>' + + '<div class="note-control-holder note-control-ne"></div>' + + '<div class="note-control-holder note-control-sw"></div>' + + '<div class="note-control-sizing note-control-se"></div>' + + '<div class="note-control-selection-info"></div>' + + '</div>' + + '</div>'; + }; + + /** + * shortcut table template + * @param {String} title + * @param {String} body + */ + var tplShortcut = function (title, body) { + return '<table class="note-shortcut">' + + '<thead>' + + '<tr><th></th><th>' + title + '</th></tr>' + + '</thead>' + + '<tbody>' + body + '</tbody>' + + '</table>'; + }; + + var tplShortcutText = function (lang) { + var body = '<tr><td>โŒ˜ + B</td><td>' + lang.font.bold + '</td></tr>' + + '<tr><td>โŒ˜ + I</td><td>' + lang.font.italic + '</td></tr>' + + '<tr><td>โŒ˜ + U</td><td>' + lang.font.underline + '</td></tr>' + + '<tr><td>โŒ˜ + โ‡ง + S</td><td>' + lang.font.strikethrough + '</td></tr>' + + '<tr><td>โŒ˜ + \\</td><td>' + lang.font.clear + '</td></tr>'; + + return tplShortcut(lang.shortcut.textFormatting, body); + }; + + var tplShortcutAction = function (lang) { + var body = '<tr><td>โŒ˜ + Z</td><td>' + lang.history.undo + '</td></tr>' + + '<tr><td>โŒ˜ + โ‡ง + Z</td><td>' + lang.history.redo + '</td></tr>' + + '<tr><td>โŒ˜ + ]</td><td>' + lang.paragraph.indent + '</td></tr>' + + '<tr><td>โŒ˜ + [</td><td>' + lang.paragraph.outdent + '</td></tr>' + + '<tr><td>โŒ˜ + ENTER</td><td>' + lang.hr.insert + '</td></tr>'; + + return tplShortcut(lang.shortcut.action, body); + }; + + var tplShortcutPara = function (lang) { + var body = '<tr><td>โŒ˜ + โ‡ง + L</td><td>' + lang.paragraph.left + '</td></tr>' + + '<tr><td>โŒ˜ + โ‡ง + E</td><td>' + lang.paragraph.center + '</td></tr>' + + '<tr><td>โŒ˜ + โ‡ง + R</td><td>' + lang.paragraph.right + '</td></tr>' + + '<tr><td>โŒ˜ + โ‡ง + J</td><td>' + lang.paragraph.justify + '</td></tr>' + + '<tr><td>โŒ˜ + โ‡ง + NUM7</td><td>' + lang.lists.ordered + '</td></tr>' + + '<tr><td>โŒ˜ + โ‡ง + NUM8</td><td>' + lang.lists.unordered + '</td></tr>'; + + return tplShortcut(lang.shortcut.paragraphFormatting, body); + }; + + var tplShortcutStyle = function (lang) { + var body = '<tr><td>โŒ˜ + NUM0</td><td>' + lang.style.normal + '</td></tr>' + + '<tr><td>โŒ˜ + NUM1</td><td>' + lang.style.h1 + '</td></tr>' + + '<tr><td>โŒ˜ + NUM2</td><td>' + lang.style.h2 + '</td></tr>' + + '<tr><td>โŒ˜ + NUM3</td><td>' + lang.style.h3 + '</td></tr>' + + '<tr><td>โŒ˜ + NUM4</td><td>' + lang.style.h4 + '</td></tr>' + + '<tr><td>โŒ˜ + NUM5</td><td>' + lang.style.h5 + '</td></tr>' + + '<tr><td>โŒ˜ + NUM6</td><td>' + lang.style.h6 + '</td></tr>'; + + return tplShortcut(lang.shortcut.documentStyle, body); + }; + + var tplExtraShortcuts = function (lang, options) { + var extraKeys = options.extraKeys; + var body = ''; + for (var key in extraKeys) { + if (extraKeys.hasOwnProperty(key)) { + body += '<tr><td>' + key + '</td><td>' + extraKeys[key] + '</td></tr>'; + } + } + + return tplShortcut(lang.shortcut.extraKeys, body); + }; + + var tplShortcutTable = function (lang, options) { + var template = '<table class="note-shortcut-layout">' + + '<tbody>' + + '<tr><td>' + tplShortcutAction(lang, options) + '</td><td>' + tplShortcutText(lang, options) + '</td></tr>' + + '<tr><td>' + tplShortcutStyle(lang, options) + '</td><td>' + tplShortcutPara(lang, options) + '</td></tr>'; + if (options.extraKeys) { + template += '<tr><td colspan="2">' + tplExtraShortcuts(lang, options) + '</td></tr>'; + } + template += '</tbody></table>'; + return template; + }; + + var replaceMacKeys = function (sHtml) { + return sHtml.replace(/โŒ˜/g, 'Ctrl').replace(/โ‡ง/g, 'Shift'); + }; + + var tplDialogs = function (lang, options) { + var tplImageDialog = function () { + var body = + '<div class="note-group-select-from-files">' + + '<h5>' + lang.image.selectFromFiles + '</h5>' + + '<input class="note-image-input" type="file" name="files" accept="image/*" />' + + '</div>' + + '<h5>' + lang.image.url + '</h5>' + + '<input class="note-image-url form-control span12" type="text" />'; + var footer = '<button href="#" class="btn btn-primary note-image-btn disabled" disabled>' + lang.image.insert + '</button>'; + return tplDialog('note-image-dialog', lang.image.insert, body, footer); + }; + + var tplLinkDialog = function () { + var body = '<div class="form-group">' + + '<label>' + lang.link.textToDisplay + '</label>' + + '<input class="note-link-text form-control span12" type="text" />' + + '</div>' + + '<div class="form-group">' + + '<label>' + lang.link.url + '</label>' + + '<input class="note-link-url form-control span12" type="text" />' + + '</div>' + + (!options.disableLinkTarget ? + '<div class="checkbox">' + + '<label>' + '<input type="checkbox" checked> ' + + lang.link.openInNewWindow + + '</label>' + + '</div>' : '' + ); + var footer = '<button href="#" class="btn btn-primary note-link-btn disabled" disabled>' + lang.link.insert + '</button>'; + return tplDialog('note-link-dialog', lang.link.insert, body, footer); + }; + + var tplVideoDialog = function () { + var body = '<div class="form-group">' + + '<label>' + lang.video.url + '</label>&nbsp;<small class="text-muted">' + lang.video.providers + '</small>' + + '<input class="note-video-url form-control span12" type="text" />' + + '</div>'; + var footer = '<button href="#" class="btn btn-primary note-video-btn disabled" disabled>' + lang.video.insert + '</button>'; + return tplDialog('note-video-dialog', lang.video.insert, body, footer); + }; + + var tplHelpDialog = function () { + var body = '<a class="modal-close pull-right" aria-hidden="true" tabindex="-1">' + lang.shortcut.close + '</a>' + + '<div class="title">' + lang.shortcut.shortcuts + '</div>' + + (agent.isMac ? tplShortcutTable(lang, options) : replaceMacKeys(tplShortcutTable(lang, options))) + + '<p class="text-center">' + + '<a href="//hackerwins.github.io/summernote/" target="_blank">Summernote 0.5.9</a> ยท ' + + '<a href="//github.com/HackerWins/summernote" target="_blank">Project</a> ยท ' + + '<a href="//github.com/HackerWins/summernote/issues" target="_blank">Issues</a>' + + '</p>'; + return tplDialog('note-help-dialog', '', body, ''); + }; + + return '<div class="note-dialog">' + + tplImageDialog() + + tplLinkDialog() + + tplVideoDialog() + + tplHelpDialog() + + '</div>'; + }; + + var tplStatusbar = function () { + return '<div class="note-resizebar">' + + '<div class="note-icon-bar"></div>' + + '<div class="note-icon-bar"></div>' + + '<div class="note-icon-bar"></div>' + + '</div>'; + }; + + var representShortcut = function (str) { + if (agent.isMac) { + str = str.replace('CMD', 'โŒ˜').replace('SHIFT', 'โ‡ง'); + } + + return str.replace('BACKSLASH', '\\') + .replace('SLASH', '/') + .replace('LEFTBRACKET', '[') + .replace('RIGHTBRACKET', ']'); + }; + + /** + * createTooltip + * + * @param {jQuery} $container + * @param {Object} keyMap + * @param {String} [sPlacement] + */ + var createTooltip = function ($container, keyMap, sPlacement) { + var invertedKeyMap = func.invertObject(keyMap); + var $buttons = $container.find('button'); + + $buttons.each(function (i, elBtn) { + var $btn = $(elBtn); + var sShortcut = invertedKeyMap[$btn.data('event')]; + if (sShortcut) { + $btn.attr('title', function (i, v) { + return v + ' (' + representShortcut(sShortcut) + ')'; + }); + } + // bootstrap tooltip on btn-group bug + // https://github.com/twbs/bootstrap/issues/5687 + }).tooltip({ + container: 'body', + trigger: 'hover', + placement: sPlacement || 'top' + }).on('click', function () { + $(this).tooltip('hide'); + }); + }; + + // createPalette + var createPalette = function ($container, options) { + var colorInfo = options.colors; + $container.find('.note-color-palette').each(function () { + var $palette = $(this), eventName = $palette.attr('data-target-event'); + var paletteContents = []; + for (var row = 0, lenRow = colorInfo.length; row < lenRow; row++) { + var colors = colorInfo[row]; + var buttons = []; + for (var col = 0, lenCol = colors.length; col < lenCol; col++) { + var color = colors[col]; + buttons.push(['<button type="button" class="note-color-btn" style="background-color:', color, + ';" data-event="', eventName, + '" data-value="', color, + '" title="', color, + '" data-toggle="button" tabindex="-1"></button>'].join('')); + } + paletteContents.push('<div class="note-color-row">' + buttons.join('') + '</div>'); + } + $palette.html(paletteContents.join('')); + }); + }; + + /** + * create summernote layout (air mode) + * + * @param {jQuery} $holder + * @param {Object} options + */ + this.createLayoutByAirMode = function ($holder, options) { + var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc']; + var langInfo = $.summernote.lang[options.lang]; + + var id = func.uniqueId(); + + $holder.addClass('note-air-editor note-editable'); + $holder.attr({ + 'id': 'note-editor-' + id, + 'contentEditable': true + }); + + var body = document.body; + + // create Popover + var $popover = $(tplPopovers(langInfo, options)); + $popover.addClass('note-air-layout'); + $popover.attr('id', 'note-popover-' + id); + $popover.appendTo(body); + createTooltip($popover, keyMap); + createPalette($popover, options); + + // create Handle + var $handle = $(tplHandles()); + $handle.addClass('note-air-layout'); + $handle.attr('id', 'note-handle-' + id); + $handle.appendTo(body); + + // create Dialog + var $dialog = $(tplDialogs(langInfo, options)); + $dialog.addClass('note-air-layout'); + $dialog.attr('id', 'note-dialog-' + id); + $dialog.find('button.close, a.modal-close').click(function () { + $(this).closest('.modal').modal('hide'); + }); + $dialog.appendTo(body); + }; + + /** + * create summernote layout (normal mode) + * + * @param {jQuery} $holder + * @param {Object} options + */ + this.createLayoutByFrame = function ($holder, options) { + //01. create Editor + var $editor = $('<div class="note-editor"></div>'); + if (options.width) { + $editor.width(options.width); + } + + //02. statusbar (resizebar) + if (options.height > 0) { + $('<div class="note-statusbar">' + (options.disableResizeEditor ? '' : tplStatusbar()) + '</div>').prependTo($editor); + } + + //03. create Editable + var isContentEditable = !$holder.is(':disabled'); + var $editable = $('<div class="note-editable" contentEditable="' + isContentEditable + '"></div>') + .prependTo($editor); + if (options.height) { + $editable.height(options.height); + } + if (options.direction) { + $editable.attr('dir', options.direction); + } + + $editable.html(dom.html($holder) || dom.emptyPara); + + //031. create codable + $('<textarea class="note-codable"></textarea>').prependTo($editor); + + var langInfo = $.summernote.lang[options.lang]; + + //04. create Toolbar + var toolbarHTML = ''; + for (var idx = 0, len = options.toolbar.length; idx < len; idx ++) { + var groupName = options.toolbar[idx][0]; + var groupButtons = options.toolbar[idx][1]; + + toolbarHTML += '<div class="note-' + groupName + ' btn-group">'; + for (var i = 0, btnLength = groupButtons.length; i < btnLength; i++) { + // continue creating toolbar even if a button doesn't exist + if (!$.isFunction(tplButtonInfo[groupButtons[i]])) { continue; } + toolbarHTML += tplButtonInfo[groupButtons[i]](langInfo, options); + } + toolbarHTML += '</div>'; + } + + toolbarHTML = '<div class="note-toolbar btn-toolbar">' + toolbarHTML + '</div>'; + + var $toolbar = $(toolbarHTML).prependTo($editor); + var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc']; + createPalette($toolbar, options); + createTooltip($toolbar, keyMap, 'bottom'); + + //05. create Popover + var $popover = $(tplPopovers(langInfo, options)).prependTo($editor); + createPalette($popover, options); + createTooltip($popover, keyMap); + + //06. handle(control selection, ...) + $(tplHandles()).prependTo($editor); + + //07. create Dialog + var $dialog = $(tplDialogs(langInfo, options)).prependTo($editor); + $dialog.find('button.close, a.modal-close').click(function () { + $(this).closest('.modal').modal('hide'); + }); + + //08. create Dropzone + $('<div class="note-dropzone"><div class="note-dropzone-message"></div></div>').prependTo($editor); + + //09. Editor/Holder switch + $editor.insertAfter($holder); + $holder.hide(); + }; + + this.noteEditorFromHolder = function ($holder) { + if ($holder.hasClass('note-air-editor')) { + return $holder; + } else if ($holder.next().hasClass('note-editor')) { + return $holder.next(); + } else { + return $(); + } + }; + + /** + * create summernote layout + * + * @param {jQuery} $holder + * @param {Object} options + */ + this.createLayout = function ($holder, options) { + if (this.noteEditorFromHolder($holder).length) { + return; + } + + if (options.airMode) { + this.createLayoutByAirMode($holder, options); + } else { + this.createLayoutByFrame($holder, options); + } + }; + + /** + * returns layoutInfo from holder + * + * @param {jQuery} $holder - placeholder + * @returns {Object} + */ + this.layoutInfoFromHolder = function ($holder) { + var $editor = this.noteEditorFromHolder($holder); + if (!$editor.length) { return; } + + var layoutInfo = dom.buildLayoutInfo($editor); + // cache all properties. + for (var key in layoutInfo) { + if (layoutInfo.hasOwnProperty(key)) { + layoutInfo[key] = layoutInfo[key].call(); + } + } + return layoutInfo; + }; + + /** + * removeLayout + * + * @param {jQuery} $holder - placeholder + * @param {Object} layoutInfo + * @param {Object} options + * + */ + this.removeLayout = function ($holder, layoutInfo, options) { + if (options.airMode) { + $holder.removeClass('note-air-editor note-editable') + .removeAttr('id contentEditable'); + + layoutInfo.popover.remove(); + layoutInfo.handle.remove(); + layoutInfo.dialog.remove(); + } else { + $holder.html(layoutInfo.editable.html()); + + layoutInfo.editor.remove(); + $holder.show(); + } + }; + }; + + // jQuery namespace for summernote + $.summernote = $.summernote || {}; + + // extends default `settings` + $.extend($.summernote, settings); + + var renderer = new Renderer(); + var eventHandler = new EventHandler(); + + /** + * extend jquery fn + */ + $.fn.extend({ + /** + * initialize summernote + * - create editor layout and attach Mouse and keyboard events. + * + * @param {Object} options + * @returns {this} + */ + summernote: function (options) { + // extend default options + options = $.extend({}, $.summernote.options, options); + + this.each(function (idx, elHolder) { + var $holder = $(elHolder); + + // createLayout with options + renderer.createLayout($holder, options); + + var info = renderer.layoutInfoFromHolder($holder); + eventHandler.attach(info, options); + + // Textarea: auto filling the code before form submit. + if (dom.isTextarea($holder[0])) { + $holder.closest('form').submit(function () { + $holder.val($holder.code()); + }); + } + }); + + // focus on first editable element + if (this.first().length && options.focus) { + var info = renderer.layoutInfoFromHolder(this.first()); + info.editable.focus(); + } + + // callback on init + if (this.length && options.oninit) { + options.oninit(); + } + + return this; + }, + // + + /** + * get the HTML contents of note or set the HTML contents of note. + * + * @param {String} [sHTML] - HTML contents(optional, set) + * @returns {this|String} - context(set) or HTML contents of note(get). + */ + code: function (sHTML) { + // get the HTML contents of note + if (sHTML === undefined) { + var $holder = this.first(); + if (!$holder.length) { return; } + var info = renderer.layoutInfoFromHolder($holder); + if (!!(info && info.editable)) { + var isCodeview = info.editor.hasClass('codeview'); + if (isCodeview && agent.hasCodeMirror) { + info.codable.data('cmEditor').save(); + } + return isCodeview ? info.codable.val() : info.editable.html(); + } + return dom.isTextarea($holder[0]) ? $holder.val() : $holder.html(); + } + + // set the HTML contents of note + this.each(function (i, elHolder) { + var info = renderer.layoutInfoFromHolder($(elHolder)); + if (info && info.editable) { info.editable.html(sHTML); } + }); + + return this; + }, + + /** + * destroy Editor Layout and dettach Key and Mouse Event + * @returns {this} + */ + destroy: function () { + this.each(function (idx, elHolder) { + var $holder = $(elHolder); + + var info = renderer.layoutInfoFromHolder($holder); + if (!info || !info.editable) { return; } + + var options = info.editor.data('options'); + + eventHandler.dettach(info, options); + renderer.removeLayout($holder, info, options); + }); + + return this; + } + }); +})); (function() { $(function() { var action, controller, controllerObj, instance; controller = $("body").data("controller"); action = $("body").data("action"); @@ -26342,69 +34261,162 @@ }); }); }).call(this); (function() { - Storytime.Dashboard.BlogPosts = (function() { - function BlogPosts() {} - - BlogPosts.prototype.initNew = function() { - return (new Storytime.Dashboard.Editor()).init(); - }; - - BlogPosts.prototype.initEdit = function() { - return (new Storytime.Dashboard.Editor()).init(); - }; - - BlogPosts.prototype.initCreate = function() { - return (new Storytime.Dashboard.Editor()).init(); - }; - - BlogPosts.prototype.initUpdate = function() { - return (new Storytime.Dashboard.Editor()).init(); - }; - - return BlogPosts; - - })(); - -}).call(this); -(function() { Storytime.Dashboard.Editor = (function() { + var addUnloadHandler; + function Editor() {} Editor.prototype.init = function() { + var excerpt_character_limit, form, self, title_character_limit; + self = this; + this.initMedia(); + this.initWysiwyg(); + title_character_limit = $("#title_character_limit").data("limit"); + $("#title_character_limit").html(title_character_limit - $("#post_title").val().length); + $("#post_title").keypress(function(e) { + if ((e.which === 32 || e.which > 0x20) && ($("#post_title").val().length > title_character_limit - 1)) { + e.preventDefault(); + } + }).keyup(function() { + $("#title_character_limit").html(title_character_limit - $("#post_title").val().length); + }); + excerpt_character_limit = $("#excerpt_character_limit").data("limit"); + $("#excerpt_character_limit").html(excerpt_character_limit - $("#post_excerpt").val().length); + $("#post_excerpt").keypress(function(e) { + if ((e.which === 32 || e.which > 0x20) && ($("#post_excerpt").val().length > excerpt_character_limit - 1)) { + e.preventDefault(); + } + }).keyup(function() { + $("#excerpt_character_limit").html(excerpt_character_limit - $("#post_excerpt").val().length); + }); + if ($(".edit_post").length) { + form = $(".edit_post").last(); + $("#preview_post").click(function() { + self.autosavePostForm(); + }); + if ($("#main").data("preview")) { + $("#preview_post").trigger("click"); + window.open($("#preview_post").attr("href")); + } + } else { + form = $(".new_post").last(); + $("#preview_new_post").click(function() { + $("<input name='preview' type='hidden' value='true'>").insertAfter($(".new_post").children().first()); + $(".new_post").submit(); + }); + } + $(".datepicker").datepicker({ + dateFormat: "MM d, yy" + }); + $(".timepicker").timepicker({ + showPeriod: true + }); + addUnloadHandler(form); + }; + + Editor.prototype.initMedia = function() { var mediaInstance; mediaInstance = new Storytime.Dashboard.Media(); mediaInstance.initPagination(); mediaInstance.initInsert(); mediaInstance.initFeaturedImageSelector(); - $(document).on('shown.bs.modal', function() { - return mediaInstance.initUpload(); + return $(document).on('shown.bs.modal', function() { + mediaInstance.initUpload(); }); - return $(".wysiwyg").wysihtml5("deepExtend", { - parserRules: { - allowAllClasses: true + }; + + Editor.prototype.initWysiwyg = function() { + $(".summernote").summernote({ + codemirror: { + htmlMode: true, + lineNumbers: true, + lineWrapping: true, + mode: 'text/html', + theme: 'monokai' }, - html: true, - color: true, - customTemplates: { - "html": function(locale, options) { - var size; - size = options && options.size ? ' btn-' + options.size : ''; - return "<li>" + "<div class='btn-group'>" + "<a class='btn" + size + " btn-default' data-wysihtml5-action='change_view' title='" + locale.html.edit + "' tabindex='-1'><i class='glyphicon glyphicon-pencil'></i>&nbsp;&nbsp;Raw HTML Mode</a>" + "</div>" + "</li>"; - }, - "image": function(locale, options) { - var $modal, size; - size = options && options.size ? ' btn-' + options.size : ''; - $modal = $("#insertMediaModal").remove(); - return "<li>" + $modal[0].outerHTML + "<a class='btn" + size + " btn-default' data-wysihtml5-command='insertImage' title='" + locale.image.insert + "' tabindex='-1'><i class='glyphicon glyphicon-picture'></i></a>" + "</li>"; + height: 300, + minHeight: null, + maxHeight: null, + toolbar: [['style', ['style']], ['font', ['bold', 'italic', 'underline', 'superscript', 'subscript', 'strikethrough', 'clear']], ['color', ['color']], ['para', ['ul', 'ol', 'paragraph']], ['table', ['table']], ['insert', ['link', 'picture', 'video', 'hr']], ['view', ['fullscreen', 'codeview']], ['editing', ['undo', 'redo']], ['help', ['help']]], + onblur: function() { + $(".summernote").data("range", document.getSelection().getRangeAt(0)); + }, + onfocus: function() { + if ($(".edit_post").length) { + self.updateLater(1000); } + }, + onkeyup: function() { + form.data("unsaved-changes", true); + }, + onImageUpload: function(files, editor, $editable) { + $("#media_file").fileupload('send', { + files: files + }).success(function(result, textStatus, jqXHR) { + editor.insertImage($editable, result.file_url); + }); } }); + $(".note-image-dialog").on('shown.bs.modal', function() { + $(".note-image-dialog").find(".row-fluid").append("<div id='gallery_copy'> <h5>Gallery</h5> <div id='media_gallery'>" + $("#media_gallery").html() + "</div> </div>"); + }); + return $(".note-image-dialog").on('hide.bs.modal', function() { + $("#gallery_copy").remove(); + }); }; + Editor.prototype.autosavePostForm = function() { + var data, post_id, self; + self = this; + post_id = $("#main").data("post-id"); + data = []; + data.push({ + name: "post[draft_content]", + value: $(".summernote").code() + }); + return $.ajax({ + type: "POST", + url: "/dashboard/posts/" + post_id + "/autosaves", + data: data + }); + }; + + Editor.prototype.updateLater = function(timer) { + var self, timeoutId; + self = this; + if (timer == null) { + timer = 120000; + } + timeoutId = window.setTimeout((function() { + return self.autosavePostForm().done(function() { + var time_now; + self.updateLater(10000); + time_now = new Date().toLocaleTimeString(); + return $("#draft_last_saved_at").html("Draft saved at " + time_now); + }).fail(function() { + return console.log("Something went wrong while trying to autosave..."); + }); + }), timer); + }; + + addUnloadHandler = function(form) { + form.find("input, textarea").on("keyup", function() { + return form.data("unsaved-changes", true); + }); + $(".save").click(function() { + return form.data("unsaved-changes", false); + }); + return $(window).on("beforeunload", function() { + if (form.data("unsaved-changes")) { + return "You haven't saved your changes."; + } + }); + }; + return Editor; })(); }).call(this); @@ -26412,102 +34424,106 @@ Storytime.Dashboard.Media = (function() { function Media() {} Media.prototype.initIndex = function() { this.initUpload(); - return this.initPagination(); + this.initPagination(); }; Media.prototype.initPagination = function() { - return $(document).on('ajax:success', '#media_gallery .pagination a', function(e, data, status, xhr) { - return $("#media_gallery").html(data); + $(document).on('ajax:success', '#media_gallery .pagination a', function(e, data, status, xhr) { + $("#media_gallery").html(data); }); }; Media.prototype.initUpload = function() { var _ref; if (!this.uploadInitialized) { $('#media_file').fileupload({ dataType: 'json', done: function(e, data) { - return $("#media_gallery").prepend(data.result.html); + $("#media_gallery").prepend(data.result.html); }, progressall: function(e, data) { var progress; progress = parseInt(data.loaded / data.total * 100, 10); - return $('#progress .progress-bar').css('width', progress + '%'); + $('#progress .progress-bar').css('width', progress + '%'); } }).prop('disabled', !$.support.fileInput).parent().addClass((_ref = $.support.fileInput) != null ? _ref : { undefined: 'disabled' }); - return this.uploadInitialized = true; + this.uploadInitialized = true; + return; } }; Media.prototype.initInsert = function() { var self; self = this; - return $(document).on("click", ".insert-image-button", function(e) { - var wysihtml5Editor; + $(document).on("click", ".insert-image-button", function(e) { + var image_tag, node; e.preventDefault(); if (self.selectingFeatured) { $("#featured_media_id").val($(this).data("media-id")); if ($("#featured_media_image").length > 0) { $("#featured_media_image").attr("src", $(this).data("thumb-url")); } else { $("#featured_media_container").html("<img id='featured_media_image' src='" + ($(this).data("thumb-url")) + "' />"); } - return $("#insertMediaModal").modal("hide"); + $("#insertMediaModal").modal("hide"); } else { - wysihtml5Editor = $("textarea.wysiwyg").data("wysihtml5").editor; - wysihtml5Editor.composer.commands.exec("insertImage", { - src: $(this).data("image-url") - }); - return $("#insertMediaModal").modal("hide"); + image_tag = "<img src='" + ($(this).data("image-url")) + "' />"; + node = $(".summernote").data("range").createContextualFragment(image_tag); + $(".summernote").data("range").insertNode(node); + $(".note-image-dialog").modal("hide"); } }); }; Media.prototype.initFeaturedImageSelector = function() { var self; self = this; $(document).on("click", "#featured_media_button", function(e) { e.preventDefault(); self.selectingFeatured = true; - return $("#insertMediaModal").modal("show"); + $("#insertMediaModal").modal("show"); }); - return $(document).on('hidden.bs.modal', function() { - return self.selectingFeatured = false; + $(document).on('hidden.bs.modal', function() { + self.selectingFeatured = false; }); }; return Media; })(); }).call(this); (function() { - Storytime.Dashboard.Pages = (function() { - function Pages() {} + Storytime.Dashboard.Posts = (function() { + function Posts() {} - Pages.prototype.initNew = function() { - return (new Storytime.Dashboard.Editor()).init(); + Posts.prototype.initNew = function() { + this.editor = new Storytime.Dashboard.Editor(); + return this.editor.init(); }; - Pages.prototype.initEdit = function() { - return (new Storytime.Dashboard.Editor()).init(); + Posts.prototype.initEdit = function() { + this.editor = new Storytime.Dashboard.Editor(); + return this.editor.init(); }; - Pages.prototype.initCreate = function() { - return (new Storytime.Dashboard.Editor()).init(); + Posts.prototype.initCreate = function() { + this.editor = new Storytime.Dashboard.Editor(); + return this.editor.init(); }; - Pages.prototype.initUpdate = function() { - return (new Storytime.Dashboard.Editor()).init(); + Posts.prototype.initUpdate = function() { + this.editor = new Storytime.Dashboard.Editor(); + return this.editor.init(); }; - return Pages; + return Posts; })(); }).call(this); (function() { @@ -26539,10 +34555,25 @@ return Sites; })(); }).call(this); +(function() { + Storytime.Dashboard.Snippets = (function() { + function Snippets() {} + + Snippets.prototype.init = function() { + this.editor = new Storytime.Dashboard.Editor(); + this.editor.initMedia(); + return this.editor.initWysiwyg(); + }; + + return Snippets; + + })(); + +}).call(this); Storytime.Utilities = { controllerFromString: function(str){ if(!str) return null; var base = window; @@ -26563,6 +34594,6 @@ } } } ; -;TI"required_assets_digest;TI"%e0f9a76c7075f1f6c8a992cd7869897f;FI" _version;TI"%361c512b9086418778df946c0d278f91;F +;TI"required_assets_digest;TI"%6d1913b34b7cb7b2176fe302d97713fe;FI" _version;TI"%5ef14a844324cba3e114bd0123f88a5e;F \ No newline at end of file