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> </p>" ||
- innerHTML == "<p> </p><p> </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> <span class='current-font'>" + locale.font_styles.normal + "</span> <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'>×</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'>×</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> <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, "'") + "'" : "") + // cell title
+ (unselectable ? "" : " data-handler='selectDay' data-event='click' data-month='" + printDate.getMonth() + "' data-year='" + printDate.getFullYear() + "'") + ">" + // actions
+ (otherMonth && !showOtherMonths ? " " : // 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) ? " " : "");
+ }
+
+ // 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) ? " " : "") + 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"> </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"> </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 ? ' ' : '<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">×</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> ' +
+ '<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> <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> 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