1 // TODO: in future try to replace most inline compability checks with polyfills for code readability
3 // element.textContent polyfill.
4 // Unsupporting browsers: IE8
6 if (Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(Element.prototype, "textContent") && !Object.getOwnPropertyDescriptor(Element.prototype, "textContent").get) {
8 var innerText = Object.getOwnPropertyDescriptor(Element.prototype, "innerText");
9 Object.defineProperty(Element.prototype, "textContent",
12 return innerText.get.call(this);
15 return innerText.set.call(this, s);
22 // isArray polyfill for ie8
24 Array.isArray = function(arg) {
25 return Object.prototype.toString.call(arg) === '[object Array]';
28 * @license wysihtml5x v0.4.15
29 * https://github.com/Edicy/wysihtml5
31 * Author: Christopher Blum (https://github.com/tiff)
32 * Secondary author of extended features: Oliver Pulges (https://github.com/pulges)
34 * Copyright (C) 2012 XING AG
35 * Licensed under the MIT license (MIT)
50 INVISIBLE_SPACE: "\uFEFF",
52 EMPTY_FUNCTION: function() {},
64 * Rangy, a cross-browser JavaScript range and selection library
65 * http://code.google.com/p/rangy/
67 * Copyright 2014, Tim Down
68 * Licensed under the MIT license.
69 * Version: 1.3alpha.20140804
70 * Build date: 4 August 2014
73 (function(factory, global) {
74 if (typeof define == "function" && define.amd) {
75 // AMD. Register as an anonymous module.
78 TODO: look into this properly.
80 } else if (typeof exports == "object") {
81 // Node/CommonJS style for Browserify
82 module.exports = factory;
85 // No AMD or CommonJS support so we place Rangy in a global variable
86 global.rangy = factory();
90 var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
92 // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
93 // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
94 var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
95 "commonAncestorContainer"];
97 // Minimal set of methods required for DOM Level 2 Range compliance
98 var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
99 "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
100 "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
102 var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
104 // Subset of TextRange's full set of methods that we're interested in
105 var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
106 "setEndPoint", "getBoundingClientRect"];
108 /*----------------------------------------------------------------------------------------------------------------*/
110 // Trio of functions taken from Peter Michaux's article:
111 // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
112 function isHostMethod(o, p) {
114 return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
117 function isHostObject(o, p) {
118 return !!(typeof o[p] == OBJECT && o[p]);
121 function isHostProperty(o, p) {
122 return typeof o[p] != UNDEFINED;
125 // Creates a convenience function to save verbose repeated calls to tests functions
126 function createMultiplePropertyTest(testFunc) {
127 return function(o, props) {
128 var i = props.length;
130 if (!testFunc(o, props[i])) {
138 // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
139 var areHostMethods = createMultiplePropertyTest(isHostMethod);
140 var areHostObjects = createMultiplePropertyTest(isHostObject);
141 var areHostProperties = createMultiplePropertyTest(isHostProperty);
143 function isTextRange(range) {
144 return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
147 function getBody(doc) {
148 return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
154 version: "1.3alpha.20140804",
159 isHostMethod: isHostMethod,
160 isHostObject: isHostObject,
161 isHostProperty: isHostProperty,
162 areHostMethods: areHostMethods,
163 areHostObjects: areHostObjects,
164 areHostProperties: areHostProperties,
165 isTextRange: isTextRange,
175 preferTextRange: false,
176 autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
180 function consoleLog(msg) {
181 if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
182 window.console.log(msg);
186 function alertOrLog(msg, shouldAlert) {
194 function fail(reason) {
195 api.initialized = true;
196 api.supported = false;
197 alertOrLog("Rangy is not supported on this page in your browser. Reason: " + reason, api.config.alertOnFail);
203 alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
208 // Add utility extend() method
209 if ({}.hasOwnProperty) {
210 api.util.extend = function(obj, props, deep) {
212 for (var i in props) {
213 if (props.hasOwnProperty(i)) {
216 if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
217 api.util.extend(o, p, true);
222 // Special case for toString, which does not show up in for...in loops in IE <= 8
223 if (props.hasOwnProperty("toString")) {
224 obj.toString = props.toString;
229 fail("hasOwnProperty not supported");
232 // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
234 var el = document.createElement("div");
235 el.appendChild(document.createElement("span"));
236 var slice = [].slice;
239 if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
240 toArray = function(arrayLike) {
241 return slice.call(arrayLike, 0);
247 toArray = function(arrayLike) {
249 for (var i = 0, len = arrayLike.length; i < len; ++i) {
250 arr[i] = arrayLike[i];
256 api.util.toArray = toArray;
260 // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
261 // normalization of event properties
263 if (isHostMethod(document, "addEventListener")) {
264 addListener = function(obj, eventType, listener) {
265 obj.addEventListener(eventType, listener, false);
267 } else if (isHostMethod(document, "attachEvent")) {
268 addListener = function(obj, eventType, listener) {
269 obj.attachEvent("on" + eventType, listener);
272 fail("Document does not have required addEventListener or attachEvent method");
275 api.util.addListener = addListener;
277 var initListeners = [];
279 function getErrorDesc(ex) {
280 return ex.message || ex.description || String(ex);
285 if (api.initialized) {
289 var implementsDomRange = false, implementsTextRange = false;
291 // First, perform basic feature tests
293 if (isHostMethod(document, "createRange")) {
294 testRange = document.createRange();
295 if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
296 implementsDomRange = true;
300 var body = getBody(document);
301 if (!body || body.nodeName.toLowerCase() != "body") {
302 fail("No body element found");
306 if (body && isHostMethod(body, "createTextRange")) {
307 testRange = body.createTextRange();
308 if (isTextRange(testRange)) {
309 implementsTextRange = true;
313 if (!implementsDomRange && !implementsTextRange) {
314 fail("Neither Range nor TextRange are available");
318 api.initialized = true;
320 implementsDomRange: implementsDomRange,
321 implementsTextRange: implementsTextRange
324 // Initialize modules
325 var module, errorMessage;
326 for (var moduleName in modules) {
327 if ( (module = modules[moduleName]) instanceof Module ) {
328 module.init(module, api);
332 // Call init listeners
333 for (var i = 0, len = initListeners.length; i < len; ++i) {
335 initListeners[i](api);
337 errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
338 consoleLog(errorMessage);
343 // Allow external scripts to initialize this library in case it's loaded after the document has loaded
346 // Execute listener immediately if already initialized
347 api.addInitListener = function(listener) {
348 if (api.initialized) {
351 initListeners.push(listener);
355 var shimListeners = [];
357 api.addShimListener = function(listener) {
358 shimListeners.push(listener);
366 for (var i = 0, len = shimListeners.length; i < len; ++i) {
367 shimListeners[i](win);
371 api.shim = api.createMissingNativeApi = shim;
373 function Module(name, dependencies, initializer) {
375 this.dependencies = dependencies;
376 this.initialized = false;
377 this.supported = false;
378 this.initializer = initializer;
383 var requiredModuleNames = this.dependencies || [];
384 for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
385 moduleName = requiredModuleNames[i];
387 requiredModule = modules[moduleName];
388 if (!requiredModule || !(requiredModule instanceof Module)) {
389 throw new Error("required module '" + moduleName + "' not found");
392 requiredModule.init();
394 if (!requiredModule.supported) {
395 throw new Error("required module '" + moduleName + "' not supported");
399 // Now run initializer
400 this.initializer(this);
403 fail: function(reason) {
404 this.initialized = true;
405 this.supported = false;
406 throw new Error("Module '" + this.name + "' failed to load: " + reason);
409 warn: function(msg) {
410 api.warn("Module " + this.name + ": " + msg);
413 deprecationNotice: function(deprecated, replacement) {
414 api.warn("DEPRECATED: " + deprecated + " in module " + this.name + "is deprecated. Please use " +
415 replacement + " instead");
418 createError: function(msg) {
419 return new Error("Error in Rangy " + this.name + " module: " + msg);
423 function createModule(isCore, name, dependencies, initFunc) {
424 var newModule = new Module(name, dependencies, function(module) {
425 if (!module.initialized) {
426 module.initialized = true;
428 initFunc(api, module);
429 module.supported = true;
431 var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
432 consoleLog(errorMessage);
436 modules[name] = newModule;
439 api.createModule = function(name) {
440 // Allow 2 or 3 arguments (second argument is an optional array of dependencies)
441 var initFunc, dependencies;
442 if (arguments.length == 2) {
443 initFunc = arguments[1];
446 initFunc = arguments[2];
447 dependencies = arguments[1];
450 var module = createModule(false, name, dependencies, initFunc);
452 // Initialize the module immediately if the core is already initialized
453 if (api.initialized) {
458 api.createCoreModule = function(name, dependencies, initFunc) {
459 createModule(true, name, dependencies, initFunc);
462 /*----------------------------------------------------------------------------------------------------------------*/
464 // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
466 function RangePrototype() {}
467 api.RangePrototype = RangePrototype;
468 api.rangePrototype = new RangePrototype();
470 function SelectionPrototype() {}
471 api.selectionPrototype = new SelectionPrototype();
473 /*----------------------------------------------------------------------------------------------------------------*/
475 // Wait for document to load before running tests
477 var docReady = false;
479 var loadHandler = function(e) {
482 if (!api.initialized && api.config.autoInitialize) {
488 // Test whether we have window and document objects that we will need
489 if (typeof window == UNDEFINED) {
490 fail("No window found");
493 if (typeof document == UNDEFINED) {
494 fail("No document found");
498 if (isHostMethod(document, "addEventListener")) {
499 document.addEventListener("DOMContentLoaded", loadHandler, false);
502 // Add a fallback in case the DOMContentLoaded event isn't supported
503 addListener(window, "load", loadHandler);
505 /*----------------------------------------------------------------------------------------------------------------*/
507 // DOM utility methods used by Rangy
508 api.createCoreModule("DomUtil", [], function(api, module) {
509 var UNDEF = "undefined";
512 // Perform feature tests
513 if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
514 module.fail("document missing a Node creation method");
517 if (!util.isHostMethod(document, "getElementsByTagName")) {
518 module.fail("document missing getElementsByTagName method");
521 var el = document.createElement("div");
522 if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
523 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
524 module.fail("Incomplete Element implementation");
527 // innerHTML is required for Range's createContextualFragment method
528 if (!util.isHostProperty(el, "innerHTML")) {
529 module.fail("Element is missing innerHTML property");
532 var textNode = document.createTextNode("test");
533 if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
534 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
535 !util.areHostProperties(textNode, ["data"]))) {
536 module.fail("Incomplete Text Node implementation");
539 /*----------------------------------------------------------------------------------------------------------------*/
541 // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
542 // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
543 // contains just the document as a single element and the value searched for is the document.
544 var arrayContains = /*Array.prototype.indexOf ?
546 return arr.indexOf(val) > -1;
552 if (arr[i] === val) {
559 // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
560 function isHtmlNamespace(node) {
562 return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
565 function parentElement(node) {
566 var parent = node.parentNode;
567 return (parent.nodeType == 1) ? parent : null;
570 function getNodeIndex(node) {
572 while( (node = node.previousSibling) ) {
578 function getNodeLength(node) {
579 switch (node.nodeType) {
587 return node.childNodes.length;
591 function getCommonAncestor(node1, node2) {
592 var ancestors = [], n;
593 for (n = node1; n; n = n.parentNode) {
597 for (n = node2; n; n = n.parentNode) {
598 if (arrayContains(ancestors, n)) {
606 function isAncestorOf(ancestor, descendant, selfIsAncestor) {
607 var n = selfIsAncestor ? descendant : descendant.parentNode;
609 if (n === ancestor) {
618 function isOrIsAncestorOf(ancestor, descendant) {
619 return isAncestorOf(ancestor, descendant, true);
622 function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
623 var p, n = selfIsAncestor ? node : node.parentNode;
626 if (p === ancestor) {
634 function isCharacterDataNode(node) {
635 var t = node.nodeType;
636 return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
639 function isTextOrCommentNode(node) {
643 var t = node.nodeType;
644 return t == 3 || t == 8 ; // Text or Comment
647 function insertAfter(node, precedingNode) {
648 var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
650 parent.insertBefore(node, nextNode);
652 parent.appendChild(node);
657 // Note that we cannot use splitText() because it is bugridden in IE 9.
658 function splitDataNode(node, index, positionsToPreserve) {
659 var newNode = node.cloneNode(false);
660 newNode.deleteData(0, index);
661 node.deleteData(index, node.length - index);
662 insertAfter(newNode, node);
664 // Preserve positions
665 if (positionsToPreserve) {
666 for (var i = 0, position; position = positionsToPreserve[i++]; ) {
667 // Handle case where position was inside the portion of node after the split point
668 if (position.node == node && position.offset > index) {
669 position.node = newNode;
670 position.offset -= index;
672 // Handle the case where the position is a node offset within node's parent
673 else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
681 function getDocument(node) {
682 if (node.nodeType == 9) {
684 } else if (typeof node.ownerDocument != UNDEF) {
685 return node.ownerDocument;
686 } else if (typeof node.document != UNDEF) {
687 return node.document;
688 } else if (node.parentNode) {
689 return getDocument(node.parentNode);
691 throw module.createError("getDocument: no document found for node");
695 function getWindow(node) {
696 var doc = getDocument(node);
697 if (typeof doc.defaultView != UNDEF) {
698 return doc.defaultView;
699 } else if (typeof doc.parentWindow != UNDEF) {
700 return doc.parentWindow;
702 throw module.createError("Cannot get a window object for node");
706 function getIframeDocument(iframeEl) {
707 if (typeof iframeEl.contentDocument != UNDEF) {
708 return iframeEl.contentDocument;
709 } else if (typeof iframeEl.contentWindow != UNDEF) {
710 return iframeEl.contentWindow.document;
712 throw module.createError("getIframeDocument: No Document object found for iframe element");
716 function getIframeWindow(iframeEl) {
717 if (typeof iframeEl.contentWindow != UNDEF) {
718 return iframeEl.contentWindow;
719 } else if (typeof iframeEl.contentDocument != UNDEF) {
720 return iframeEl.contentDocument.defaultView;
722 throw module.createError("getIframeWindow: No Window object found for iframe element");
726 // This looks bad. Is it worth it?
727 function isWindow(obj) {
728 return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
731 function getContentDocument(obj, module, methodName) {
738 // Test if a DOM node has been passed and obtain a document object for it if so
739 else if (util.isHostProperty(obj, "nodeType")) {
740 doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
741 getIframeDocument(obj) : getDocument(obj);
744 // Test if the doc parameter appears to be a Window object
745 else if (isWindow(obj)) {
750 throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
756 function getRootContainer(node) {
758 while ( (parent = node.parentNode) ) {
764 function comparePoints(nodeA, offsetA, nodeB, offsetB) {
765 // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
766 var nodeC, root, childA, childB, n;
767 if (nodeA == nodeB) {
768 // Case 1: nodes are the same
769 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
770 } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
771 // Case 2: node C (container B or an ancestor) is a child node of A
772 return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
773 } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
774 // Case 3: node C (container A or an ancestor) is a child node of B
775 return getNodeIndex(nodeC) < offsetB ? -1 : 1;
777 root = getCommonAncestor(nodeA, nodeB);
779 throw new Error("comparePoints error: nodes have no common ancestor");
782 // Case 4: containers are siblings or descendants of siblings
783 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
784 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
786 if (childA === childB) {
787 // This shouldn't be possible
788 throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
794 } else if (n === childB) {
803 /*----------------------------------------------------------------------------------------------------------------*/
805 // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
806 var crashyTextNodes = false;
808 function isBrokenNode(node) {
819 var el = document.createElement("b");
821 var textNode = el.firstChild;
822 el.innerHTML = "<br>";
823 crashyTextNodes = isBrokenNode(textNode);
825 api.features.crashyTextNodes = crashyTextNodes;
828 /*----------------------------------------------------------------------------------------------------------------*/
830 function inspectNode(node) {
834 if (crashyTextNodes && isBrokenNode(node)) {
835 return "[Broken node]";
837 if (isCharacterDataNode(node)) {
838 return '"' + node.data + '"';
840 if (node.nodeType == 1) {
841 var idAttr = node.id ? ' id="' + node.id + '"' : "";
842 return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
844 return node.nodeName;
847 function fragmentFromNodeChildren(node) {
848 var fragment = getDocument(node).createDocumentFragment(), child;
849 while ( (child = node.firstChild) ) {
850 fragment.appendChild(child);
855 var getComputedStyleProperty;
856 if (typeof window.getComputedStyle != UNDEF) {
857 getComputedStyleProperty = function(el, propName) {
858 return getWindow(el).getComputedStyle(el, null)[propName];
860 } else if (typeof document.documentElement.currentStyle != UNDEF) {
861 getComputedStyleProperty = function(el, propName) {
862 return el.currentStyle[propName];
865 module.fail("No means of obtaining computed style properties found");
868 function NodeIterator(root) {
873 NodeIterator.prototype = {
876 hasNext: function() {
881 var n = this._current = this._next;
884 child = n.firstChild;
889 while ((n !== this.root) && !(next = n.nextSibling)) {
895 return this._current;
899 this._current = this._next = this.root = null;
903 function createIterator(root) {
904 return new NodeIterator(root);
907 function DomPosition(node, offset) {
909 this.offset = offset;
912 DomPosition.prototype = {
913 equals: function(pos) {
914 return !!pos && this.node === pos.node && this.offset == pos.offset;
917 inspect: function() {
918 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
921 toString: function() {
922 return this.inspect();
926 function DOMException(codeName) {
927 this.code = this[codeName];
928 this.codeName = codeName;
929 this.message = "DOMException: " + this.codeName;
932 DOMException.prototype = {
934 HIERARCHY_REQUEST_ERR: 3,
935 WRONG_DOCUMENT_ERR: 4,
936 NO_MODIFICATION_ALLOWED_ERR: 7,
938 NOT_SUPPORTED_ERR: 9,
939 INVALID_STATE_ERR: 11,
940 INVALID_NODE_TYPE_ERR: 24
943 DOMException.prototype.toString = function() {
948 arrayContains: arrayContains,
949 isHtmlNamespace: isHtmlNamespace,
950 parentElement: parentElement,
951 getNodeIndex: getNodeIndex,
952 getNodeLength: getNodeLength,
953 getCommonAncestor: getCommonAncestor,
954 isAncestorOf: isAncestorOf,
955 isOrIsAncestorOf: isOrIsAncestorOf,
956 getClosestAncestorIn: getClosestAncestorIn,
957 isCharacterDataNode: isCharacterDataNode,
958 isTextOrCommentNode: isTextOrCommentNode,
959 insertAfter: insertAfter,
960 splitDataNode: splitDataNode,
961 getDocument: getDocument,
962 getWindow: getWindow,
963 getIframeWindow: getIframeWindow,
964 getIframeDocument: getIframeDocument,
965 getBody: util.getBody,
967 getContentDocument: getContentDocument,
968 getRootContainer: getRootContainer,
969 comparePoints: comparePoints,
970 isBrokenNode: isBrokenNode,
971 inspectNode: inspectNode,
972 getComputedStyleProperty: getComputedStyleProperty,
973 fragmentFromNodeChildren: fragmentFromNodeChildren,
974 createIterator: createIterator,
975 DomPosition: DomPosition
978 api.DOMException = DOMException;
981 /*----------------------------------------------------------------------------------------------------------------*/
983 // Pure JavaScript implementation of DOM Range
984 api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
987 var DomPosition = dom.DomPosition;
988 var DOMException = api.DOMException;
990 var isCharacterDataNode = dom.isCharacterDataNode;
991 var getNodeIndex = dom.getNodeIndex;
992 var isOrIsAncestorOf = dom.isOrIsAncestorOf;
993 var getDocument = dom.getDocument;
994 var comparePoints = dom.comparePoints;
995 var splitDataNode = dom.splitDataNode;
996 var getClosestAncestorIn = dom.getClosestAncestorIn;
997 var getNodeLength = dom.getNodeLength;
998 var arrayContains = dom.arrayContains;
999 var getRootContainer = dom.getRootContainer;
1000 var crashyTextNodes = api.features.crashyTextNodes;
1002 /*----------------------------------------------------------------------------------------------------------------*/
1004 // Utility functions
1006 function isNonTextPartiallySelected(node, range) {
1007 return (node.nodeType != 3) &&
1008 (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
1011 function getRangeDocument(range) {
1012 return range.document || getDocument(range.startContainer);
1015 function getBoundaryBeforeNode(node) {
1016 return new DomPosition(node.parentNode, getNodeIndex(node));
1019 function getBoundaryAfterNode(node) {
1020 return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
1023 function insertNodeAtPosition(node, n, o) {
1024 var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
1025 if (isCharacterDataNode(n)) {
1026 if (o == n.length) {
1027 dom.insertAfter(node, n);
1029 n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
1031 } else if (o >= n.childNodes.length) {
1032 n.appendChild(node);
1034 n.insertBefore(node, n.childNodes[o]);
1036 return firstNodeInserted;
1039 function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
1040 assertRangeValid(rangeA);
1041 assertRangeValid(rangeB);
1043 if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
1044 throw new DOMException("WRONG_DOCUMENT_ERR");
1047 var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
1048 endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
1050 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1053 function cloneSubtree(iterator) {
1054 var partiallySelected;
1055 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1056 partiallySelected = iterator.isPartiallySelectedSubtree();
1057 node = node.cloneNode(!partiallySelected);
1058 if (partiallySelected) {
1059 subIterator = iterator.getSubtreeIterator();
1060 node.appendChild(cloneSubtree(subIterator));
1061 subIterator.detach();
1064 if (node.nodeType == 10) { // DocumentType
1065 throw new DOMException("HIERARCHY_REQUEST_ERR");
1067 frag.appendChild(node);
1072 function iterateSubtree(rangeIterator, func, iteratorState) {
1074 iteratorState = iteratorState || { stop: false };
1075 for (var node, subRangeIterator; node = rangeIterator.next(); ) {
1076 if (rangeIterator.isPartiallySelectedSubtree()) {
1077 if (func(node) === false) {
1078 iteratorState.stop = true;
1081 // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
1082 // the node selected by the Range.
1083 subRangeIterator = rangeIterator.getSubtreeIterator();
1084 iterateSubtree(subRangeIterator, func, iteratorState);
1085 subRangeIterator.detach();
1086 if (iteratorState.stop) {
1091 // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1093 it = dom.createIterator(node);
1094 while ( (n = it.next()) ) {
1095 if (func(n) === false) {
1096 iteratorState.stop = true;
1104 function deleteSubtree(iterator) {
1106 while (iterator.next()) {
1107 if (iterator.isPartiallySelectedSubtree()) {
1108 subIterator = iterator.getSubtreeIterator();
1109 deleteSubtree(subIterator);
1110 subIterator.detach();
1117 function extractSubtree(iterator) {
1118 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1120 if (iterator.isPartiallySelectedSubtree()) {
1121 node = node.cloneNode(false);
1122 subIterator = iterator.getSubtreeIterator();
1123 node.appendChild(extractSubtree(subIterator));
1124 subIterator.detach();
1128 if (node.nodeType == 10) { // DocumentType
1129 throw new DOMException("HIERARCHY_REQUEST_ERR");
1131 frag.appendChild(node);
1136 function getNodesInRange(range, nodeTypes, filter) {
1137 var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
1138 var filterExists = !!filter;
1139 if (filterNodeTypes) {
1140 regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
1144 iterateSubtree(new RangeIterator(range, false), function(node) {
1145 if (filterNodeTypes && !regex.test(node.nodeType)) {
1148 if (filterExists && !filter(node)) {
1151 // Don't include a boundary container if it is a character data node and the range does not contain any
1152 // of its character data. See issue 190.
1153 var sc = range.startContainer;
1154 if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
1158 var ec = range.endContainer;
1159 if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
1168 function inspect(range) {
1169 var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
1170 return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
1171 dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
1174 /*----------------------------------------------------------------------------------------------------------------*/
1176 // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
1178 function RangeIterator(range, clonePartiallySelectedTextNodes) {
1180 this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
1183 if (!range.collapsed) {
1184 this.sc = range.startContainer;
1185 this.so = range.startOffset;
1186 this.ec = range.endContainer;
1187 this.eo = range.endOffset;
1188 var root = range.commonAncestorContainer;
1190 if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
1191 this.isSingleCharacterDataNode = true;
1192 this._first = this._last = this._next = this.sc;
1194 this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
1195 this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
1196 this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
1197 this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
1202 RangeIterator.prototype = {
1207 isSingleCharacterDataNode: false,
1210 this._current = null;
1211 this._next = this._first;
1214 hasNext: function() {
1215 return !!this._next;
1219 // Move to next node
1220 var current = this._current = this._next;
1222 this._next = (current !== this._last) ? current.nextSibling : null;
1224 // Check for partially selected text nodes
1225 if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
1226 if (current === this.ec) {
1227 (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
1229 if (this._current === this.sc) {
1230 (current = current.cloneNode(true)).deleteData(0, this.so);
1238 remove: function() {
1239 var current = this._current, start, end;
1241 if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
1242 start = (current === this.sc) ? this.so : 0;
1243 end = (current === this.ec) ? this.eo : current.length;
1245 current.deleteData(start, end - start);
1248 if (current.parentNode) {
1249 current.parentNode.removeChild(current);
1255 // Checks if the current node is partially selected
1256 isPartiallySelectedSubtree: function() {
1257 var current = this._current;
1258 return isNonTextPartiallySelected(current, this.range);
1261 getSubtreeIterator: function() {
1263 if (this.isSingleCharacterDataNode) {
1264 subRange = this.range.cloneRange();
1265 subRange.collapse(false);
1267 subRange = new Range(getRangeDocument(this.range));
1268 var current = this._current;
1269 var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
1271 if (isOrIsAncestorOf(current, this.sc)) {
1272 startContainer = this.sc;
1273 startOffset = this.so;
1275 if (isOrIsAncestorOf(current, this.ec)) {
1276 endContainer = this.ec;
1277 endOffset = this.eo;
1280 updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
1282 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
1285 detach: function() {
1286 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
1290 /*----------------------------------------------------------------------------------------------------------------*/
1292 var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1293 var rootContainerNodeTypes = [2, 9, 11];
1294 var readonlyNodeTypes = [5, 6, 10, 12];
1295 var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1296 var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1298 function createAncestorFinder(nodeTypes) {
1299 return function(node, selfIsAncestor) {
1300 var t, n = selfIsAncestor ? node : node.parentNode;
1303 if (arrayContains(nodeTypes, t)) {
1312 var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1313 var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1314 var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1316 function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1317 if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1318 throw new DOMException("INVALID_NODE_TYPE_ERR");
1322 function assertValidNodeType(node, invalidTypes) {
1323 if (!arrayContains(invalidTypes, node.nodeType)) {
1324 throw new DOMException("INVALID_NODE_TYPE_ERR");
1328 function assertValidOffset(node, offset) {
1329 if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1330 throw new DOMException("INDEX_SIZE_ERR");
1334 function assertSameDocumentOrFragment(node1, node2) {
1335 if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1336 throw new DOMException("WRONG_DOCUMENT_ERR");
1340 function assertNodeNotReadOnly(node) {
1341 if (getReadonlyAncestor(node, true)) {
1342 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1346 function assertNode(node, codeName) {
1348 throw new DOMException(codeName);
1352 function isOrphan(node) {
1353 return (crashyTextNodes && dom.isBrokenNode(node)) ||
1354 !arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
1357 function isValidOffset(node, offset) {
1358 return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1361 function isRangeValid(range) {
1362 return (!!range.startContainer && !!range.endContainer &&
1363 !isOrphan(range.startContainer) &&
1364 !isOrphan(range.endContainer) &&
1365 isValidOffset(range.startContainer, range.startOffset) &&
1366 isValidOffset(range.endContainer, range.endOffset));
1369 function assertRangeValid(range) {
1370 if (!isRangeValid(range)) {
1371 throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
1375 /*----------------------------------------------------------------------------------------------------------------*/
1377 // Test the browser's innerHTML support to decide how to implement createContextualFragment
1378 var styleEl = document.createElement("style");
1379 var htmlParsingConforms = false;
1381 styleEl.innerHTML = "<b>x</b>";
1382 htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1387 api.features.htmlParsingConforms = htmlParsingConforms;
1389 var createContextualFragment = htmlParsingConforms ?
1391 // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1392 // discussion and base code for this implementation at issue 67.
1393 // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1394 // Thanks to Aleks Williams.
1395 function(fragmentStr) {
1396 // "Let node the context object's start's node."
1397 var node = this.startContainer;
1398 var doc = getDocument(node);
1400 // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1401 // exception and abort these steps."
1403 throw new DOMException("INVALID_STATE_ERR");
1406 // "Let element be as follows, depending on node's interface:"
1407 // Document, Document Fragment: null
1411 if (node.nodeType == 1) {
1414 // "Text, Comment: node's parentElement"
1415 } else if (isCharacterDataNode(node)) {
1416 el = dom.parentElement(node);
1419 // "If either element is null or element's ownerDocument is an HTML document
1420 // and element's local name is "html" and element's namespace is the HTML
1422 if (el === null || (
1423 el.nodeName == "HTML" &&
1424 dom.isHtmlNamespace(getDocument(el).documentElement) &&
1425 dom.isHtmlNamespace(el)
1428 // "let element be a new Element with "body" as its local name and the HTML
1429 // namespace as its namespace.""
1430 el = doc.createElement("body");
1432 el = el.cloneNode(false);
1435 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1436 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1437 // "In either case, the algorithm must be invoked with fragment as the input
1438 // and element as the context element."
1439 el.innerHTML = fragmentStr;
1441 // "If this raises an exception, then abort these steps. Otherwise, let new
1442 // children be the nodes returned."
1444 // "Let fragment be a new DocumentFragment."
1445 // "Append all new children to fragment."
1446 // "Return fragment."
1447 return dom.fragmentFromNodeChildren(el);
1450 // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1451 // previous versions of Rangy used (with the exception of using a body element rather than a div)
1452 function(fragmentStr) {
1453 var doc = getRangeDocument(this);
1454 var el = doc.createElement("body");
1455 el.innerHTML = fragmentStr;
1457 return dom.fragmentFromNodeChildren(el);
1460 function splitRangeBoundaries(range, positionsToPreserve) {
1461 assertRangeValid(range);
1463 var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
1464 var startEndSame = (sc === ec);
1466 if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1467 splitDataNode(ec, eo, positionsToPreserve);
1470 if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1471 sc = splitDataNode(sc, so, positionsToPreserve);
1475 } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
1480 range.setStartAndEnd(sc, so, ec, eo);
1483 function rangeToHtml(range) {
1484 assertRangeValid(range);
1485 var container = range.commonAncestorContainer.parentNode.cloneNode(false);
1486 container.appendChild( range.cloneContents() );
1487 return container.innerHTML;
1490 /*----------------------------------------------------------------------------------------------------------------*/
1492 var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1493 "commonAncestorContainer"];
1495 var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1496 var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1498 util.extend(api.rangePrototype, {
1499 compareBoundaryPoints: function(how, range) {
1500 assertRangeValid(this);
1501 assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1503 var nodeA, offsetA, nodeB, offsetB;
1504 var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1505 var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1506 nodeA = this[prefixA + "Container"];
1507 offsetA = this[prefixA + "Offset"];
1508 nodeB = range[prefixB + "Container"];
1509 offsetB = range[prefixB + "Offset"];
1510 return comparePoints(nodeA, offsetA, nodeB, offsetB);
1513 insertNode: function(node) {
1514 assertRangeValid(this);
1515 assertValidNodeType(node, insertableNodeTypes);
1516 assertNodeNotReadOnly(this.startContainer);
1518 if (isOrIsAncestorOf(node, this.startContainer)) {
1519 throw new DOMException("HIERARCHY_REQUEST_ERR");
1522 // No check for whether the container of the start of the Range is of a type that does not allow
1523 // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1526 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1527 this.setStartBefore(firstNodeInserted);
1530 cloneContents: function() {
1531 assertRangeValid(this);
1534 if (this.collapsed) {
1535 return getRangeDocument(this).createDocumentFragment();
1537 if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
1538 clone = this.startContainer.cloneNode(true);
1539 clone.data = clone.data.slice(this.startOffset, this.endOffset);
1540 frag = getRangeDocument(this).createDocumentFragment();
1541 frag.appendChild(clone);
1544 var iterator = new RangeIterator(this, true);
1545 clone = cloneSubtree(iterator);
1552 canSurroundContents: function() {
1553 assertRangeValid(this);
1554 assertNodeNotReadOnly(this.startContainer);
1555 assertNodeNotReadOnly(this.endContainer);
1557 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
1558 // no non-text nodes.
1559 var iterator = new RangeIterator(this, true);
1560 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
1561 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
1563 return !boundariesInvalid;
1566 surroundContents: function(node) {
1567 assertValidNodeType(node, surroundNodeTypes);
1569 if (!this.canSurroundContents()) {
1570 throw new DOMException("INVALID_STATE_ERR");
1573 // Extract the contents
1574 var content = this.extractContents();
1576 // Clear the children of the node
1577 if (node.hasChildNodes()) {
1578 while (node.lastChild) {
1579 node.removeChild(node.lastChild);
1583 // Insert the new node and add the extracted contents
1584 insertNodeAtPosition(node, this.startContainer, this.startOffset);
1585 node.appendChild(content);
1587 this.selectNode(node);
1590 cloneRange: function() {
1591 assertRangeValid(this);
1592 var range = new Range(getRangeDocument(this));
1593 var i = rangeProperties.length, prop;
1595 prop = rangeProperties[i];
1596 range[prop] = this[prop];
1601 toString: function() {
1602 assertRangeValid(this);
1603 var sc = this.startContainer;
1604 if (sc === this.endContainer && isCharacterDataNode(sc)) {
1605 return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
1607 var textParts = [], iterator = new RangeIterator(this, true);
1608 iterateSubtree(iterator, function(node) {
1609 // Accept only text or CDATA nodes, not comments
1610 if (node.nodeType == 3 || node.nodeType == 4) {
1611 textParts.push(node.data);
1615 return textParts.join("");
1619 // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1620 // been removed from Mozilla.
1622 compareNode: function(node) {
1623 assertRangeValid(this);
1625 var parent = node.parentNode;
1626 var nodeIndex = getNodeIndex(node);
1629 throw new DOMException("NOT_FOUND_ERR");
1632 var startComparison = this.comparePoint(parent, nodeIndex),
1633 endComparison = this.comparePoint(parent, nodeIndex + 1);
1635 if (startComparison < 0) { // Node starts before
1636 return (endComparison > 0) ? n_b_a : n_b;
1638 return (endComparison > 0) ? n_a : n_i;
1642 comparePoint: function(node, offset) {
1643 assertRangeValid(this);
1644 assertNode(node, "HIERARCHY_REQUEST_ERR");
1645 assertSameDocumentOrFragment(node, this.startContainer);
1647 if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1649 } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1655 createContextualFragment: createContextualFragment,
1657 toHtml: function() {
1658 return rangeToHtml(this);
1661 // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
1662 // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
1663 intersectsNode: function(node, touchingIsIntersecting) {
1664 assertRangeValid(this);
1665 assertNode(node, "NOT_FOUND_ERR");
1666 if (getDocument(node) !== getRangeDocument(this)) {
1670 var parent = node.parentNode, offset = getNodeIndex(node);
1671 assertNode(parent, "NOT_FOUND_ERR");
1673 var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
1674 endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1676 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1679 isPointInRange: function(node, offset) {
1680 assertRangeValid(this);
1681 assertNode(node, "HIERARCHY_REQUEST_ERR");
1682 assertSameDocumentOrFragment(node, this.startContainer);
1684 return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1685 (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1688 // The methods below are non-standard and invented by me.
1690 // Sharing a boundary start-to-end or end-to-start does not count as intersection.
1691 intersectsRange: function(range) {
1692 return rangesIntersect(this, range, false);
1695 // Sharing a boundary start-to-end or end-to-start does count as intersection.
1696 intersectsOrTouchesRange: function(range) {
1697 return rangesIntersect(this, range, true);
1700 intersection: function(range) {
1701 if (this.intersectsRange(range)) {
1702 var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
1703 endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
1705 var intersectionRange = this.cloneRange();
1706 if (startComparison == -1) {
1707 intersectionRange.setStart(range.startContainer, range.startOffset);
1709 if (endComparison == 1) {
1710 intersectionRange.setEnd(range.endContainer, range.endOffset);
1712 return intersectionRange;
1717 union: function(range) {
1718 if (this.intersectsOrTouchesRange(range)) {
1719 var unionRange = this.cloneRange();
1720 if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
1721 unionRange.setStart(range.startContainer, range.startOffset);
1723 if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1724 unionRange.setEnd(range.endContainer, range.endOffset);
1728 throw new DOMException("Ranges do not intersect");
1732 containsNode: function(node, allowPartial) {
1734 return this.intersectsNode(node, false);
1736 return this.compareNode(node) == n_i;
1740 containsNodeContents: function(node) {
1741 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
1744 containsRange: function(range) {
1745 var intersection = this.intersection(range);
1746 return intersection !== null && range.equals(intersection);
1749 containsNodeText: function(node) {
1750 var nodeRange = this.cloneRange();
1751 nodeRange.selectNode(node);
1752 var textNodes = nodeRange.getNodes([3]);
1753 if (textNodes.length > 0) {
1754 nodeRange.setStart(textNodes[0], 0);
1755 var lastTextNode = textNodes.pop();
1756 nodeRange.setEnd(lastTextNode, lastTextNode.length);
1757 return this.containsRange(nodeRange);
1759 return this.containsNodeContents(node);
1763 getNodes: function(nodeTypes, filter) {
1764 assertRangeValid(this);
1765 return getNodesInRange(this, nodeTypes, filter);
1768 getDocument: function() {
1769 return getRangeDocument(this);
1772 collapseBefore: function(node) {
1773 this.setEndBefore(node);
1774 this.collapse(false);
1777 collapseAfter: function(node) {
1778 this.setStartAfter(node);
1779 this.collapse(true);
1782 getBookmark: function(containerNode) {
1783 var doc = getRangeDocument(this);
1784 var preSelectionRange = api.createRange(doc);
1785 containerNode = containerNode || dom.getBody(doc);
1786 preSelectionRange.selectNodeContents(containerNode);
1787 var range = this.intersection(preSelectionRange);
1788 var start = 0, end = 0;
1790 preSelectionRange.setEnd(range.startContainer, range.startOffset);
1791 start = preSelectionRange.toString().length;
1792 end = start + range.toString().length;
1798 containerNode: containerNode
1802 moveToBookmark: function(bookmark) {
1803 var containerNode = bookmark.containerNode;
1805 this.setStart(containerNode, 0);
1806 this.collapse(true);
1807 var nodeStack = [containerNode], node, foundStart = false, stop = false;
1808 var nextCharIndex, i, childNodes;
1810 while (!stop && (node = nodeStack.pop())) {
1811 if (node.nodeType == 3) {
1812 nextCharIndex = charIndex + node.length;
1813 if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
1814 this.setStart(node, bookmark.start - charIndex);
1817 if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
1818 this.setEnd(node, bookmark.end - charIndex);
1821 charIndex = nextCharIndex;
1823 childNodes = node.childNodes;
1824 i = childNodes.length;
1826 nodeStack.push(childNodes[i]);
1832 getName: function() {
1836 equals: function(range) {
1837 return Range.rangesEqual(this, range);
1840 isValid: function() {
1841 return isRangeValid(this);
1844 inspect: function() {
1845 return inspect(this);
1848 detach: function() {
1849 // In DOM4, detach() is now a no-op.
1853 function copyComparisonConstantsToObject(obj) {
1854 obj.START_TO_START = s2s;
1855 obj.START_TO_END = s2e;
1856 obj.END_TO_END = e2e;
1857 obj.END_TO_START = e2s;
1859 obj.NODE_BEFORE = n_b;
1860 obj.NODE_AFTER = n_a;
1861 obj.NODE_BEFORE_AND_AFTER = n_b_a;
1862 obj.NODE_INSIDE = n_i;
1865 function copyComparisonConstants(constructor) {
1866 copyComparisonConstantsToObject(constructor);
1867 copyComparisonConstantsToObject(constructor.prototype);
1870 function createRangeContentRemover(remover, boundaryUpdater) {
1872 assertRangeValid(this);
1874 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1876 var iterator = new RangeIterator(this, true);
1878 // Work out where to position the range after content removal
1881 node = getClosestAncestorIn(sc, root, true);
1882 boundary = getBoundaryAfterNode(node);
1884 so = boundary.offset;
1887 // Check none of the range is read-only
1888 iterateSubtree(iterator, assertNodeNotReadOnly);
1892 // Remove the content
1893 var returnValue = remover(iterator);
1896 // Move to the new position
1897 boundaryUpdater(this, sc, so, sc, so);
1903 function createPrototypeRange(constructor, boundaryUpdater) {
1904 function createBeforeAfterNodeSetter(isBefore, isStart) {
1905 return function(node) {
1906 assertValidNodeType(node, beforeAfterNodeTypes);
1907 assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1909 var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1910 (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1914 function setRangeStart(range, node, offset) {
1915 var ec = range.endContainer, eo = range.endOffset;
1916 if (node !== range.startContainer || offset !== range.startOffset) {
1917 // Check the root containers of the range and the new boundary, and also check whether the new boundary
1918 // is after the current end. In either case, collapse the range to the new position
1919 if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
1923 boundaryUpdater(range, node, offset, ec, eo);
1927 function setRangeEnd(range, node, offset) {
1928 var sc = range.startContainer, so = range.startOffset;
1929 if (node !== range.endContainer || offset !== range.endOffset) {
1930 // Check the root containers of the range and the new boundary, and also check whether the new boundary
1931 // is after the current end. In either case, collapse the range to the new position
1932 if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
1936 boundaryUpdater(range, sc, so, node, offset);
1940 // Set up inheritance
1941 var F = function() {};
1942 F.prototype = api.rangePrototype;
1943 constructor.prototype = new F();
1945 util.extend(constructor.prototype, {
1946 setStart: function(node, offset) {
1947 assertNoDocTypeNotationEntityAncestor(node, true);
1948 assertValidOffset(node, offset);
1950 setRangeStart(this, node, offset);
1953 setEnd: function(node, offset) {
1954 assertNoDocTypeNotationEntityAncestor(node, true);
1955 assertValidOffset(node, offset);
1957 setRangeEnd(this, node, offset);
1961 * Convenience method to set a range's start and end boundaries. Overloaded as follows:
1962 * - Two parameters (node, offset) creates a collapsed range at that position
1963 * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
1964 * startOffset and ending at endOffset
1965 * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
1966 * startNode and ending at endOffset in endNode
1968 setStartAndEnd: function() {
1969 var args = arguments;
1970 var sc = args[0], so = args[1], ec = sc, eo = so;
1972 switch (args.length) {
1982 boundaryUpdater(this, sc, so, ec, eo);
1985 setBoundary: function(node, offset, isStart) {
1986 this["set" + (isStart ? "Start" : "End")](node, offset);
1989 setStartBefore: createBeforeAfterNodeSetter(true, true),
1990 setStartAfter: createBeforeAfterNodeSetter(false, true),
1991 setEndBefore: createBeforeAfterNodeSetter(true, false),
1992 setEndAfter: createBeforeAfterNodeSetter(false, false),
1994 collapse: function(isStart) {
1995 assertRangeValid(this);
1997 boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1999 boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
2003 selectNodeContents: function(node) {
2004 assertNoDocTypeNotationEntityAncestor(node, true);
2006 boundaryUpdater(this, node, 0, node, getNodeLength(node));
2009 selectNode: function(node) {
2010 assertNoDocTypeNotationEntityAncestor(node, false);
2011 assertValidNodeType(node, beforeAfterNodeTypes);
2013 var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
2014 boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
2017 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
2019 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
2021 canSurroundContents: function() {
2022 assertRangeValid(this);
2023 assertNodeNotReadOnly(this.startContainer);
2024 assertNodeNotReadOnly(this.endContainer);
2026 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
2027 // no non-text nodes.
2028 var iterator = new RangeIterator(this, true);
2029 var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
2030 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
2032 return !boundariesInvalid;
2035 splitBoundaries: function() {
2036 splitRangeBoundaries(this);
2039 splitBoundariesPreservingPositions: function(positionsToPreserve) {
2040 splitRangeBoundaries(this, positionsToPreserve);
2043 normalizeBoundaries: function() {
2044 assertRangeValid(this);
2046 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
2048 var mergeForward = function(node) {
2049 var sibling = node.nextSibling;
2050 if (sibling && sibling.nodeType == node.nodeType) {
2053 node.appendData(sibling.data);
2054 sibling.parentNode.removeChild(sibling);
2058 var mergeBackward = function(node) {
2059 var sibling = node.previousSibling;
2060 if (sibling && sibling.nodeType == node.nodeType) {
2062 var nodeLength = node.length;
2063 so = sibling.length;
2064 node.insertData(0, sibling.data);
2065 sibling.parentNode.removeChild(sibling);
2069 } else if (ec == node.parentNode) {
2070 var nodeIndex = getNodeIndex(node);
2071 if (eo == nodeIndex) {
2074 } else if (eo > nodeIndex) {
2081 var normalizeStart = true;
2083 if (isCharacterDataNode(ec)) {
2084 if (ec.length == eo) {
2089 var endNode = ec.childNodes[eo - 1];
2090 if (endNode && isCharacterDataNode(endNode)) {
2091 mergeForward(endNode);
2094 normalizeStart = !this.collapsed;
2097 if (normalizeStart) {
2098 if (isCharacterDataNode(sc)) {
2103 if (so < sc.childNodes.length) {
2104 var startNode = sc.childNodes[so];
2105 if (startNode && isCharacterDataNode(startNode)) {
2106 mergeBackward(startNode);
2115 boundaryUpdater(this, sc, so, ec, eo);
2118 collapseToPoint: function(node, offset) {
2119 assertNoDocTypeNotationEntityAncestor(node, true);
2120 assertValidOffset(node, offset);
2121 this.setStartAndEnd(node, offset);
2125 copyComparisonConstants(constructor);
2128 /*----------------------------------------------------------------------------------------------------------------*/
2130 // Updates commonAncestorContainer and collapsed after boundary change
2131 function updateCollapsedAndCommonAncestor(range) {
2132 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2133 range.commonAncestorContainer = range.collapsed ?
2134 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
2137 function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
2138 range.startContainer = startContainer;
2139 range.startOffset = startOffset;
2140 range.endContainer = endContainer;
2141 range.endOffset = endOffset;
2142 range.document = dom.getDocument(startContainer);
2144 updateCollapsedAndCommonAncestor(range);
2147 function Range(doc) {
2148 this.startContainer = doc;
2149 this.startOffset = 0;
2150 this.endContainer = doc;
2152 this.document = doc;
2153 updateCollapsedAndCommonAncestor(this);
2156 createPrototypeRange(Range, updateBoundaries);
2158 util.extend(Range, {
2159 rangeProperties: rangeProperties,
2160 RangeIterator: RangeIterator,
2161 copyComparisonConstants: copyComparisonConstants,
2162 createPrototypeRange: createPrototypeRange,
2164 toHtml: rangeToHtml,
2165 getRangeDocument: getRangeDocument,
2166 rangesEqual: function(r1, r2) {
2167 return r1.startContainer === r2.startContainer &&
2168 r1.startOffset === r2.startOffset &&
2169 r1.endContainer === r2.endContainer &&
2170 r1.endOffset === r2.endOffset;
2174 api.DomRange = Range;
2177 /*----------------------------------------------------------------------------------------------------------------*/
2179 // Wrappers for the browser's native DOM Range and/or TextRange implementation
2180 api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
2181 var WrappedRange, WrappedTextRange;
2183 var util = api.util;
2184 var DomPosition = dom.DomPosition;
2185 var DomRange = api.DomRange;
2186 var getBody = dom.getBody;
2187 var getContentDocument = dom.getContentDocument;
2188 var isCharacterDataNode = dom.isCharacterDataNode;
2191 /*----------------------------------------------------------------------------------------------------------------*/
2193 if (api.features.implementsDomRange) {
2194 // This is a wrapper around the browser's native DOM Range. It has two aims:
2195 // - Provide workarounds for specific browser bugs
2196 // - provide convenient extensions, which are inherited from Rangy's DomRange
2200 var rangeProperties = DomRange.rangeProperties;
2202 function updateRangeProperties(range) {
2203 var i = rangeProperties.length, prop;
2205 prop = rangeProperties[i];
2206 range[prop] = range.nativeRange[prop];
2208 // Fix for broken collapsed property in IE 9.
2209 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2212 function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
2213 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2214 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2215 var nativeRangeDifferent = !range.equals(range.nativeRange);
2217 // Always set both boundaries for the benefit of IE9 (see issue 35)
2218 if (startMoved || endMoved || nativeRangeDifferent) {
2219 range.setEnd(endContainer, endOffset);
2220 range.setStart(startContainer, startOffset);
2224 var createBeforeAfterNodeSetter;
2226 WrappedRange = function(range) {
2228 throw module.createError("WrappedRange: Range must be specified");
2230 this.nativeRange = range;
2231 updateRangeProperties(this);
2234 DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
2236 rangeProto = WrappedRange.prototype;
2238 rangeProto.selectNode = function(node) {
2239 this.nativeRange.selectNode(node);
2240 updateRangeProperties(this);
2243 rangeProto.cloneContents = function() {
2244 return this.nativeRange.cloneContents();
2247 // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
2248 // insertNode() is never delegated to the native range.
2250 rangeProto.surroundContents = function(node) {
2251 this.nativeRange.surroundContents(node);
2252 updateRangeProperties(this);
2255 rangeProto.collapse = function(isStart) {
2256 this.nativeRange.collapse(isStart);
2257 updateRangeProperties(this);
2260 rangeProto.cloneRange = function() {
2261 return new WrappedRange(this.nativeRange.cloneRange());
2264 rangeProto.refresh = function() {
2265 updateRangeProperties(this);
2268 rangeProto.toString = function() {
2269 return this.nativeRange.toString();
2272 // Create test range and node for feature detection
2274 var testTextNode = document.createTextNode("test");
2275 getBody(document).appendChild(testTextNode);
2276 var range = document.createRange();
2278 /*--------------------------------------------------------------------------------------------------------*/
2280 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2283 range.setStart(testTextNode, 0);
2284 range.setEnd(testTextNode, 0);
2287 range.setStart(testTextNode, 1);
2289 rangeProto.setStart = function(node, offset) {
2290 this.nativeRange.setStart(node, offset);
2291 updateRangeProperties(this);
2294 rangeProto.setEnd = function(node, offset) {
2295 this.nativeRange.setEnd(node, offset);
2296 updateRangeProperties(this);
2299 createBeforeAfterNodeSetter = function(name) {
2300 return function(node) {
2301 this.nativeRange[name](node);
2302 updateRangeProperties(this);
2308 rangeProto.setStart = function(node, offset) {
2310 this.nativeRange.setStart(node, offset);
2312 this.nativeRange.setEnd(node, offset);
2313 this.nativeRange.setStart(node, offset);
2315 updateRangeProperties(this);
2318 rangeProto.setEnd = function(node, offset) {
2320 this.nativeRange.setEnd(node, offset);
2322 this.nativeRange.setStart(node, offset);
2323 this.nativeRange.setEnd(node, offset);
2325 updateRangeProperties(this);
2328 createBeforeAfterNodeSetter = function(name, oppositeName) {
2329 return function(node) {
2331 this.nativeRange[name](node);
2333 this.nativeRange[oppositeName](node);
2334 this.nativeRange[name](node);
2336 updateRangeProperties(this);
2341 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2342 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2343 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2344 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2346 /*--------------------------------------------------------------------------------------------------------*/
2348 // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
2349 // whether the native implementation can be trusted
2350 rangeProto.selectNodeContents = function(node) {
2351 this.setStartAndEnd(node, 0, dom.getNodeLength(node));
2354 /*--------------------------------------------------------------------------------------------------------*/
2356 // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
2357 // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2359 range.selectNodeContents(testTextNode);
2360 range.setEnd(testTextNode, 3);
2362 var range2 = document.createRange();
2363 range2.selectNodeContents(testTextNode);
2364 range2.setEnd(testTextNode, 4);
2365 range2.setStart(testTextNode, 2);
2367 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
2368 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2369 // This is the wrong way round, so correct for it
2371 rangeProto.compareBoundaryPoints = function(type, range) {
2372 range = range.nativeRange || range;
2373 if (type == range.START_TO_END) {
2374 type = range.END_TO_START;
2375 } else if (type == range.END_TO_START) {
2376 type = range.START_TO_END;
2378 return this.nativeRange.compareBoundaryPoints(type, range);
2381 rangeProto.compareBoundaryPoints = function(type, range) {
2382 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2386 /*--------------------------------------------------------------------------------------------------------*/
2388 // Test for IE 9 deleteContents() and extractContents() bug and correct it. See issue 107.
2390 var el = document.createElement("div");
2391 el.innerHTML = "123";
2392 var textNode = el.firstChild;
2393 var body = getBody(document);
2394 body.appendChild(el);
2396 range.setStart(textNode, 1);
2397 range.setEnd(textNode, 2);
2398 range.deleteContents();
2400 if (textNode.data == "13") {
2401 // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
2402 // extractContents()
2403 rangeProto.deleteContents = function() {
2404 this.nativeRange.deleteContents();
2405 updateRangeProperties(this);
2408 rangeProto.extractContents = function() {
2409 var frag = this.nativeRange.extractContents();
2410 updateRangeProperties(this);
2416 body.removeChild(el);
2419 /*--------------------------------------------------------------------------------------------------------*/
2421 // Test for existence of createContextualFragment and delegate to it if it exists
2422 if (util.isHostMethod(range, "createContextualFragment")) {
2423 rangeProto.createContextualFragment = function(fragmentStr) {
2424 return this.nativeRange.createContextualFragment(fragmentStr);
2428 /*--------------------------------------------------------------------------------------------------------*/
2431 getBody(document).removeChild(testTextNode);
2433 rangeProto.getName = function() {
2434 return "WrappedRange";
2437 api.WrappedRange = WrappedRange;
2439 api.createNativeRange = function(doc) {
2440 doc = getContentDocument(doc, module, "createNativeRange");
2441 return doc.createRange();
2446 if (api.features.implementsTextRange) {
2448 This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
2449 method. For example, in the following (where pipes denote the selection boundaries):
2451 <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
2453 var range = document.selection.createRange();
2454 alert(range.parentElement().id); // Should alert "ul" but alerts "b"
2456 This method returns the common ancestor node of the following:
2457 - the parentElement() of the textRange
2458 - the parentElement() of the textRange after calling collapse(true)
2459 - the parentElement() of the textRange after calling collapse(false)
2461 var getTextRangeContainerElement = function(textRange) {
2462 var parentEl = textRange.parentElement();
2463 var range = textRange.duplicate();
2464 range.collapse(true);
2465 var startEl = range.parentElement();
2466 range = textRange.duplicate();
2467 range.collapse(false);
2468 var endEl = range.parentElement();
2469 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
2471 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
2474 var textRangeIsCollapsed = function(textRange) {
2475 return textRange.compareEndPoints("StartToEnd", textRange) == 0;
2478 // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
2479 // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
2480 // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
2481 // bugs, handling for inputs and images, plus optimizations.
2482 var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
2483 var workingRange = textRange.duplicate();
2484 workingRange.collapse(isStart);
2485 var containerElement = workingRange.parentElement();
2487 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
2489 if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
2490 containerElement = wholeRangeContainerElement;
2494 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
2495 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
2496 if (!containerElement.canHaveHTML) {
2497 var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
2499 boundaryPosition: pos,
2501 nodeIndex: pos.offset,
2502 containerElement: pos.node
2507 var workingNode = dom.getDocument(containerElement).createElement("span");
2509 // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
2510 // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
2511 if (workingNode.parentNode) {
2512 workingNode.parentNode.removeChild(workingNode);
2515 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
2516 var previousNode, nextNode, boundaryPosition, boundaryNode;
2517 var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
2518 var childNodeCount = containerElement.childNodes.length;
2519 var end = childNodeCount;
2521 // Check end first. Code within the loop assumes that the endth child node of the container is definitely
2522 // after the range boundary.
2523 var nodeIndex = end;
2526 if (nodeIndex == childNodeCount) {
2527 containerElement.appendChild(workingNode);
2529 containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
2531 workingRange.moveToElementText(workingNode);
2532 comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
2533 if (comparison == 0 || start == end) {
2535 } else if (comparison == -1) {
2536 if (end == start + 1) {
2537 // We know the endth child node is after the range boundary, so we must be done.
2543 end = (end == start + 1) ? start : nodeIndex;
2545 nodeIndex = Math.floor((start + end) / 2);
2546 containerElement.removeChild(workingNode);
2550 // We've now reached or gone past the boundary of the text range we're interested in
2551 // so have identified the node we want
2552 boundaryNode = workingNode.nextSibling;
2554 if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
2555 // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
2556 // the node containing the text range's boundary, so we move the end of the working range to the
2557 // boundary point and measure the length of its text to get the boundary's offset within the node.
2558 workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
2562 if (/[\r\n]/.test(boundaryNode.data)) {
2564 For the particular case of a boundary within a text node containing rendered line breaks (within a
2565 <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
2568 - Each line break is represented as \r in the text node's data/nodeValue properties
2569 - Each line break is represented as \r\n in the TextRange's 'text' property
2570 - The 'text' property of the TextRange does not contain trailing line breaks
2572 To get round the problem presented by the final fact above, we can use the fact that TextRange's
2573 moveStart() and moveEnd() methods return the actual number of characters moved, which is not
2574 necessarily the same as the number of characters it was instructed to move. The simplest approach is
2575 to use this to store the characters moved when moving both the start and end of the range to the
2576 start of the document body and subtracting the start offset from the end offset (the
2577 "move-negative-gazillion" method). However, this is extremely slow when the document is large and
2578 the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
2579 the end of the document) has the same problem.
2581 Another approach that works is to use moveStart() to move the start boundary of the range up to the
2582 end boundary one character at a time and incrementing a counter with the value returned by the
2583 moveStart() call. However, the check for whether the start boundary has reached the end boundary is
2584 expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
2585 by the location of the range within the document).
2587 The approach used below is a hybrid of the two methods above. It uses the fact that a string
2588 containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
2589 be longer than the text of the TextRange, so the start of the range is moved that length initially
2590 and then a character at a time to make up for any trailing line breaks not contained in the 'text'
2591 property. This has good performance in most situations compared to the previous two methods.
2593 var tempRange = workingRange.duplicate();
2594 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2596 offset = tempRange.moveStart("character", rangeLength);
2597 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2599 tempRange.moveStart("character", 1);
2602 offset = workingRange.text.length;
2604 boundaryPosition = new DomPosition(boundaryNode, offset);
2607 // If the boundary immediately follows a character data node and this is the end boundary, we should favour
2608 // a position within that, and likewise for a start boundary preceding a character data node
2609 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
2610 nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
2611 if (nextNode && isCharacterDataNode(nextNode)) {
2612 boundaryPosition = new DomPosition(nextNode, 0);
2613 } else if (previousNode && isCharacterDataNode(previousNode)) {
2614 boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
2616 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2621 workingNode.parentNode.removeChild(workingNode);
2624 boundaryPosition: boundaryPosition,
2626 nodeIndex: nodeIndex,
2627 containerElement: containerElement
2632 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
2633 // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
2634 // (http://code.google.com/p/ierange/)
2635 var createBoundaryTextRange = function(boundaryPosition, isStart) {
2636 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
2637 var doc = dom.getDocument(boundaryPosition.node);
2638 var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
2639 var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
2641 if (nodeIsDataNode) {
2642 boundaryNode = boundaryPosition.node;
2643 boundaryParent = boundaryNode.parentNode;
2645 childNodes = boundaryPosition.node.childNodes;
2646 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
2647 boundaryParent = boundaryPosition.node;
2650 // Position the range immediately before the node containing the boundary
2651 workingNode = doc.createElement("span");
2653 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
2654 // the element rather than immediately before or after it
2655 workingNode.innerHTML = "&#feff;";
2657 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
2658 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
2660 boundaryParent.insertBefore(workingNode, boundaryNode);
2662 boundaryParent.appendChild(workingNode);
2665 workingRange.moveToElementText(workingNode);
2666 workingRange.collapse(!isStart);
2669 boundaryParent.removeChild(workingNode);
2671 // Move the working range to the text offset, if required
2672 if (nodeIsDataNode) {
2673 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
2676 return workingRange;
2679 /*------------------------------------------------------------------------------------------------------------*/
2681 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
2684 WrappedTextRange = function(textRange) {
2685 this.textRange = textRange;
2689 WrappedTextRange.prototype = new DomRange(document);
2691 WrappedTextRange.prototype.refresh = function() {
2692 var start, end, startBoundary;
2694 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
2695 var rangeContainerElement = getTextRangeContainerElement(this.textRange);
2697 if (textRangeIsCollapsed(this.textRange)) {
2698 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
2699 true).boundaryPosition;
2701 startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
2702 start = startBoundary.boundaryPosition;
2704 // An optimization used here is that if the start and end boundaries have the same parent element, the
2705 // search scope for the end boundary can be limited to exclude the portion of the element that precedes
2706 // the start boundary
2707 end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
2708 startBoundary.nodeInfo).boundaryPosition;
2711 this.setStart(start.node, start.offset);
2712 this.setEnd(end.node, end.offset);
2715 WrappedTextRange.prototype.getName = function() {
2716 return "WrappedTextRange";
2719 DomRange.copyComparisonConstants(WrappedTextRange);
2721 var rangeToTextRange = function(range) {
2722 if (range.collapsed) {
2723 return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2725 var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2726 var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
2727 var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
2728 textRange.setEndPoint("StartToStart", startRange);
2729 textRange.setEndPoint("EndToEnd", endRange);
2734 WrappedTextRange.rangeToTextRange = rangeToTextRange;
2736 WrappedTextRange.prototype.toTextRange = function() {
2737 return rangeToTextRange(this);
2740 api.WrappedTextRange = WrappedTextRange;
2742 // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
2743 // implementation to use by default.
2744 if (!api.features.implementsDomRange || api.config.preferTextRange) {
2745 // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
2746 var globalObj = (function() { return this; })();
2747 if (typeof globalObj.Range == "undefined") {
2748 globalObj.Range = WrappedTextRange;
2751 api.createNativeRange = function(doc) {
2752 doc = getContentDocument(doc, module, "createNativeRange");
2753 return getBody(doc).createTextRange();
2756 api.WrappedRange = WrappedTextRange;
2760 api.createRange = function(doc) {
2761 doc = getContentDocument(doc, module, "createRange");
2762 return new api.WrappedRange(api.createNativeRange(doc));
2765 api.createRangyRange = function(doc) {
2766 doc = getContentDocument(doc, module, "createRangyRange");
2767 return new DomRange(doc);
2770 api.createIframeRange = function(iframeEl) {
2771 module.deprecationNotice("createIframeRange()", "createRange(iframeEl)");
2772 return api.createRange(iframeEl);
2775 api.createIframeRangyRange = function(iframeEl) {
2776 module.deprecationNotice("createIframeRangyRange()", "createRangyRange(iframeEl)");
2777 return api.createRangyRange(iframeEl);
2780 api.addShimListener(function(win) {
2781 var doc = win.document;
2782 if (typeof doc.createRange == "undefined") {
2783 doc.createRange = function() {
2784 return api.createRange(doc);
2791 /*----------------------------------------------------------------------------------------------------------------*/
2793 // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
2794 // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
2795 api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
2796 api.config.checkSelectionRanges = true;
2798 var BOOLEAN = "boolean";
2799 var NUMBER = "number";
2801 var util = api.util;
2802 var isHostMethod = util.isHostMethod;
2803 var DomRange = api.DomRange;
2804 var WrappedRange = api.WrappedRange;
2805 var DOMException = api.DOMException;
2806 var DomPosition = dom.DomPosition;
2807 var getNativeSelection;
2808 var selectionIsCollapsed;
2809 var features = api.features;
2810 var CONTROL = "Control";
2811 var getDocument = dom.getDocument;
2812 var getBody = dom.getBody;
2813 var rangesEqual = DomRange.rangesEqual;
2816 // Utility function to support direction parameters in the API that may be a string ("backward" or "forward") or a
2817 // Boolean (true for backwards).
2818 function isDirectionBackward(dir) {
2819 return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
2822 function getWindow(win, methodName) {
2825 } else if (dom.isWindow(win)) {
2827 } else if (win instanceof WrappedSelection) {
2830 var doc = dom.getContentDocument(win, module, methodName);
2831 return dom.getWindow(doc);
2835 function getWinSelection(winParam) {
2836 return getWindow(winParam, "getWinSelection").getSelection();
2839 function getDocSelection(winParam) {
2840 return getWindow(winParam, "getDocSelection").document.selection;
2843 function winSelectionIsBackward(sel) {
2844 var backward = false;
2845 if (sel.anchorNode) {
2846 backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
2851 // Test for the Range/TextRange and Selection features required
2852 // Test for ability to retrieve selection
2853 var implementsWinGetSelection = isHostMethod(window, "getSelection"),
2854 implementsDocSelection = util.isHostObject(document, "selection");
2856 features.implementsWinGetSelection = implementsWinGetSelection;
2857 features.implementsDocSelection = implementsDocSelection;
2859 var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
2861 if (useDocumentSelection) {
2862 getNativeSelection = getDocSelection;
2863 api.isSelectionValid = function(winParam) {
2864 var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
2866 // Check whether the selection TextRange is actually contained within the correct document
2867 return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
2869 } else if (implementsWinGetSelection) {
2870 getNativeSelection = getWinSelection;
2871 api.isSelectionValid = function() {
2875 module.fail("Neither document.selection or window.getSelection() detected.");
2878 api.getNativeSelection = getNativeSelection;
2880 var testSelection = getNativeSelection();
2881 var testRange = api.createNativeRange(document);
2882 var body = getBody(document);
2884 // Obtaining a range from a selection
2885 var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
2886 ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
2888 features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
2890 // Test for existence of native selection extend() method
2891 var selectionHasExtend = isHostMethod(testSelection, "extend");
2892 features.selectionHasExtend = selectionHasExtend;
2894 // Test if rangeCount exists
2895 var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
2896 features.selectionHasRangeCount = selectionHasRangeCount;
2898 var selectionSupportsMultipleRanges = false;
2899 var collapsedNonEditableSelectionsSupported = true;
2901 var addRangeBackwardToNative = selectionHasExtend ?
2902 function(nativeSelection, range) {
2903 var doc = DomRange.getRangeDocument(range);
2904 var endRange = api.createRange(doc);
2905 endRange.collapseToPoint(range.endContainer, range.endOffset);
2906 nativeSelection.addRange(getNativeRange(endRange));
2907 nativeSelection.extend(range.startContainer, range.startOffset);
2910 if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
2911 typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
2914 // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
2915 // performed on the current document's selection. See issue 109.
2917 // Note also that if a selection previously existed, it is wiped by these tests. This should usually be fine
2918 // because initialization usually happens when the document loads, but could be a problem for a script that
2919 // loads and initializes Rangy later. If anyone complains, code could be added to save and restore the
2921 var sel = window.getSelection();
2923 // Store the current selection
2924 var originalSelectionRangeCount = sel.rangeCount;
2925 var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
2926 var originalSelectionRanges = [];
2927 var originalSelectionBackward = winSelectionIsBackward(sel);
2928 for (var i = 0; i < originalSelectionRangeCount; ++i) {
2929 originalSelectionRanges[i] = sel.getRangeAt(i);
2932 // Create some test elements
2933 var body = getBody(document);
2934 var testEl = body.appendChild( document.createElement("div") );
2935 testEl.contentEditable = "false";
2936 var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
2938 // Test whether the native selection will allow a collapsed selection within a non-editable element
2939 var r1 = document.createRange();
2941 r1.setStart(textNode, 1);
2944 collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
2945 sel.removeAllRanges();
2947 // Test whether the native selection is capable of supporting multiple ranges.
2948 if (!selectionHasMultipleRanges) {
2949 // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
2950 // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
2951 // nothing we can do about this while retaining the feature test so we have to resort to a browser
2952 // sniff. I'm not happy about it. See
2953 // https://code.google.com/p/chromium/issues/detail?id=399791
2954 var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
2955 if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
2956 selectionSupportsMultipleRanges = false;
2958 var r2 = r1.cloneRange();
2959 r1.setStart(textNode, 0);
2960 r2.setEnd(textNode, 3);
2961 r2.setStart(textNode, 2);
2964 selectionSupportsMultipleRanges = (sel.rangeCount == 2);
2969 body.removeChild(testEl);
2970 sel.removeAllRanges();
2972 for (i = 0; i < originalSelectionRangeCount; ++i) {
2973 if (i == 0 && originalSelectionBackward) {
2974 if (addRangeBackwardToNative) {
2975 addRangeBackwardToNative(sel, originalSelectionRanges[i]);
2977 api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
2978 sel.addRange(originalSelectionRanges[i]);
2981 sel.addRange(originalSelectionRanges[i]);
2988 features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
2989 features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
2992 var implementsControlRange = false, testControlRange;
2994 if (body && isHostMethod(body, "createControlRange")) {
2995 testControlRange = body.createControlRange();
2996 if (util.areHostProperties(testControlRange, ["item", "add"])) {
2997 implementsControlRange = true;
3000 features.implementsControlRange = implementsControlRange;
3002 // Selection collapsedness
3003 if (selectionHasAnchorAndFocus) {
3004 selectionIsCollapsed = function(sel) {
3005 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
3008 selectionIsCollapsed = function(sel) {
3009 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
3013 function updateAnchorAndFocusFromRange(sel, range, backward) {
3014 var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
3015 sel.anchorNode = range[anchorPrefix + "Container"];
3016 sel.anchorOffset = range[anchorPrefix + "Offset"];
3017 sel.focusNode = range[focusPrefix + "Container"];
3018 sel.focusOffset = range[focusPrefix + "Offset"];
3021 function updateAnchorAndFocusFromNativeSelection(sel) {
3022 var nativeSel = sel.nativeSelection;
3023 sel.anchorNode = nativeSel.anchorNode;
3024 sel.anchorOffset = nativeSel.anchorOffset;
3025 sel.focusNode = nativeSel.focusNode;
3026 sel.focusOffset = nativeSel.focusOffset;
3029 function updateEmptySelection(sel) {
3030 sel.anchorNode = sel.focusNode = null;
3031 sel.anchorOffset = sel.focusOffset = 0;
3033 sel.isCollapsed = true;
3034 sel._ranges.length = 0;
3037 function getNativeRange(range) {
3039 if (range instanceof DomRange) {
3040 nativeRange = api.createNativeRange(range.getDocument());
3041 nativeRange.setEnd(range.endContainer, range.endOffset);
3042 nativeRange.setStart(range.startContainer, range.startOffset);
3043 } else if (range instanceof WrappedRange) {
3044 nativeRange = range.nativeRange;
3045 } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
3046 nativeRange = range;
3051 function rangeContainsSingleElement(rangeNodes) {
3052 if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
3055 for (var i = 1, len = rangeNodes.length; i < len; ++i) {
3056 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
3063 function getSingleElementFromRange(range) {
3064 var nodes = range.getNodes();
3065 if (!rangeContainsSingleElement(nodes)) {
3066 throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
3071 // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
3072 function isTextRange(range) {
3073 return !!range && typeof range.text != "undefined";
3076 function updateFromTextRange(sel, range) {
3077 // Create a Range from the selected TextRange
3078 var wrappedRange = new WrappedRange(range);
3079 sel._ranges = [wrappedRange];
3081 updateAnchorAndFocusFromRange(sel, wrappedRange, false);
3083 sel.isCollapsed = wrappedRange.collapsed;
3086 function updateControlSelection(sel) {
3087 // Update the wrapped selection based on what's now in the native selection
3088 sel._ranges.length = 0;
3089 if (sel.docSelection.type == "None") {
3090 updateEmptySelection(sel);
3092 var controlRange = sel.docSelection.createRange();
3093 if (isTextRange(controlRange)) {
3094 // This case (where the selection type is "Control" and calling createRange() on the selection returns
3095 // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
3096 // ControlRange have been removed from the ControlRange and removed from the document.
3097 updateFromTextRange(sel, controlRange);
3099 sel.rangeCount = controlRange.length;
3100 var range, doc = getDocument(controlRange.item(0));
3101 for (var i = 0; i < sel.rangeCount; ++i) {
3102 range = api.createRange(doc);
3103 range.selectNode(controlRange.item(i));
3104 sel._ranges.push(range);
3106 sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
3107 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
3112 function addRangeToControlSelection(sel, range) {
3113 var controlRange = sel.docSelection.createRange();
3114 var rangeElement = getSingleElementFromRange(range);
3116 // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
3117 // contained by the supplied range
3118 var doc = getDocument(controlRange.item(0));
3119 var newControlRange = getBody(doc).createControlRange();
3120 for (var i = 0, len = controlRange.length; i < len; ++i) {
3121 newControlRange.add(controlRange.item(i));
3124 newControlRange.add(rangeElement);
3126 throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
3128 newControlRange.select();
3130 // Update the wrapped selection based on what's now in the native selection
3131 updateControlSelection(sel);
3134 var getSelectionRangeAt;
3136 if (isHostMethod(testSelection, "getRangeAt")) {
3137 // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
3138 // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
3139 // lesson to us all, especially me.
3140 getSelectionRangeAt = function(sel, index) {
3142 return sel.getRangeAt(index);
3147 } else if (selectionHasAnchorAndFocus) {
3148 getSelectionRangeAt = function(sel) {
3149 var doc = getDocument(sel.anchorNode);
3150 var range = api.createRange(doc);
3151 range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
3153 // Handle the case when the selection was selected backwards (from the end to the start in the
3155 if (range.collapsed !== this.isCollapsed) {
3156 range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
3163 function WrappedSelection(selection, docSelection, win) {
3164 this.nativeSelection = selection;
3165 this.docSelection = docSelection;
3171 WrappedSelection.prototype = api.selectionPrototype;
3173 function deleteProperties(sel) {
3174 sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
3175 sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
3176 sel.detached = true;
3179 var cachedRangySelections = [];
3181 function actOnCachedSelection(win, action) {
3182 var i = cachedRangySelections.length, cached, sel;
3184 cached = cachedRangySelections[i];
3185 sel = cached.selection;
3186 if (action == "deleteAll") {
3187 deleteProperties(sel);
3188 } else if (cached.win == win) {
3189 if (action == "delete") {
3190 cachedRangySelections.splice(i, 1);
3197 if (action == "deleteAll") {
3198 cachedRangySelections.length = 0;
3203 var getSelection = function(win) {
3204 // Check if the parameter is a Rangy Selection object
3205 if (win && win instanceof WrappedSelection) {
3210 win = getWindow(win, "getNativeSelection");
3212 var sel = actOnCachedSelection(win);
3213 var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
3215 sel.nativeSelection = nativeSel;
3216 sel.docSelection = docSel;
3219 sel = new WrappedSelection(nativeSel, docSel, win);
3220 cachedRangySelections.push( { win: win, selection: sel } );
3225 api.getSelection = getSelection;
3227 api.getIframeSelection = function(iframeEl) {
3228 module.deprecationNotice("getIframeSelection()", "getSelection(iframeEl)");
3229 return api.getSelection(dom.getIframeWindow(iframeEl));
3232 var selProto = WrappedSelection.prototype;
3234 function createControlSelection(sel, ranges) {
3235 // Ensure that the selection becomes of type "Control"
3236 var doc = getDocument(ranges[0].startContainer);
3237 var controlRange = getBody(doc).createControlRange();
3238 for (var i = 0, el, len = ranges.length; i < len; ++i) {
3239 el = getSingleElementFromRange(ranges[i]);
3241 controlRange.add(el);
3243 throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
3246 controlRange.select();
3248 // Update the wrapped selection based on what's now in the native selection
3249 updateControlSelection(sel);
3252 // Selecting a range
3253 if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
3254 selProto.removeAllRanges = function() {
3255 this.nativeSelection.removeAllRanges();
3256 updateEmptySelection(this);
3259 var addRangeBackward = function(sel, range) {
3260 addRangeBackwardToNative(sel.nativeSelection, range);
3264 if (selectionHasRangeCount) {
3265 selProto.addRange = function(range, direction) {
3266 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3267 addRangeToControlSelection(this, range);
3269 if (isDirectionBackward(direction) && selectionHasExtend) {
3270 addRangeBackward(this, range);
3272 var previousRangeCount;
3273 if (selectionSupportsMultipleRanges) {
3274 previousRangeCount = this.rangeCount;
3276 this.removeAllRanges();
3277 previousRangeCount = 0;
3279 // Clone the native range so that changing the selected range does not affect the selection.
3280 // This is contrary to the spec but is the only way to achieve consistency between browsers. See
3282 this.nativeSelection.addRange(getNativeRange(range).cloneRange());
3284 // Check whether adding the range was successful
3285 this.rangeCount = this.nativeSelection.rangeCount;
3287 if (this.rangeCount == previousRangeCount + 1) {
3288 // The range was added successfully
3290 // Check whether the range that we added to the selection is reflected in the last range extracted from
3292 if (api.config.checkSelectionRanges) {
3293 var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
3294 if (nativeRange && !rangesEqual(nativeRange, range)) {
3295 // Happens in WebKit with, for example, a selection placed at the start of a text node
3296 range = new WrappedRange(nativeRange);
3299 this._ranges[this.rangeCount - 1] = range;
3300 updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
3301 this.isCollapsed = selectionIsCollapsed(this);
3303 // The range was not added successfully. The simplest thing is to refresh
3310 selProto.addRange = function(range, direction) {
3311 if (isDirectionBackward(direction) && selectionHasExtend) {
3312 addRangeBackward(this, range);
3314 this.nativeSelection.addRange(getNativeRange(range));
3320 selProto.setRanges = function(ranges) {
3321 if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
3322 createControlSelection(this, ranges);
3324 this.removeAllRanges();
3325 for (var i = 0, len = ranges.length; i < len; ++i) {
3326 this.addRange(ranges[i]);
3330 } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
3331 implementsControlRange && useDocumentSelection) {
3333 selProto.removeAllRanges = function() {
3334 // Added try/catch as fix for issue #21
3336 this.docSelection.empty();
3338 // Check for empty() not working (issue #24)
3339 if (this.docSelection.type != "None") {
3340 // Work around failure to empty a control selection by instead selecting a TextRange and then
3343 if (this.anchorNode) {
3344 doc = getDocument(this.anchorNode);
3345 } else if (this.docSelection.type == CONTROL) {
3346 var controlRange = this.docSelection.createRange();
3347 if (controlRange.length) {
3348 doc = getDocument( controlRange.item(0) );
3352 var textRange = getBody(doc).createTextRange();
3354 this.docSelection.empty();
3358 updateEmptySelection(this);
3361 selProto.addRange = function(range) {
3362 if (this.docSelection.type == CONTROL) {
3363 addRangeToControlSelection(this, range);
3365 api.WrappedTextRange.rangeToTextRange(range).select();
3366 this._ranges[0] = range;
3367 this.rangeCount = 1;
3368 this.isCollapsed = this._ranges[0].collapsed;
3369 updateAnchorAndFocusFromRange(this, range, false);
3373 selProto.setRanges = function(ranges) {
3374 this.removeAllRanges();
3375 var rangeCount = ranges.length;
3376 if (rangeCount > 1) {
3377 createControlSelection(this, ranges);
3378 } else if (rangeCount) {
3379 this.addRange(ranges[0]);
3383 module.fail("No means of selecting a Range or TextRange was found");
3387 selProto.getRangeAt = function(index) {
3388 if (index < 0 || index >= this.rangeCount) {
3389 throw new DOMException("INDEX_SIZE_ERR");
3391 // Clone the range to preserve selection-range independence. See issue 80.
3392 return this._ranges[index].cloneRange();
3396 var refreshSelection;
3398 if (useDocumentSelection) {
3399 refreshSelection = function(sel) {
3401 if (api.isSelectionValid(sel.win)) {
3402 range = sel.docSelection.createRange();
3404 range = getBody(sel.win.document).createTextRange();
3405 range.collapse(true);
3408 if (sel.docSelection.type == CONTROL) {
3409 updateControlSelection(sel);
3410 } else if (isTextRange(range)) {
3411 updateFromTextRange(sel, range);
3413 updateEmptySelection(sel);
3416 } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
3417 refreshSelection = function(sel) {
3418 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
3419 updateControlSelection(sel);
3421 sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
3422 if (sel.rangeCount) {
3423 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3424 sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
3426 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
3427 sel.isCollapsed = selectionIsCollapsed(sel);
3429 updateEmptySelection(sel);
3433 } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
3434 refreshSelection = function(sel) {
3435 var range, nativeSel = sel.nativeSelection;
3436 if (nativeSel.anchorNode) {
3437 range = getSelectionRangeAt(nativeSel, 0);
3438 sel._ranges = [range];
3440 updateAnchorAndFocusFromNativeSelection(sel);
3441 sel.isCollapsed = selectionIsCollapsed(sel);
3443 updateEmptySelection(sel);
3447 module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
3451 selProto.refresh = function(checkForChanges) {
3452 var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
3453 var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
3455 refreshSelection(this);
3456 if (checkForChanges) {
3457 // Check the range count first
3458 var i = oldRanges.length;
3459 if (i != this._ranges.length) {
3463 // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
3464 // ranges after this
3465 if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
3469 // Finally, compare each range in turn
3471 if (!rangesEqual(oldRanges[i], this._ranges[i])) {
3479 // Removal of a single range
3480 var removeRangeManually = function(sel, range) {
3481 var ranges = sel.getAllRanges();
3482 sel.removeAllRanges();
3483 for (var i = 0, len = ranges.length; i < len; ++i) {
3484 if (!rangesEqual(range, ranges[i])) {
3485 sel.addRange(ranges[i]);
3488 if (!sel.rangeCount) {
3489 updateEmptySelection(sel);
3493 if (implementsControlRange && implementsDocSelection) {
3494 selProto.removeRange = function(range) {
3495 if (this.docSelection.type == CONTROL) {
3496 var controlRange = this.docSelection.createRange();
3497 var rangeElement = getSingleElementFromRange(range);
3499 // Create a new ControlRange containing all the elements in the selected ControlRange minus the
3500 // element contained by the supplied range
3501 var doc = getDocument(controlRange.item(0));
3502 var newControlRange = getBody(doc).createControlRange();
3503 var el, removed = false;
3504 for (var i = 0, len = controlRange.length; i < len; ++i) {
3505 el = controlRange.item(i);
3506 if (el !== rangeElement || removed) {
3507 newControlRange.add(controlRange.item(i));
3512 newControlRange.select();
3514 // Update the wrapped selection based on what's now in the native selection
3515 updateControlSelection(this);
3517 removeRangeManually(this, range);
3521 selProto.removeRange = function(range) {
3522 removeRangeManually(this, range);
3526 // Detecting if a selection is backward
3527 var selectionIsBackward;
3528 if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
3529 selectionIsBackward = winSelectionIsBackward;
3531 selProto.isBackward = function() {
3532 return selectionIsBackward(this);
3535 selectionIsBackward = selProto.isBackward = function() {
3540 // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
3541 selProto.isBackwards = selProto.isBackward;
3543 // Selection stringifier
3544 // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
3545 // The current spec does not yet define this method.
3546 selProto.toString = function() {
3547 var rangeTexts = [];
3548 for (var i = 0, len = this.rangeCount; i < len; ++i) {
3549 rangeTexts[i] = "" + this._ranges[i];
3551 return rangeTexts.join("");
3554 function assertNodeInSameDocument(sel, node) {
3555 if (sel.win.document != getDocument(node)) {
3556 throw new DOMException("WRONG_DOCUMENT_ERR");
3560 // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
3561 selProto.collapse = function(node, offset) {
3562 assertNodeInSameDocument(this, node);
3563 var range = api.createRange(node);
3564 range.collapseToPoint(node, offset);
3565 this.setSingleRange(range);
3566 this.isCollapsed = true;
3569 selProto.collapseToStart = function() {
3570 if (this.rangeCount) {
3571 var range = this._ranges[0];
3572 this.collapse(range.startContainer, range.startOffset);
3574 throw new DOMException("INVALID_STATE_ERR");
3578 selProto.collapseToEnd = function() {
3579 if (this.rangeCount) {
3580 var range = this._ranges[this.rangeCount - 1];
3581 this.collapse(range.endContainer, range.endOffset);
3583 throw new DOMException("INVALID_STATE_ERR");
3587 // The spec is very specific on how selectAllChildren should be implemented so the native implementation is
3588 // never used by Rangy.
3589 selProto.selectAllChildren = function(node) {
3590 assertNodeInSameDocument(this, node);
3591 var range = api.createRange(node);
3592 range.selectNodeContents(node);
3593 this.setSingleRange(range);
3596 selProto.deleteFromDocument = function() {
3597 // Sepcial behaviour required for IE's control selections
3598 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3599 var controlRange = this.docSelection.createRange();
3601 while (controlRange.length) {
3602 element = controlRange.item(0);
3603 controlRange.remove(element);
3604 element.parentNode.removeChild(element);
3607 } else if (this.rangeCount) {
3608 var ranges = this.getAllRanges();
3609 if (ranges.length) {
3610 this.removeAllRanges();
3611 for (var i = 0, len = ranges.length; i < len; ++i) {
3612 ranges[i].deleteContents();
3614 // The spec says nothing about what the selection should contain after calling deleteContents on each
3615 // range. Firefox moves the selection to where the final selected range was, so we emulate that
3616 this.addRange(ranges[len - 1]);
3621 // The following are non-standard extensions
3622 selProto.eachRange = function(func, returnValue) {
3623 for (var i = 0, len = this._ranges.length; i < len; ++i) {
3624 if ( func( this.getRangeAt(i) ) ) {
3630 selProto.getAllRanges = function() {
3632 this.eachRange(function(range) {
3638 selProto.setSingleRange = function(range, direction) {
3639 this.removeAllRanges();
3640 this.addRange(range, direction);
3643 selProto.callMethodOnEachRange = function(methodName, params) {
3645 this.eachRange( function(range) {
3646 results.push( range[methodName].apply(range, params) );
3651 function createStartOrEndSetter(isStart) {
3652 return function(node, offset) {
3654 if (this.rangeCount) {
3655 range = this.getRangeAt(0);
3656 range["set" + (isStart ? "Start" : "End")](node, offset);
3658 range = api.createRange(this.win.document);
3659 range.setStartAndEnd(node, offset);
3661 this.setSingleRange(range, this.isBackward());
3665 selProto.setStart = createStartOrEndSetter(true);
3666 selProto.setEnd = createStartOrEndSetter(false);
3668 // Add select() method to Range prototype. Any existing selection will be removed.
3669 api.rangePrototype.select = function(direction) {
3670 getSelection( this.getDocument() ).setSingleRange(this, direction);
3673 selProto.changeEachRange = function(func) {
3675 var backward = this.isBackward();
3677 this.eachRange(function(range) {
3682 this.removeAllRanges();
3683 if (backward && ranges.length == 1) {
3684 this.addRange(ranges[0], "backward");
3686 this.setRanges(ranges);
3690 selProto.containsNode = function(node, allowPartial) {
3691 return this.eachRange( function(range) {
3692 return range.containsNode(node, allowPartial);
3696 selProto.getBookmark = function(containerNode) {
3698 backward: this.isBackward(),
3699 rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
3703 selProto.moveToBookmark = function(bookmark) {
3705 for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
3706 range = api.createRange(this.win);
3707 range.moveToBookmark(rangeBookmark);
3708 selRanges.push(range);
3710 if (bookmark.backward) {
3711 this.setSingleRange(selRanges[0], "backward");
3713 this.setRanges(selRanges);
3717 selProto.toHtml = function() {
3718 var rangeHtmls = [];
3719 this.eachRange(function(range) {
3720 rangeHtmls.push( DomRange.toHtml(range) );
3722 return rangeHtmls.join("");
3725 if (features.implementsTextRange) {
3726 selProto.getNativeTextRange = function() {
3728 if ( (sel = this.docSelection) ) {
3729 var range = sel.createRange();
3730 if (isTextRange(range)) {
3733 throw module.createError("getNativeTextRange: selection is a control selection");
3735 } else if (this.rangeCount > 0) {
3736 return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
3738 throw module.createError("getNativeTextRange: selection contains no range");
3743 function inspect(sel) {
3744 var rangeInspects = [];
3745 var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
3746 var focus = new DomPosition(sel.focusNode, sel.focusOffset);
3747 var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
3749 if (typeof sel.rangeCount != "undefined") {
3750 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3751 rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
3754 return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
3755 ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
3758 selProto.getName = function() {
3759 return "WrappedSelection";
3762 selProto.inspect = function() {
3763 return inspect(this);
3766 selProto.detach = function() {
3767 actOnCachedSelection(this.win, "delete");
3768 deleteProperties(this);
3771 WrappedSelection.detachAll = function() {
3772 actOnCachedSelection(null, "deleteAll");
3775 WrappedSelection.inspect = inspect;
3776 WrappedSelection.isDirectionBackward = isDirectionBackward;
3778 api.Selection = WrappedSelection;
3780 api.selectionPrototype = selProto;
3782 api.addShimListener(function(win) {
3783 if (typeof win.getSelection == "undefined") {
3784 win.getSelection = function() {
3785 return getSelection(win);
3793 /*----------------------------------------------------------------------------------------------------------------*/
3797 * Selection save and restore module for Rangy.
3798 * Saves and restores user selections using marker invisible elements in the DOM.
3800 * Part of Rangy, a cross-browser JavaScript range and selection library
3801 * http://code.google.com/p/rangy/
3803 * Depends on Rangy core.
3805 * Copyright 2014, Tim Down
3806 * Licensed under the MIT license.
3807 * Version: 1.3alpha.20140804
3808 * Build date: 4 August 2014
3810 (function(factory, global) {
3811 if (typeof define == "function" && define.amd) {
3812 // AMD. Register as an anonymous module with a dependency on Rangy.
3813 define(["rangy"], factory);
3815 } else if (typeof exports == "object") {
3816 // Node/CommonJS style for Browserify
3817 module.exports = factory;
3820 // No AMD or CommonJS support so we use the rangy global variable
3821 factory(global.rangy);
3823 })(function(rangy) {
3824 rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
3827 var markerTextChar = "\ufeff";
3829 function gEBI(id, doc) {
3830 return (doc || document).getElementById(id);
3833 function insertRangeBoundaryMarker(range, atStart) {
3834 var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
3836 var doc = dom.getDocument(range.startContainer);
3838 // Clone the Range and collapse to the appropriate boundary point
3839 var boundaryRange = range.cloneRange();
3840 boundaryRange.collapse(atStart);
3842 // Create the marker element containing a single invisible character using DOM methods and insert it
3843 markerEl = doc.createElement("span");
3844 markerEl.id = markerId;
3845 markerEl.style.lineHeight = "0";
3846 markerEl.style.display = "none";
3847 markerEl.className = "rangySelectionBoundary";
3848 markerEl.appendChild(doc.createTextNode(markerTextChar));
3850 boundaryRange.insertNode(markerEl);
3854 function setRangeBoundary(doc, range, markerId, atStart) {
3855 var markerEl = gEBI(markerId, doc);
3857 range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
3858 markerEl.parentNode.removeChild(markerEl);
3860 module.warn("Marker element has been removed. Cannot restore selection.");
3864 function compareRanges(r1, r2) {
3865 return r2.compareBoundaryPoints(r1.START_TO_START, r1);
3868 function saveRange(range, backward) {
3869 var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
3871 if (range.collapsed) {
3872 endEl = insertRangeBoundaryMarker(range, false);
3879 endEl = insertRangeBoundaryMarker(range, false);
3880 startEl = insertRangeBoundaryMarker(range, true);
3884 startMarkerId: startEl.id,
3885 endMarkerId: endEl.id,
3888 toString: function() {
3889 return "original text: '" + text + "', new text: '" + range.toString() + "'";
3895 function restoreRange(rangeInfo, normalize) {
3896 var doc = rangeInfo.document;
3897 if (typeof normalize == "undefined") {
3900 var range = api.createRange(doc);
3901 if (rangeInfo.collapsed) {
3902 var markerEl = gEBI(rangeInfo.markerId, doc);
3904 markerEl.style.display = "inline";
3905 var previousNode = markerEl.previousSibling;
3907 // Workaround for issue 17
3908 if (previousNode && previousNode.nodeType == 3) {
3909 markerEl.parentNode.removeChild(markerEl);
3910 range.collapseToPoint(previousNode, previousNode.length);
3912 range.collapseBefore(markerEl);
3913 markerEl.parentNode.removeChild(markerEl);
3916 module.warn("Marker element has been removed. Cannot restore selection.");
3919 setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
3920 setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
3924 range.normalizeBoundaries();
3930 function saveRanges(ranges, backward) {
3931 var rangeInfos = [], range, doc;
3933 // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
3934 ranges = ranges.slice(0);
3935 ranges.sort(compareRanges);
3937 for (var i = 0, len = ranges.length; i < len; ++i) {
3938 rangeInfos[i] = saveRange(ranges[i], backward);
3941 // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
3942 // between its markers
3943 for (i = len - 1; i >= 0; --i) {
3945 doc = api.DomRange.getRangeDocument(range);
3946 if (range.collapsed) {
3947 range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
3949 range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
3950 range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
3957 function saveSelection(win) {
3958 if (!api.isSelectionValid(win)) {
3959 module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
3962 var sel = api.getSelection(win);
3963 var ranges = sel.getAllRanges();
3964 var backward = (ranges.length == 1 && sel.isBackward());
3966 var rangeInfos = saveRanges(ranges, backward);
3968 // Ensure current selection is unaffected
3970 sel.setSingleRange(ranges[0], "backward");
3972 sel.setRanges(ranges);
3977 rangeInfos: rangeInfos,
3982 function restoreRanges(rangeInfos) {
3985 // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
3986 // normalization affecting previously restored ranges.
3987 var rangeCount = rangeInfos.length;
3989 for (var i = rangeCount - 1; i >= 0; i--) {
3990 ranges[i] = restoreRange(rangeInfos[i], true);
3996 function restoreSelection(savedSelection, preserveDirection) {
3997 if (!savedSelection.restored) {
3998 var rangeInfos = savedSelection.rangeInfos;
3999 var sel = api.getSelection(savedSelection.win);
4000 var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
4002 if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
4003 sel.removeAllRanges();
4004 sel.addRange(ranges[0], true);
4006 sel.setRanges(ranges);
4009 savedSelection.restored = true;
4013 function removeMarkerElement(doc, markerId) {
4014 var markerEl = gEBI(markerId, doc);
4016 markerEl.parentNode.removeChild(markerEl);
4020 function removeMarkers(savedSelection) {
4021 var rangeInfos = savedSelection.rangeInfos;
4022 for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
4023 rangeInfo = rangeInfos[i];
4024 if (rangeInfo.collapsed) {
4025 removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
4027 removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
4028 removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
4033 api.util.extend(api, {
4034 saveRange: saveRange,
4035 restoreRange: restoreRange,
4036 saveRanges: saveRanges,
4037 restoreRanges: restoreRanges,
4038 saveSelection: saveSelection,
4039 restoreSelection: restoreSelection,
4040 removeMarkerElement: removeMarkerElement,
4041 removeMarkers: removeMarkers
4046 Base.js, version 1.1a
4047 Copyright 2006-2010, Dean Edwards
4048 License: http://www.opensource.org/licenses/mit-license.php
4051 var Base = function() {
4055 Base.extend = function(_instance, _static) { // subclass
4056 var extend = Base.prototype.extend;
4058 // build the prototype
4059 Base._prototyping = true;
4060 var proto = new this;
4061 extend.call(proto, _instance);
4062 proto.base = function() {
4063 // call this method from any other method to invoke that method's ancestor
4065 delete Base._prototyping;
4067 // create the wrapper for the constructor function
4068 //var constructor = proto.constructor.valueOf(); //-dean
4069 var constructor = proto.constructor;
4070 var klass = proto.constructor = function() {
4071 if (!Base._prototyping) {
4072 if (this._constructing || this.constructor == klass) { // instantiation
4073 this._constructing = true;
4074 constructor.apply(this, arguments);
4075 delete this._constructing;
4076 } else if (arguments[0] != null) { // casting
4077 return (arguments[0].extend || extend).call(arguments[0], proto);
4082 // build the class interface
4083 klass.ancestor = this;
4084 klass.extend = this.extend;
4085 klass.forEach = this.forEach;
4086 klass.implement = this.implement;
4087 klass.prototype = proto;
4088 klass.toString = this.toString;
4089 klass.valueOf = function(type) {
4090 //return (type == "object") ? klass : constructor; //-dean
4091 return (type == "object") ? klass : constructor.valueOf();
4093 extend.call(klass, _static);
4094 // class initialisation
4095 if (typeof klass.init == "function") klass.init();
4100 extend: function(source, value) {
4101 if (arguments.length > 1) { // extending with a name/value pair
4102 var ancestor = this[source];
4103 if (ancestor && (typeof value == "function") && // overriding a method?
4104 // the valueOf() comparison is to avoid circular references
4105 (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
4106 /\bbase\b/.test(value)) {
4107 // get the underlying method
4108 var method = value.valueOf();
4110 value = function() {
4111 var previous = this.base || Base.prototype.base;
4112 this.base = ancestor;
4113 var returnValue = method.apply(this, arguments);
4114 this.base = previous;
4117 // point to the underlying method
4118 value.valueOf = function(type) {
4119 return (type == "object") ? value : method;
4121 value.toString = Base.toString;
4123 this[source] = value;
4124 } else if (source) { // extending with an object literal
4125 var extend = Base.prototype.extend;
4126 // if this object has a customised extend method then use it
4127 if (!Base._prototyping && typeof this != "function") {
4128 extend = this.extend || extend;
4130 var proto = {toSource: null};
4131 // do the "toString" and other methods manually
4132 var hidden = ["constructor", "toString", "valueOf"];
4133 // if we are prototyping then include the constructor
4134 var i = Base._prototyping ? 0 : 1;
4135 while (key = hidden[i++]) {
4136 if (source[key] != proto[key]) {
4137 extend.call(this, key, source[key]);
4141 // copy each of the source object's properties to this object
4142 for (var key in source) {
4143 if (!proto[key]) extend.call(this, key, source[key]);
4151 Base = Base.extend({
4152 constructor: function() {
4153 this.extend(arguments[0]);
4159 forEach: function(object, block, context) {
4160 for (var key in object) {
4161 if (this.prototype[key] === undefined) {
4162 block.call(context, object[key], key, object);
4167 implement: function() {
4168 for (var i = 0; i < arguments.length; i++) {
4169 if (typeof arguments[i] == "function") {
4170 // if it's a function, call it
4171 arguments[i](this.prototype);
4173 // add the interface using the extend method
4174 this.prototype.extend(arguments[i]);
4180 toString: function() {
4181 return String(this.valueOf());
4184 * Detect browser support for specific features
4186 wysihtml5.browser = (function() {
4187 var userAgent = navigator.userAgent,
4188 testElement = document.createElement("div"),
4189 // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
4190 isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1,
4191 isWebKit = userAgent.indexOf("AppleWebKit/") !== -1,
4192 isChrome = userAgent.indexOf("Chrome/") !== -1,
4193 isOpera = userAgent.indexOf("Opera/") !== -1;
4195 function iosVersion(userAgent) {
4196 return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1];
4199 function androidVersion(userAgent) {
4200 return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1];
4203 function isIE(version, equation) {
4207 if (navigator.appName == 'Microsoft Internet Explorer') {
4208 re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
4209 } else if (navigator.appName == 'Netscape') {
4210 re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})");
4213 if (re && re.exec(navigator.userAgent) != null) {
4214 rv = parseFloat(RegExp.$1);
4217 if (rv === -1) { return false; }
4218 if (!version) { return true; }
4219 if (!equation) { return version === rv; }
4220 if (equation === "<") { return version < rv; }
4221 if (equation === ">") { return version > rv; }
4222 if (equation === "<=") { return version <= rv; }
4223 if (equation === ">=") { return version >= rv; }
4227 // Static variable needed, publicly accessible, to be able override it in unit tests
4228 USER_AGENT: userAgent,
4231 * Exclude browsers that are not capable of displaying and handling
4232 * contentEditable as desired:
4233 * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
4234 * - IE < 8 create invalid markup and crash randomly from time to time
4238 supported: function() {
4239 var userAgent = this.USER_AGENT.toLowerCase(),
4240 // Essential for making html elements editable
4241 hasContentEditableSupport = "contentEditable" in testElement,
4242 // Following methods are needed in order to interact with the contentEditable area
4243 hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState,
4244 // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
4245 hasQuerySelectorSupport = document.querySelector && document.querySelectorAll,
4246 // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
4247 isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || (this.isAndroid() && androidVersion(userAgent) < 4) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;
4248 return hasContentEditableSupport
4249 && hasEditingApiSupport
4250 && hasQuerySelectorSupport
4251 && !isIncompatibleMobileBrowser;
4254 isTouchDevice: function() {
4255 return this.supportsEvent("touchmove");
4259 return (/ipad|iphone|ipod/i).test(this.USER_AGENT);
4262 isAndroid: function() {
4263 return this.USER_AGENT.indexOf("Android") !== -1;
4267 * Whether the browser supports sandboxed iframes
4268 * Currently only IE 6+ offers such feature <iframe security="restricted">
4270 * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx
4271 * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx
4273 * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)
4275 supportsSandboxedIframes: function() {
4280 * IE6+7 throw a mixed content warning when the src of an iframe
4281 * is empty/unset or about:blank
4282 * window.querySelector is implemented as of IE8
4284 throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {
4285 return !("querySelector" in document);
4289 * Whether the caret is correctly displayed in contentEditable elements
4290 * Firefox sometimes shows a huge caret in the beginning after focusing
4292 displaysCaretInEmptyContentEditableCorrectly: function() {
4297 * Opera and IE are the only browsers who offer the css value
4298 * in the original unit, thx to the currentStyle object
4299 * All other browsers provide the computed style in px via window.getComputedStyle
4301 hasCurrentStyleProperty: function() {
4302 return "currentStyle" in testElement;
4306 * Firefox on OSX navigates through history when hitting CMD + Arrow right/left
4308 hasHistoryIssue: function() {
4309 return isGecko && navigator.platform.substr(0, 3) === "Mac";
4313 * Whether the browser inserts a <br> when pressing enter in a contentEditable element
4315 insertsLineBreaksOnReturn: function() {
4319 supportsPlaceholderAttributeOn: function(element) {
4320 return "placeholder" in element;
4323 supportsEvent: function(eventName) {
4324 return "on" + eventName in testElement || (function() {
4325 testElement.setAttribute("on" + eventName, "return;");
4326 return typeof(testElement["on" + eventName]) === "function";
4331 * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe
4333 supportsEventsInIframeCorrectly: function() {
4338 * Everything below IE9 doesn't know how to treat HTML5 tags
4340 * @param {Object} context The document object on which to check HTML5 support
4343 * wysihtml5.browser.supportsHTML5Tags(document);
4345 supportsHTML5Tags: function(context) {
4346 var element = context.createElement("div"),
4347 html5 = "<article>foo</article>";
4348 element.innerHTML = html5;
4349 return element.innerHTML.toLowerCase() === html5;
4353 * Checks whether a document supports a certain queryCommand
4354 * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree
4355 * in oder to report correct results
4357 * @param {Object} doc Document object on which to check for a query command
4358 * @param {String} command The query command to check for
4362 * wysihtml5.browser.supportsCommand(document, "bold");
4364 supportsCommand: (function() {
4365 // Following commands are supported but contain bugs in some browsers
4366 var buggyCommands = {
4367 // formatBlock fails with some tags (eg. <blockquote>)
4368 "formatBlock": isIE(10, "<="),
4369 // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets
4370 // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)
4371 // IE and Opera act a bit different here as they convert the entire content of the current block element into a list
4372 "insertUnorderedList": isIE(),
4373 "insertOrderedList": isIE()
4376 // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands
4378 "insertHTML": isGecko
4381 return function(doc, command) {
4382 var isBuggy = buggyCommands[command];
4384 // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled
4386 return doc.queryCommandSupported(command);
4390 return doc.queryCommandEnabled(command);
4392 return !!supported[command];
4400 * IE: URLs starting with:
4401 * www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://,
4402 * nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url:
4403 * will automatically be auto-linked when either the user inserts them via copy&paste or presses the
4404 * space bar when the caret is directly after such an url.
4405 * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll
4406 * (related blog post on msdn
4407 * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).
4409 doesAutoLinkingInContentEditable: function() {
4414 * As stated above, IE auto links urls typed into contentEditable elements
4415 * Since IE9 it's possible to prevent this behavior
4417 canDisableAutoLinking: function() {
4418 return this.supportsCommand(document, "AutoUrlDetect");
4422 * IE leaves an empty paragraph in the contentEditable element after clearing it
4423 * Chrome/Safari sometimes an empty <div>
4425 clearsContentEditableCorrectly: function() {
4426 return isGecko || isOpera || isWebKit;
4430 * IE gives wrong results for getAttribute
4432 supportsGetAttributeCorrectly: function() {
4433 var td = document.createElement("td");
4434 return td.getAttribute("rowspan") != "1";
4438 * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them.
4439 * Chrome and Safari both don't support this
4441 canSelectImagesInContentEditable: function() {
4442 return isGecko || isIE() || isOpera;
4446 * All browsers except Safari and Chrome automatically scroll the range/caret position into view
4448 autoScrollsToCaret: function() {
4453 * Check whether the browser automatically closes tags that don't need to be opened
4455 autoClosesUnclosedTags: function() {
4456 var clonedTestElement = testElement.cloneNode(false),
4460 clonedTestElement.innerHTML = "<p><div></div>";
4461 innerHTML = clonedTestElement.innerHTML.toLowerCase();
4462 returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";
4464 // Cache result by overwriting current function
4465 this.autoClosesUnclosedTags = function() { return returnValue; };
4471 * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists
4473 supportsNativeGetElementsByClassName: function() {
4474 return String(document.getElementsByClassName).indexOf("[native code]") !== -1;
4478 * As of now (19.04.2011) only supported by Firefox 4 and Chrome
4479 * See https://developer.mozilla.org/en/DOM/Selection/modify
4481 supportsSelectionModify: function() {
4482 return "getSelection" in window && "modify" in window.getSelection();
4486 * Opera needs a white space after a <br> in order to position the caret correctly
4488 needsSpaceAfterLineBreak: function() {
4493 * Whether the browser supports the speech api on the given element
4494 * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
4497 * var input = document.createElement("input");
4498 * if (wysihtml5.browser.supportsSpeechApiOn(input)) {
4502 supportsSpeechApiOn: function(input) {
4503 var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [undefined, 0];
4504 return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input);
4508 * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest
4509 * See https://connect.microsoft.com/ie/feedback/details/650112
4510 * or try the POC http://tifftiff.de/ie9_crash/
4512 crashesWhenDefineProperty: function(property) {
4513 return isIE(9) && (property === "XMLHttpRequest" || property === "XDomainRequest");
4517 * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element
4519 doesAsyncFocus: function() {
4524 * 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
4526 hasProblemsSettingCaretAfterImg: function() {
4530 hasUndoInContextMenu: function() {
4531 return isGecko || isChrome || isOpera;
4535 * Opera sometimes doesn't insert the node at the right position when range.insertNode(someNode)
4536 * is used (regardless if rangy or native)
4537 * This especially happens when the caret is positioned right after a <br> because then
4538 * insertNode() will insert the node right before the <br>
4540 hasInsertNodeIssue: function() {
4545 * IE 8+9 don't fire the focus event of the <body> when the iframe gets focused (even though the caret gets set into the <body>)
4547 hasIframeFocusIssue: function() {
4552 * Chrome + Safari create invalid nested markup after paste
4556 * <p>bar</p> <!-- BOO! -->
4559 createsNestedInvalidMarkupAfterPaste: function() {
4563 supportsMutationEvents: function() {
4564 return ("MutationEvent" in window);
4568 IE (at least up to 11) does not support clipboardData on event.
4569 It is on window but cannot return text/html
4570 Should actually check for clipboardData on paste event, but cannot in firefox
4572 supportsModenPaste: function () {
4573 return !("clipboardData" in window);
4577 ;wysihtml5.lang.array = function(arr) {
4580 * Check whether a given object exists in an array
4583 * wysihtml5.lang.array([1, 2]).contains(1);
4586 * Can be used to match array with array. If intersection is found true is returned
4588 contains: function(needle) {
4589 if (Array.isArray(needle)) {
4590 for (var i = needle.length; i--;) {
4591 if (wysihtml5.lang.array(arr).indexOf(needle[i]) !== -1) {
4597 return wysihtml5.lang.array(arr).indexOf(needle) !== -1;
4602 * Check whether a given object exists in an array and return index
4603 * If no elelemt found returns -1
4606 * wysihtml5.lang.array([1, 2]).indexOf(2);
4609 indexOf: function(needle) {
4611 return arr.indexOf(needle);
4613 for (var i=0, length=arr.length; i<length; i++) {
4614 if (arr[i] === needle) { return i; }
4621 * Substract one array from another
4624 * wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]);
4627 without: function(arrayToSubstract) {
4628 arrayToSubstract = wysihtml5.lang.array(arrayToSubstract);
4631 length = arr.length;
4632 for (; i<length; i++) {
4633 if (!arrayToSubstract.contains(arr[i])) {
4634 newArr.push(arr[i]);
4641 * Return a clean native array
4643 * Following will convert a Live NodeList to a proper Array
4645 * var childNodes = wysihtml5.lang.array(document.body.childNodes).get();
4649 length = arr.length,
4651 for (; i<length; i++) {
4652 newArray.push(arr[i]);
4658 * Creates a new array with the results of calling a provided function on every element in this array.
4659 * optionally this can be provided as second argument
4662 * var childNodes = wysihtml5.lang.array([1,2,3,4]).map(function (value, index, array) {
4667 map: function(callback, thisArg) {
4668 if (Array.prototype.map) {
4669 return arr.map(callback, thisArg);
4671 var len = arr.length >>> 0,
4674 for (; i < len; i++) {
4675 A[i] = callback.call(thisArg, arr[i], i, arr);
4681 /* ReturnS new array without duplicate entries
4684 * var uniq = wysihtml5.lang.array([1,2,3,2,1,4]).unique();
4687 unique: function() {
4693 if (!wysihtml5.lang.array(vals).contains(arr[idx])) {
4694 vals.push(arr[idx]);
4703 ;wysihtml5.lang.Dispatcher = Base.extend(
4704 /** @scope wysihtml5.lang.Dialog.prototype */ {
4705 on: function(eventName, handler) {
4706 this.events = this.events || {};
4707 this.events[eventName] = this.events[eventName] || [];
4708 this.events[eventName].push(handler);
4712 off: function(eventName, handler) {
4713 this.events = this.events || {};
4718 handlers = this.events[eventName] || [],
4720 for (; i<handlers.length; i++) {
4721 if (handlers[i] !== handler && handler) {
4722 newHandlers.push(handlers[i]);
4725 this.events[eventName] = newHandlers;
4727 // Clean up all events
4733 fire: function(eventName, payload) {
4734 this.events = this.events || {};
4735 var handlers = this.events[eventName] || [],
4737 for (; i<handlers.length; i++) {
4738 handlers[i].call(this, payload);
4743 // deprecated, use .on()
4744 observe: function() {
4745 return this.on.apply(this, arguments);
4748 // deprecated, use .off()
4749 stopObserving: function() {
4750 return this.off.apply(this, arguments);
4753 ;wysihtml5.lang.object = function(obj) {
4757 * wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get();
4758 * // => { foo: 1, bar: 2, baz: 3 }
4760 merge: function(otherObj) {
4761 for (var i in otherObj) {
4762 obj[i] = otherObj[i];
4773 * wysihtml5.lang.object({ foo: 1 }).clone();
4776 * v0.4.14 adds options for deep clone : wysihtml5.lang.object({ foo: 1 }).clone(true);
4778 clone: function(deep) {
4782 if (obj === null || !wysihtml5.lang.object(obj).isPlainObject()) {
4787 if(obj.hasOwnProperty(i)) {
4789 newObj[i] = wysihtml5.lang.object(obj[i]).clone(deep);
4800 * wysihtml5.lang.object([]).isArray();
4803 isArray: function() {
4804 return Object.prototype.toString.call(obj) === "[object Array]";
4809 * wysihtml5.lang.object(function() {}).isFunction();
4812 isFunction: function() {
4813 return Object.prototype.toString.call(obj) === '[object Function]';
4816 isPlainObject: function () {
4817 return Object.prototype.toString.call(obj) === '[object Object]';
4822 var WHITE_SPACE_START = /^\s+/,
4823 WHITE_SPACE_END = /\s+$/,
4824 ENTITY_REG_EXP = /[&<>\t"]/g,
4832 wysihtml5.lang.string = function(str) {
4837 * wysihtml5.lang.string(" foo ").trim();
4841 return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, "");
4846 * wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" });
4847 * // => "Hello Christopher"
4849 interpolate: function(vars) {
4850 for (var i in vars) {
4851 str = this.replace("#{" + i + "}").by(vars[i]);
4858 * wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans");
4859 * // => "Hello Hans"
4861 replace: function(search) {
4863 by: function(replace) {
4864 return str.split(search).join(replace);
4871 * wysihtml5.lang.string("hello<br>").escapeHTML();
4872 * // => "hello<br>"
4874 escapeHTML: function(linebreaks, convertSpaces) {
4875 var html = str.replace(ENTITY_REG_EXP, function(c) { return ENTITY_MAP[c]; });
4877 html = html.replace(/(?:\r\n|\r|\n)/g, '<br />');
4879 if (convertSpaces) {
4880 html = html.replace(/ /gi, " ");
4888 * Find urls in descendant text nodes of an element and auto-links them
4889 * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/
4891 * @param {Element} element Container element in which to search for urls
4894 * <div id="text-container">Please click here: www.google.com</div>
4895 * <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script>
4897 (function(wysihtml5) {
4899 * Don't auto-link urls that are contained in the following elements:
4901 IGNORE_URLS_IN = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),
4904 * /(\S+\.{1}[^\s\,\.\!]+)/g
4907 * /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
4909 * put this in the beginning if you don't wan't to match within a word
4910 * (^|[\>\(\{\[\s\>])
4912 URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi,
4913 TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,
4914 MAX_DISPLAY_LENGTH = 100,
4915 BRACKETS = { ")": "(", "]": "[", "}": "{" };
4917 function autoLink(element, ignoreInClasses) {
4918 if (_hasParentThatShouldBeIgnored(element, ignoreInClasses)) {
4922 if (element === element.ownerDocument.documentElement) {
4923 element = element.ownerDocument.body;
4926 return _parseNode(element, ignoreInClasses);
4930 * This is basically a rebuild of
4931 * the rails auto_link_urls text helper
4933 function _convertUrlsToLinks(str) {
4934 return str.replace(URL_REG_EXP, function(match, url) {
4935 var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",
4936 opening = BRACKETS[punctuation];
4937 url = url.replace(TRAILING_CHAR_REG_EXP, "");
4939 if (url.split(opening).length > url.split(punctuation).length) {
4940 url = url + punctuation;
4945 if (url.length > MAX_DISPLAY_LENGTH) {
4946 displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";
4948 // Add http prefix if necessary
4949 if (realUrl.substr(0, 4) === "www.") {
4950 realUrl = "http://" + realUrl;
4953 return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation;
4958 * Creates or (if already cached) returns a temp element
4959 * for the given document object
4961 function _getTempElement(context) {
4962 var tempElement = context._wysihtml5_tempElement;
4964 tempElement = context._wysihtml5_tempElement = context.createElement("div");
4970 * Replaces the original text nodes with the newly auto-linked dom tree
4972 function _wrapMatchesInNode(textNode) {
4973 var parentNode = textNode.parentNode,
4974 nodeValue = wysihtml5.lang.string(textNode.data).escapeHTML(),
4975 tempElement = _getTempElement(parentNode.ownerDocument);
4977 // We need to insert an empty/temporary <span /> to fix IE quirks
4978 // Elsewise IE would strip white space in the beginning
4979 tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(nodeValue);
4980 tempElement.removeChild(tempElement.firstChild);
4982 while (tempElement.firstChild) {
4983 // inserts tempElement.firstChild before textNode
4984 parentNode.insertBefore(tempElement.firstChild, textNode);
4986 parentNode.removeChild(textNode);
4989 function _hasParentThatShouldBeIgnored(node, ignoreInClasses) {
4991 while (node.parentNode) {
4992 node = node.parentNode;
4993 nodeName = node.nodeName;
4994 if (node.className && wysihtml5.lang.array(node.className.split(' ')).contains(ignoreInClasses)) {
4997 if (IGNORE_URLS_IN.contains(nodeName)) {
4999 } else if (nodeName === "body") {
5006 function _parseNode(element, ignoreInClasses) {
5007 if (IGNORE_URLS_IN.contains(element.nodeName)) {
5011 if (element.className && wysihtml5.lang.array(element.className.split(' ')).contains(ignoreInClasses)) {
5015 if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) {
5016 _wrapMatchesInNode(element);
5020 var childNodes = wysihtml5.lang.array(element.childNodes).get(),
5021 childNodesLength = childNodes.length,
5024 for (; i<childNodesLength; i++) {
5025 _parseNode(childNodes[i], ignoreInClasses);
5031 wysihtml5.dom.autoLink = autoLink;
5033 // Reveal url reg exp to the outside
5034 wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP;
5036 ;(function(wysihtml5) {
5037 var api = wysihtml5.dom;
5039 api.addClass = function(element, className) {
5040 var classList = element.classList;
5042 return classList.add(className);
5044 if (api.hasClass(element, className)) {
5047 element.className += " " + className;
5050 api.removeClass = function(element, className) {
5051 var classList = element.classList;
5053 return classList.remove(className);
5056 element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " ");
5059 api.hasClass = function(element, className) {
5060 var classList = element.classList;
5062 return classList.contains(className);
5065 var elementClassName = element.className;
5066 return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
5069 ;wysihtml5.dom.contains = (function() {
5070 var documentElement = document.documentElement;
5071 if (documentElement.contains) {
5072 return function(container, element) {
5073 if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
5074 element = element.parentNode;
5076 return container !== element && container.contains(element);
5078 } else if (documentElement.compareDocumentPosition) {
5079 return function(container, element) {
5080 // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
5081 return !!(container.compareDocumentPosition(element) & 16);
5086 * Converts an HTML fragment/element into a unordered/ordered list
5088 * @param {Element} element The element which should be turned into a list
5089 * @param {String} listType The list type in which to convert the tree (either "ul" or "ol")
5090 * @return {Element} The created list
5093 * <!-- Assume the following dom: -->
5094 * <span id="pseudo-list">
5097 * <div>50 Cent</div>
5101 * wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul");
5104 * <!-- Will result in: -->
5111 wysihtml5.dom.convertToList = (function() {
5112 function _createListItem(doc, list) {
5113 var listItem = doc.createElement("li");
5114 list.appendChild(listItem);
5118 function _createList(doc, type) {
5119 return doc.createElement(type);
5122 function convertToList(element, listType, uneditableClass) {
5123 if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {
5128 var doc = element.ownerDocument,
5129 list = _createList(doc, listType),
5130 lineBreaks = element.querySelectorAll("br"),
5131 lineBreaksLength = lineBreaks.length,
5142 // First find <br> at the end of inline elements and move them behind them
5143 for (i=0; i<lineBreaksLength; i++) {
5144 lineBreak = lineBreaks[i];
5145 while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) {
5146 if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") {
5147 parentNode.removeChild(lineBreak);
5150 wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode);
5154 childNodes = wysihtml5.lang.array(element.childNodes).get();
5155 childNodesLength = childNodes.length;
5157 for (i=0; i<childNodesLength; i++) {
5158 currentListItem = currentListItem || _createListItem(doc, list);
5159 childNode = childNodes[i];
5160 isBlockElement = wysihtml5.dom.getStyle("display").from(childNode) === "block";
5161 isLineBreak = childNode.nodeName === "BR";
5163 // consider uneditable as an inline element
5164 if (isBlockElement && (!uneditableClass || !wysihtml5.dom.hasClass(childNode, uneditableClass))) {
5165 // Append blockElement to current <li> if empty, otherwise create a new one
5166 currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem;
5167 currentListItem.appendChild(childNode);
5168 currentListItem = null;
5173 // Only create a new list item in the next iteration when the current one has already content
5174 currentListItem = currentListItem.firstChild ? null : currentListItem;
5178 currentListItem.appendChild(childNode);
5181 if (childNodes.length === 0) {
5182 _createListItem(doc, list);
5185 element.parentNode.replaceChild(list, element);
5189 return convertToList;
5192 * Copy a set of attributes from one element to another
5194 * @param {Array} attributesToCopy List of attributes which should be copied
5195 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
5196 * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked
5197 * with the element where to copy the attributes to (see example)
5200 * var textarea = document.querySelector("textarea"),
5201 * div = document.querySelector("div[contenteditable=true]"),
5202 * anotherDiv = document.querySelector("div.preview");
5203 * wysihtml5.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
5206 wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5208 from: function(elementToCopyFrom) {
5210 to: function(elementToCopyTo) {
5213 length = attributesToCopy.length;
5214 for (; i<length; i++) {
5215 attribute = attributesToCopy[i];
5216 if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") {
5217 elementToCopyTo[attribute] = elementToCopyFrom[attribute];
5220 return { andTo: arguments.callee };
5227 * Copy a set of styles from one element to another
5228 * Please note that this only works properly across browsers when the element from which to copy the styles
5231 * Interesting article on how to copy styles
5233 * @param {Array} stylesToCopy List of styles which should be copied
5234 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
5235 * copy the styles from., this again returns an object which provides a method named "to" which can be invoked
5236 * with the element where to copy the styles to (see example)
5239 * var textarea = document.querySelector("textarea"),
5240 * div = document.querySelector("div[contenteditable=true]"),
5241 * anotherDiv = document.querySelector("div.preview");
5242 * wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv);
5248 * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set
5249 * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then
5250 * its computed css width will be 198px
5252 * See https://bugzilla.mozilla.org/show_bug.cgi?id=520992
5254 var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"];
5256 var shouldIgnoreBoxSizingBorderBox = function(element) {
5257 if (hasBoxSizingBorderBox(element)) {
5258 return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
5263 var hasBoxSizingBorderBox = function(element) {
5265 length = BOX_SIZING_PROPERTIES.length;
5266 for (; i<length; i++) {
5267 if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {
5268 return BOX_SIZING_PROPERTIES[i];
5273 dom.copyStyles = function(stylesToCopy) {
5275 from: function(element) {
5276 if (shouldIgnoreBoxSizingBorderBox(element)) {
5277 stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);
5281 length = stylesToCopy.length,
5284 for (; i<length; i++) {
5285 property = stylesToCopy[i];
5286 cssText += property + ":" + dom.getStyle(property).from(element) + ";";
5290 to: function(element) {
5291 dom.setStyles(cssText).on(element);
5292 return { andTo: arguments.callee };
5303 * wysihtml5.dom.delegate(document.body, "a", "click", function() {
5307 (function(wysihtml5) {
5309 wysihtml5.dom.delegate = function(container, selector, eventName, handler) {
5310 return wysihtml5.dom.observe(container, eventName, function(event) {
5311 var target = event.target,
5312 match = wysihtml5.lang.array(container.querySelectorAll(selector));
5314 while (target && target !== container) {
5315 if (match.contains(target)) {
5316 handler.call(target, event);
5319 target = target.parentNode;
5325 ;// TODO: Refactor dom tree traversing here
5326 (function(wysihtml5) {
5327 wysihtml5.dom.domNode = function(node) {
5328 var defaultNodeTypes = [wysihtml5.ELEMENT_NODE, wysihtml5.TEXT_NODE];
5330 var _isBlankText = function(node) {
5331 return node.nodeType === wysihtml5.TEXT_NODE && (/^\s*$/g).test(node.data);
5336 // var node = wysihtml5.dom.domNode(element).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
5337 prev: function(options) {
5338 var prevNode = node.previousSibling,
5339 types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes;
5346 (!wysihtml5.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check.
5347 (options && options.ignoreBlankTexts && _isBlankText(prevNode)) // Blank text nodes bypassed if set
5349 return wysihtml5.dom.domNode(prevNode).prev(options);
5355 // var node = wysihtml5.dom.domNode(element).next({nodeTypes: [1,3], ignoreBlankTexts: true});
5356 next: function(options) {
5357 var nextNode = node.nextSibling,
5358 types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes;
5365 (!wysihtml5.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check.
5366 (options && options.ignoreBlankTexts && _isBlankText(nextNode)) // blank text nodes bypassed if set
5368 return wysihtml5.dom.domNode(nextNode).next(options);
5379 * Returns the given html wrapped in a div element
5381 * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
5382 * when inserted via innerHTML
5384 * @param {String} html The html which should be wrapped in a dom element
5385 * @param {Obejct} [context] Document object of the context the html belongs to
5388 * wysihtml5.dom.getAsDom("<article>foo</article>");
5390 wysihtml5.dom.getAsDom = (function() {
5392 var _innerHTMLShiv = function(html, context) {
5393 var tempElement = context.createElement("div");
5394 tempElement.style.display = "none";
5395 context.body.appendChild(tempElement);
5396 // IE throws an exception when trying to insert <frameset></frameset> via innerHTML
5397 try { tempElement.innerHTML = html; } catch(e) {}
5398 context.body.removeChild(tempElement);
5403 * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
5405 var _ensureHTML5Compatibility = function(context) {
5406 if (context._wysihtml5_supportsHTML5Tags) {
5409 for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) {
5410 context.createElement(HTML5_ELEMENTS[i]);
5412 context._wysihtml5_supportsHTML5Tags = true;
5417 * List of html5 tags
5418 * taken from http://simon.html5.org/html5-elements
5420 var HTML5_ELEMENTS = [
5421 "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption",
5422 "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress",
5423 "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr"
5426 return function(html, context) {
5427 context = context || document;
5429 if (typeof(html) === "object" && html.nodeType) {
5430 tempElement = context.createElement("div");
5431 tempElement.appendChild(html);
5432 } else if (wysihtml5.browser.supportsHTML5Tags(context)) {
5433 tempElement = context.createElement("div");
5434 tempElement.innerHTML = html;
5436 _ensureHTML5Compatibility(context);
5437 tempElement = _innerHTMLShiv(html, context);
5443 * Walks the dom tree from the given node up until it finds a match
5444 * Designed for optimal performance.
5446 * @param {Element} node The from which to check the parent nodes
5447 * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp)
5448 * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
5449 * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
5451 * var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] });
5453 * var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" });
5455 * var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g });
5457 wysihtml5.dom.getParentElement = (function() {
5459 function _isSameNodeName(nodeName, desiredNodeNames) {
5460 if (!desiredNodeNames || !desiredNodeNames.length) {
5464 if (typeof(desiredNodeNames) === "string") {
5465 return nodeName === desiredNodeNames;
5467 return wysihtml5.lang.array(desiredNodeNames).contains(nodeName);
5471 function _isElement(node) {
5472 return node.nodeType === wysihtml5.ELEMENT_NODE;
5475 function _hasClassName(element, className, classRegExp) {
5476 var classNames = (element.className || "").match(classRegExp) || [];
5478 return !!classNames.length;
5480 return classNames[classNames.length - 1] === className;
5483 function _hasStyle(element, cssStyle, styleRegExp) {
5484 var styles = (element.getAttribute('style') || "").match(styleRegExp) || [];
5486 return !!styles.length;
5488 return styles[styles.length - 1] === cssStyle;
5491 return function(node, matchingSet, levels, container) {
5492 var findByStyle = (matchingSet.cssStyle || matchingSet.styleRegExp),
5493 findByClass = (matchingSet.className || matchingSet.classRegExp);
5495 levels = levels || 50; // Go max 50 nodes upwards from current node
5497 while (levels-- && node && node.nodeName !== "BODY" && (!container || node !== container)) {
5498 if (_isElement(node) && _isSameNodeName(node.nodeName, matchingSet.nodeName) &&
5499 (!findByStyle || _hasStyle(node, matchingSet.cssStyle, matchingSet.styleRegExp)) &&
5500 (!findByClass || _hasClassName(node, matchingSet.className, matchingSet.classRegExp))
5504 node = node.parentNode;
5510 * Get element's style for a specific css property
5512 * @param {Element} element The element on which to retrieve the style
5513 * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)
5516 * wysihtml5.dom.getStyle("display").from(document.body);
5519 wysihtml5.dom.getStyle = (function() {
5520 var stylePropertyMapping = {
5521 "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"
5523 REG_EXP_CAMELIZE = /\-[a-z]/g;
5525 function camelize(str) {
5526 return str.replace(REG_EXP_CAMELIZE, function(match) {
5527 return match.charAt(1).toUpperCase();
5531 return function(property) {
5533 from: function(element) {
5534 if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
5538 var doc = element.ownerDocument,
5539 camelizedProperty = stylePropertyMapping[property] || camelize(property),
5540 style = element.style,
5541 currentStyle = element.currentStyle,
5542 styleValue = style[camelizedProperty];
5547 // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant
5548 // window.getComputedStyle, since it returns css property values in their original unit:
5549 // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle
5550 // gives you the original "50%".
5551 // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio
5554 return currentStyle[camelizedProperty];
5556 //ie will occasionally fail for unknown reasons. swallowing exception
5560 var win = doc.defaultView || doc.parentWindow,
5561 needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
5565 if (win.getComputedStyle) {
5566 // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars
5567 // therfore we remove and restore the scrollbar and calculate the value in between
5568 if (needsOverflowReset) {
5569 originalOverflow = style.overflow;
5570 style.overflow = "hidden";
5572 returnValue = win.getComputedStyle(element, null).getPropertyValue(property);
5573 if (needsOverflowReset) {
5574 style.overflow = originalOverflow || "";
5582 ;wysihtml5.dom.getTextNodes = function(node, ingoreEmpty){
5584 for (node=node.firstChild;node;node=node.nextSibling){
5585 if (node.nodeType == 3) {
5586 if (!ingoreEmpty || !(/^\s*$/).test(node.innerText || node.textContent)) {
5590 all = all.concat(wysihtml5.dom.getTextNodes(node, ingoreEmpty));
5595 * High performant way to check whether an element with a specific tag name is in the given document
5596 * Optimized for being heavily executed
5597 * Unleashes the power of live node lists
5599 * @param {Object} doc The document object of the context where to check
5600 * @param {String} tagName Upper cased tag name
5602 * wysihtml5.dom.hasElementWithTagName(document, "IMG");
5604 wysihtml5.dom.hasElementWithTagName = (function() {
5605 var LIVE_CACHE = {},
5606 DOCUMENT_IDENTIFIER = 1;
5608 function _getDocumentIdentifier(doc) {
5609 return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
5612 return function(doc, tagName) {
5613 var key = _getDocumentIdentifier(doc) + ":" + tagName,
5614 cacheEntry = LIVE_CACHE[key];
5616 cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
5619 return cacheEntry.length > 0;
5623 * High performant way to check whether an element with a specific class name is in the given document
5624 * Optimized for being heavily executed
5625 * Unleashes the power of live node lists
5627 * @param {Object} doc The document object of the context where to check
5628 * @param {String} tagName Upper cased tag name
5630 * wysihtml5.dom.hasElementWithClassName(document, "foobar");
5632 (function(wysihtml5) {
5633 var LIVE_CACHE = {},
5634 DOCUMENT_IDENTIFIER = 1;
5636 function _getDocumentIdentifier(doc) {
5637 return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
5640 wysihtml5.dom.hasElementWithClassName = function(doc, className) {
5641 // getElementsByClassName is not supported by IE<9
5642 // but is sometimes mocked via library code (which then doesn't return live node lists)
5643 if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) {
5644 return !!doc.querySelector("." + className);
5647 var key = _getDocumentIdentifier(doc) + ":" + className,
5648 cacheEntry = LIVE_CACHE[key];
5650 cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);
5653 return cacheEntry.length > 0;
5656 ;wysihtml5.dom.insert = function(elementToInsert) {
5658 after: function(element) {
5659 element.parentNode.insertBefore(elementToInsert, element.nextSibling);
5662 before: function(element) {
5663 element.parentNode.insertBefore(elementToInsert, element);
5666 into: function(element) {
5667 element.appendChild(elementToInsert);
5671 ;wysihtml5.dom.insertCSS = function(rules) {
5672 rules = rules.join("\n");
5675 into: function(doc) {
5676 var styleElement = doc.createElement("style");
5677 styleElement.type = "text/css";
5679 if (styleElement.styleSheet) {
5680 styleElement.styleSheet.cssText = rules;
5682 styleElement.appendChild(doc.createTextNode(rules));
5685 var link = doc.querySelector("head link");
5687 link.parentNode.insertBefore(styleElement, link);
5690 var head = doc.querySelector("head");
5692 head.appendChild(styleElement);
5698 ;// TODO: Refactor dom tree traversing here
5699 (function(wysihtml5) {
5700 wysihtml5.dom.lineBreaks = function(node) {
5702 function _isLineBreak(n) {
5703 return n.nodeName === "BR";
5707 * Checks whether the elment causes a visual line break
5708 * (<br> or block elements)
5710 function _isLineBreakOrBlockElement(element) {
5711 if (_isLineBreak(element)) {
5715 if (wysihtml5.dom.getStyle("display").from(element) === "block") {
5724 /* wysihtml5.dom.lineBreaks(element).add();
5726 * Adds line breaks before and after the given node if the previous and next siblings
5727 * aren't already causing a visual line break (block element or <br>)
5729 add: function(options) {
5730 var doc = node.ownerDocument,
5731 nextSibling = wysihtml5.dom.domNode(node).next({ignoreBlankTexts: true}),
5732 previousSibling = wysihtml5.dom.domNode(node).prev({ignoreBlankTexts: true});
5734 if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
5735 wysihtml5.dom.insert(doc.createElement("br")).after(node);
5737 if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
5738 wysihtml5.dom.insert(doc.createElement("br")).before(node);
5742 /* wysihtml5.dom.lineBreaks(element).remove();
5744 * Removes line breaks before and after the given node
5746 remove: function(options) {
5747 var nextSibling = wysihtml5.dom.domNode(node).next({ignoreBlankTexts: true}),
5748 previousSibling = wysihtml5.dom.domNode(node).prev({ignoreBlankTexts: true});
5750 if (nextSibling && _isLineBreak(nextSibling)) {
5751 nextSibling.parentNode.removeChild(nextSibling);
5753 if (previousSibling && _isLineBreak(previousSibling)) {
5754 previousSibling.parentNode.removeChild(previousSibling);
5760 * Method to set dom events
5763 * wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
5765 wysihtml5.dom.observe = function(element, eventNames, handler) {
5766 eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;
5771 length = eventNames.length;
5773 for (; i<length; i++) {
5774 eventName = eventNames[i];
5775 if (element.addEventListener) {
5776 element.addEventListener(eventName, handler, false);
5778 handlerWrapper = function(event) {
5779 if (!("target" in event)) {
5780 event.target = event.srcElement;
5782 event.preventDefault = event.preventDefault || function() {
5783 this.returnValue = false;
5785 event.stopPropagation = event.stopPropagation || function() {
5786 this.cancelBubble = true;
5788 handler.call(element, event);
5790 element.attachEvent("on" + eventName, handlerWrapper);
5798 length = eventNames.length;
5799 for (; i<length; i++) {
5800 eventName = eventNames[i];
5801 if (element.removeEventListener) {
5802 element.removeEventListener(eventName, handler, false);
5804 element.detachEvent("on" + eventName, handlerWrapper);
5812 * Rewrites the HTML based on given rules
5814 * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized
5815 * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will
5816 * be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the
5817 * desired substitution.
5818 * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing
5820 * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.
5823 * var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>';
5824 * wysihtml5.dom.parse(userHTML, {
5826 * p: "div", // Rename p tags to div tags
5827 * font: "span" // Rename font tags to span tags
5828 * div: true, // Keep them, also possible (same result when passing: "div" or true)
5829 * script: undefined // Remove script elements
5832 * // => <div><div><span>foo bar</span></div></div>
5834 * var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>';
5835 * wysihtml5.dom.parse(userHTML);
5836 * // => '<span><span><span><span>I'm a table!</span></span></span></span>'
5838 * var userHTML = '<div>foobar<br>foobar</div>';
5839 * wysihtml5.dom.parse(userHTML, {
5847 * var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
5848 * wysihtml5.dom.parse(userHTML, {
5859 * // => '<p class="red">foo</p><p>bar</p>'
5862 wysihtml5.dom.parse = function(elementOrHtml_current, config_current) {
5863 /* TODO: Currently escaped module pattern as otherwise folloowing default swill be shared among multiple editors.
5864 * Refactor whole code as this method while workind is kind of awkward too */
5867 * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML
5868 * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the
5871 * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
5873 var NODE_TYPE_MAPPING = {
5874 "1": _handleElement,
5878 // Rename unknown tags to this
5879 DEFAULT_NODE_NAME = "span",
5880 WHITE_SPACE_REG_EXP = /\s+/,
5881 defaultRules = { tags: {}, classes: {} },
5885 * Iterates over all childs of the element, recreates them, appends them into a document fragment
5886 * which later replaces the entire body content
5888 function parse(elementOrHtml, config) {
5889 wysihtml5.lang.object(currentRules).merge(defaultRules).merge(config.rules).get();
5891 var context = config.context || elementOrHtml.ownerDocument || document,
5892 fragment = context.createDocumentFragment(),
5893 isString = typeof(elementOrHtml) === "string",
5894 clearInternals = false,
5899 if (config.clearInternals === true) {
5900 clearInternals = true;
5904 element = wysihtml5.dom.getAsDom(elementOrHtml, context);
5906 element = elementOrHtml;
5909 if (currentRules.selectors) {
5910 _applySelectorRules(element, currentRules.selectors);
5913 while (element.firstChild) {
5914 firstChild = element.firstChild;
5915 newNode = _convert(firstChild, config.cleanUp, clearInternals, config.uneditableClass);
5917 fragment.appendChild(newNode);
5919 if (firstChild !== newNode) {
5920 element.removeChild(firstChild);
5924 if (config.unjoinNbsps) {
5925 // replace joined non-breakable spaces with unjoined
5926 var txtnodes = wysihtml5.dom.getTextNodes(fragment);
5927 for (var n = txtnodes.length; n--;) {
5928 txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 ");
5932 // Clear element contents
5933 element.innerHTML = "";
5935 // Insert new DOM tree
5936 element.appendChild(fragment);
5938 return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element;
5941 function _convert(oldNode, cleanUp, clearInternals, uneditableClass) {
5942 var oldNodeType = oldNode.nodeType,
5943 oldChilds = oldNode.childNodes,
5944 oldChildsLength = oldChilds.length,
5945 method = NODE_TYPE_MAPPING[oldNodeType],
5951 // Passes directly elemets with uneditable class
5952 if (uneditableClass && oldNodeType === 1 && wysihtml5.dom.hasClass(oldNode, uneditableClass)) {
5956 newNode = method && method(oldNode, clearInternals);
5958 // Remove or unwrap node in case of return value null or false
5960 if (newNode === false) {
5961 // false defines that tag should be removed but contents should remain (unwrap)
5962 fragment = oldNode.ownerDocument.createDocumentFragment();
5964 for (i = oldChildsLength; i--;) {
5966 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
5968 if (oldChilds[i] === newChild) {
5971 fragment.insertBefore(newChild, fragment.firstChild);
5976 if (wysihtml5.dom.getStyle("display").from(oldNode) === "block") {
5977 fragment.appendChild(oldNode.ownerDocument.createElement("br"));
5980 // TODO: try to minimize surplus spaces
5981 if (wysihtml5.lang.array([
5983 "table", "td", "th",
5986 "footer", "header", "section",
5987 "h1", "h2", "h3", "h4", "h5", "h6"
5988 ]).contains(oldNode.nodeName.toLowerCase()) && oldNode.parentNode.lastChild !== oldNode) {
5989 // add space at first when unwraping non-textflow elements
5990 if (!oldNode.nextSibling || oldNode.nextSibling.nodeType !== 3 || !(/^\s/).test(oldNode.nextSibling.nodeValue)) {
5991 fragment.appendChild(oldNode.ownerDocument.createTextNode(" "));
5995 if (fragment.normalize) {
5996 fragment.normalize();
6005 // Converts all childnodes
6006 for (i=0; i<oldChildsLength; i++) {
6008 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
6010 if (oldChilds[i] === newChild) {
6013 newNode.appendChild(newChild);
6018 // Cleanup senseless <span> elements
6020 newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&
6021 (!newNode.childNodes.length ||
6022 ((/^\s*$/gi).test(newNode.innerHTML) && (clearInternals || (oldNode.className !== "_wysihtml5-temp-placeholder" && oldNode.className !== "rangySelectionBoundary"))) ||
6023 !newNode.attributes.length)
6025 fragment = newNode.ownerDocument.createDocumentFragment();
6026 while (newNode.firstChild) {
6027 fragment.appendChild(newNode.firstChild);
6029 if (fragment.normalize) {
6030 fragment.normalize();
6035 if (newNode.normalize) {
6036 newNode.normalize();
6041 function _applySelectorRules (element, selectorRules) {
6042 var sel, method, els;
6044 for (sel in selectorRules) {
6045 if (selectorRules.hasOwnProperty(sel)) {
6046 if (wysihtml5.lang.object(selectorRules[sel]).isFunction()) {
6047 method = selectorRules[sel];
6048 } else if (typeof(selectorRules[sel]) === "string" && elementHandlingMethods[selectorRules[sel]]) {
6049 method = elementHandlingMethods[selectorRules[sel]];
6051 els = element.querySelectorAll(sel);
6052 for (var i = els.length; i--;) {
6059 function _handleElement(oldNode, clearInternals) {
6062 tagRules = currentRules.tags,
6063 nodeName = oldNode.nodeName.toLowerCase(),
6064 scopeName = oldNode.scopeName,
6068 * We already parsed that element
6069 * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
6071 if (oldNode._wysihtml5) {
6074 oldNode._wysihtml5 = 1;
6076 if (oldNode.className === "wysihtml5-temp") {
6081 * IE is the only browser who doesn't include the namespace in the
6082 * nodeName, that's why we have to prepend it by ourselves
6083 * scopeName is a proprietary IE feature
6084 * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
6086 if (scopeName && scopeName != "HTML") {
6087 nodeName = scopeName + ":" + nodeName;
6091 * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags
6092 * 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
6094 if ("outerHTML" in oldNode) {
6095 if (!wysihtml5.browser.autoClosesUnclosedTags() &&
6096 oldNode.nodeName === "P" &&
6097 oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
6102 if (nodeName in tagRules) {
6103 rule = tagRules[nodeName];
6104 if (!rule || rule.remove) {
6106 } else if (rule.unwrap) {
6109 rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;
6110 } else if (oldNode.firstChild) {
6111 rule = { rename_tag: DEFAULT_NODE_NAME };
6113 // Remove empty unknown elements
6117 // tests if type condition is met or node should be removed/unwrapped/renamed
6118 if (rule.one_of_type && !_testTypes(oldNode, currentRules, rule.one_of_type, clearInternals)) {
6119 if (rule.remove_action) {
6120 if (rule.remove_action === "unwrap") {
6122 } else if (rule.remove_action === "rename") {
6123 renameTag = rule.remove_action_rename_to || DEFAULT_NODE_NAME;
6132 newNode = oldNode.ownerDocument.createElement(renameTag || rule.rename_tag || nodeName);
6133 _handleAttributes(oldNode, newNode, rule, clearInternals);
6134 _handleStyles(oldNode, newNode, rule);
6138 if (newNode.normalize) { newNode.normalize(); }
6142 function _testTypes(oldNode, rules, types, clearInternals) {
6143 var definition, type;
6145 // do not interfere with placeholder span or pasting caret position is not maintained
6146 if (oldNode.nodeName === "SPAN" && !clearInternals && (oldNode.className === "_wysihtml5-temp-placeholder" || oldNode.className === "rangySelectionBoundary")) {
6150 for (type in types) {
6151 if (types.hasOwnProperty(type) && rules.type_definitions && rules.type_definitions[type]) {
6152 definition = rules.type_definitions[type];
6153 if (_testType(oldNode, definition)) {
6161 function array_contains(a, obj) {
6171 function _testType(oldNode, definition) {
6173 var nodeClasses = oldNode.getAttribute("class"),
6174 nodeStyles = oldNode.getAttribute("style"),
6175 classesLength, s, s_corrected, a, attr, currentClass, styleProp;
6178 if (definition.methods) {
6179 for (var m in definition.methods) {
6180 if (definition.methods.hasOwnProperty(m) && typeCeckMethods[m]) {
6182 if (typeCeckMethods[m](oldNode)) {
6189 // test for classes, if one found return true
6190 if (nodeClasses && definition.classes) {
6191 nodeClasses = nodeClasses.replace(/^\s+/g, '').replace(/\s+$/g, '').split(WHITE_SPACE_REG_EXP);
6192 classesLength = nodeClasses.length;
6193 for (var i = 0; i < classesLength; i++) {
6194 if (definition.classes[nodeClasses[i]]) {
6200 // test for styles, if one found return true
6201 if (nodeStyles && definition.styles) {
6203 nodeStyles = nodeStyles.split(';');
6204 for (s in definition.styles) {
6205 if (definition.styles.hasOwnProperty(s)) {
6206 for (var sp = nodeStyles.length; sp--;) {
6207 styleProp = nodeStyles[sp].split(':');
6209 if (styleProp[0].replace(/\s/g, '').toLowerCase() === s) {
6210 if (definition.styles[s] === true || definition.styles[s] === 1 || wysihtml5.lang.array(definition.styles[s]).contains(styleProp[1].replace(/\s/g, '').toLowerCase()) ) {
6219 // test for attributes in general against regex match
6220 if (definition.attrs) {
6221 for (a in definition.attrs) {
6222 if (definition.attrs.hasOwnProperty(a)) {
6223 attr = wysihtml5.dom.getAttribute(oldNode, a);
6224 if (typeof(attr) === "string") {
6225 if (attr.search(definition.attrs[a]) > -1) {
6235 function _handleStyles(oldNode, newNode, rule) {
6237 if(rule && rule.keep_styles) {
6238 for (s in rule.keep_styles) {
6239 if (rule.keep_styles.hasOwnProperty(s)) {
6240 v = (s === "float") ? oldNode.style.styleFloat || oldNode.style.cssFloat : oldNode.style[s];
6241 // value can be regex and if so should match or style skipped
6242 if (rule.keep_styles[s] instanceof RegExp && !(rule.keep_styles[s].test(v))) {
6245 if (s === "float") {
6247 newNode.style[(oldNode.style.styleFloat) ? 'styleFloat': 'cssFloat'] = v;
6248 } else if (oldNode.style[s]) {
6249 newNode.style[s] = v;
6256 function _getAttributesBeginningWith(beginning, attributes) {
6257 var returnAttributes = [];
6258 for (var attr in attributes) {
6259 if (attributes.hasOwnProperty(attr) && attr.indexOf(beginning) === 0) {
6260 returnAttributes.push(attr);
6263 return returnAttributes;
6266 function _checkAttribute(attributeName, attributeValue, methodName, nodeName) {
6267 var method = attributeCheckMethods[methodName],
6271 if (attributeValue || (attributeName === "alt" && nodeName == "IMG")) {
6272 newAttributeValue = method(attributeValue);
6273 if (typeof(newAttributeValue) === "string") {
6274 return newAttributeValue;
6282 function _checkAttributes(oldNode, local_attributes) {
6283 var globalAttributes = wysihtml5.lang.object(currentRules.attributes || {}).clone(), // global values for check/convert values of attributes
6284 checkAttributes = wysihtml5.lang.object(globalAttributes).merge( wysihtml5.lang.object(local_attributes || {}).clone()).get(),
6286 oldAttributes = wysihtml5.dom.getAttributes(oldNode),
6287 attributeName, newValue, matchingAttributes;
6289 for (attributeName in checkAttributes) {
6290 if ((/\*$/).test(attributeName)) {
6292 matchingAttributes = _getAttributesBeginningWith(attributeName.slice(0,-1), oldAttributes);
6293 for (var i = 0, imax = matchingAttributes.length; i < imax; i++) {
6295 newValue = _checkAttribute(matchingAttributes[i], oldAttributes[matchingAttributes[i]], checkAttributes[attributeName], oldNode.nodeName);
6296 if (newValue !== false) {
6297 attributes[matchingAttributes[i]] = newValue;
6301 newValue = _checkAttribute(attributeName, oldAttributes[attributeName], checkAttributes[attributeName], oldNode.nodeName);
6302 if (newValue !== false) {
6303 attributes[attributeName] = newValue;
6311 // TODO: refactor. Too long to read
6312 function _handleAttributes(oldNode, newNode, rule, clearInternals) {
6313 var attributes = {}, // fresh new set of attributes to set on newNode
6314 setClass = rule.set_class, // classes to set
6315 addClass = rule.add_class, // add classes based on existing attributes
6316 addStyle = rule.add_style, // add styles based on existing attributes
6317 setAttributes = rule.set_attributes, // attributes to set on the current node
6318 allowedClasses = currentRules.classes,
6331 if (setAttributes) {
6332 attributes = wysihtml5.lang.object(setAttributes).clone();
6335 // check/convert values of attributes
6336 attributes = wysihtml5.lang.object(attributes).merge(_checkAttributes(oldNode, rule.check_attributes)).get();
6339 classes.push(setClass);
6343 for (attributeName in addClass) {
6344 method = addClassMethods[addClass[attributeName]];
6348 newClass = method(wysihtml5.dom.getAttribute(oldNode, attributeName));
6349 if (typeof(newClass) === "string") {
6350 classes.push(newClass);
6356 for (attributeName in addStyle) {
6357 method = addStyleMethods[addStyle[attributeName]];
6362 newStyle = method(wysihtml5.dom.getAttribute(oldNode, attributeName));
6363 if (typeof(newStyle) === "string") {
6364 styles.push(newStyle);
6370 if (typeof(allowedClasses) === "string" && allowedClasses === "any" && oldNode.getAttribute("class")) {
6371 if (currentRules.classes_blacklist) {
6372 oldClasses = oldNode.getAttribute("class");
6374 classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
6377 classesLength = classes.length;
6378 for (; i<classesLength; i++) {
6379 currentClass = classes[i];
6380 if (!currentRules.classes_blacklist[currentClass]) {
6381 newClasses.push(currentClass);
6385 if (newClasses.length) {
6386 attributes["class"] = wysihtml5.lang.array(newClasses).unique().join(" ");
6390 attributes["class"] = oldNode.getAttribute("class");
6393 // make sure that wysihtml5 temp class doesn't get stripped out
6394 if (!clearInternals) {
6395 allowedClasses["_wysihtml5-temp-placeholder"] = 1;
6396 allowedClasses["_rangySelectionBoundary"] = 1;
6397 allowedClasses["wysiwyg-tmp-selected-cell"] = 1;
6400 // add old classes last
6401 oldClasses = oldNode.getAttribute("class");
6403 classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
6405 classesLength = classes.length;
6406 for (; i<classesLength; i++) {
6407 currentClass = classes[i];
6408 if (allowedClasses[currentClass]) {
6409 newClasses.push(currentClass);
6413 if (newClasses.length) {
6414 attributes["class"] = wysihtml5.lang.array(newClasses).unique().join(" ");
6418 // remove table selection class if present
6419 if (attributes["class"] && clearInternals) {
6420 attributes["class"] = attributes["class"].replace("wysiwyg-tmp-selected-cell", "");
6421 if ((/^\s*$/g).test(attributes["class"])) {
6422 delete attributes["class"];
6426 if (styles.length) {
6427 attributes["style"] = wysihtml5.lang.array(styles).unique().join(" ");
6430 // set attributes on newNode
6431 for (attributeName in attributes) {
6432 // Setting attributes can cause a js error in IE under certain circumstances
6433 // eg. on a <img> under https when it's new attribute value is non-https
6434 // TODO: Investigate this further and check for smarter handling
6436 newNode.setAttribute(attributeName, attributes[attributeName]);
6440 // IE8 sometimes loses the width/height attributes when those are set before the "src"
6441 // so we make sure to set them again
6442 if (attributes.src) {
6443 if (typeof(attributes.width) !== "undefined") {
6444 newNode.setAttribute("width", attributes.width);
6446 if (typeof(attributes.height) !== "undefined") {
6447 newNode.setAttribute("height", attributes.height);
6452 var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g;
6453 function _handleText(oldNode) {
6454 var nextSibling = oldNode.nextSibling;
6455 if (nextSibling && nextSibling.nodeType === wysihtml5.TEXT_NODE) {
6456 // Concatenate text nodes
6457 nextSibling.data = oldNode.data.replace(INVISIBLE_SPACE_REG_EXP, "") + nextSibling.data.replace(INVISIBLE_SPACE_REG_EXP, "");
6459 // \uFEFF = wysihtml5.INVISIBLE_SPACE (used as a hack in certain rich text editing situations)
6460 var data = oldNode.data.replace(INVISIBLE_SPACE_REG_EXP, "");
6461 return oldNode.ownerDocument.createTextNode(data);
6465 function _handleComment(oldNode) {
6466 if (currentRules.comments) {
6467 return oldNode.ownerDocument.createComment(oldNode.nodeValue);
6471 // ------------ attribute checks ------------ \\
6472 var attributeCheckMethods = {
6474 var REG_EXP = /^https?:\/\//i;
6475 return function(attributeValue) {
6476 if (!attributeValue || !attributeValue.match(REG_EXP)) {
6479 return attributeValue.replace(REG_EXP, function(match) {
6480 return match.toLowerCase();
6486 var REG_EXP = /^(\/|https?:\/\/)/i;
6487 return function(attributeValue) {
6488 if (!attributeValue || !attributeValue.match(REG_EXP)) {
6491 return attributeValue.replace(REG_EXP, function(match) {
6492 return match.toLowerCase();
6498 var REG_EXP = /^(#|\/|https?:\/\/|mailto:)/i;
6499 return function(attributeValue) {
6500 if (!attributeValue || !attributeValue.match(REG_EXP)) {
6503 return attributeValue.replace(REG_EXP, function(match) {
6504 return match.toLowerCase();
6510 var REG_EXP = /[^ a-z0-9_\-]/gi;
6511 return function(attributeValue) {
6512 if (!attributeValue) {
6515 return attributeValue.replace(REG_EXP, "");
6519 numbers: (function() {
6520 var REG_EXP = /\D/g;
6521 return function(attributeValue) {
6522 attributeValue = (attributeValue || "").replace(REG_EXP, "");
6523 return attributeValue || null;
6528 return function(attributeValue) {
6529 return attributeValue;
6534 // ------------ style converter (converts an html attribute to a style) ------------ \\
6535 var addStyleMethods = {
6536 align_text: (function() {
6538 left: "text-align: left;",
6539 right: "text-align: right;",
6540 center: "text-align: center;"
6542 return function(attributeValue) {
6543 return mapping[String(attributeValue).toLowerCase()];
6548 // ------------ class converter (converts an html attribute to a class name) ------------ \\
6549 var addClassMethods = {
6550 align_img: (function() {
6552 left: "wysiwyg-float-left",
6553 right: "wysiwyg-float-right"
6555 return function(attributeValue) {
6556 return mapping[String(attributeValue).toLowerCase()];
6560 align_text: (function() {
6562 left: "wysiwyg-text-align-left",
6563 right: "wysiwyg-text-align-right",
6564 center: "wysiwyg-text-align-center",
6565 justify: "wysiwyg-text-align-justify"
6567 return function(attributeValue) {
6568 return mapping[String(attributeValue).toLowerCase()];
6572 clear_br: (function() {
6574 left: "wysiwyg-clear-left",
6575 right: "wysiwyg-clear-right",
6576 both: "wysiwyg-clear-both",
6577 all: "wysiwyg-clear-both"
6579 return function(attributeValue) {
6580 return mapping[String(attributeValue).toLowerCase()];
6584 size_font: (function() {
6586 "1": "wysiwyg-font-size-xx-small",
6587 "2": "wysiwyg-font-size-small",
6588 "3": "wysiwyg-font-size-medium",
6589 "4": "wysiwyg-font-size-large",
6590 "5": "wysiwyg-font-size-x-large",
6591 "6": "wysiwyg-font-size-xx-large",
6592 "7": "wysiwyg-font-size-xx-large",
6593 "-": "wysiwyg-font-size-smaller",
6594 "+": "wysiwyg-font-size-larger"
6596 return function(attributeValue) {
6597 return mapping[String(attributeValue).charAt(0)];
6602 // checks if element is possibly visible
6603 var typeCeckMethods = {
6604 has_visible_contet: (function() {
6607 visibleElements = ['img', 'video', 'picture', 'br', 'script', 'noscript',
6608 'style', 'table', 'iframe', 'object', 'embed', 'audio',
6609 'svg', 'input', 'button', 'select','textarea', 'canvas'];
6611 return function(el) {
6613 // has visible innertext. so is visible
6614 txt = (el.innerText || el.textContent).replace(/\s/g, '');
6615 if (txt && txt.length > 0) {
6619 // matches list of visible dimensioned elements
6620 for (var i = visibleElements.length; i--;) {
6621 if (el.querySelector(visibleElements[i])) {
6626 // try to measure dimesions in last resort. (can find only of elements in dom)
6627 if (el.offsetWidth && el.offsetWidth > 0 && el.offsetHeight && el.offsetHeight > 0) {
6636 var elementHandlingMethods = {
6637 unwrap: function (element) {
6638 wysihtml5.dom.unwrap(element);
6641 remove: function (element) {
6642 element.parentNode.removeChild(element);
6646 return parse(elementOrHtml_current, config_current);
6649 * Checks for empty text node childs and removes them
6651 * @param {Element} node The element in which to cleanup
6653 * wysihtml5.dom.removeEmptyTextNodes(element);
6655 wysihtml5.dom.removeEmptyTextNodes = function(node) {
6657 childNodes = wysihtml5.lang.array(node.childNodes).get(),
6658 childNodesLength = childNodes.length,
6660 for (; i<childNodesLength; i++) {
6661 childNode = childNodes[i];
6662 if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") {
6663 childNode.parentNode.removeChild(childNode);
6668 * Renames an element (eg. a <div> to a <p>) and keeps its childs
6670 * @param {Element} element The list element which should be renamed
6671 * @param {Element} newNodeName The desired tag name
6674 * <!-- Assume the following dom: -->
6682 * wysihtml5.dom.renameElement(document.getElementById("list"), "ol");
6685 * <!-- Will result in: -->
6692 wysihtml5.dom.renameElement = function(element, newNodeName) {
6693 var newElement = element.ownerDocument.createElement(newNodeName),
6695 while (firstChild = element.firstChild) {
6696 newElement.appendChild(firstChild);
6698 wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement);
6699 element.parentNode.replaceChild(newElement, element);
6703 * Takes an element, removes it and replaces it with it's childs
6705 * @param {Object} node The node which to replace with it's child nodes
6708 * <span>hello</span>
6711 * // Remove #foo and replace with it's children
6712 * wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo"));
6715 wysihtml5.dom.replaceWithChildNodes = function(node) {
6716 if (!node.parentNode) {
6720 if (!node.firstChild) {
6721 node.parentNode.removeChild(node);
6725 var fragment = node.ownerDocument.createDocumentFragment();
6726 while (node.firstChild) {
6727 fragment.appendChild(node.firstChild);
6729 node.parentNode.replaceChild(fragment, node);
6730 node = fragment = null;
6733 * Unwraps an unordered/ordered list
6735 * @param {Element} element The list element which should be unwrapped
6738 * <!-- Assume the following dom: -->
6746 * wysihtml5.dom.resolveList(document.getElementById("list"));
6749 * <!-- Will result in: -->
6755 function _isBlockElement(node) {
6756 return dom.getStyle("display").from(node) === "block";
6759 function _isLineBreak(node) {
6760 return node.nodeName === "BR";
6763 function _appendLineBreak(element) {
6764 var lineBreak = element.ownerDocument.createElement("br");
6765 element.appendChild(lineBreak);
6768 function resolveList(list, useLineBreaks) {
6769 if (!list.nodeName.match(/^(MENU|UL|OL)$/)) {
6773 var doc = list.ownerDocument,
6774 fragment = doc.createDocumentFragment(),
6775 previousSibling = wysihtml5.dom.domNode(list).prev({ignoreBlankTexts: true}),
6779 shouldAppendLineBreak,
6783 if (useLineBreaks) {
6784 // Insert line break if list is after a non-block element
6785 if (previousSibling && !_isBlockElement(previousSibling) && !_isLineBreak(previousSibling)) {
6786 _appendLineBreak(fragment);
6789 while (listItem = (list.firstElementChild || list.firstChild)) {
6790 lastChild = listItem.lastChild;
6791 while (firstChild = listItem.firstChild) {
6792 isLastChild = firstChild === lastChild;
6793 // This needs to be done before appending it to the fragment, as it otherwise will lose style information
6794 shouldAppendLineBreak = isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);
6795 fragment.appendChild(firstChild);
6796 if (shouldAppendLineBreak) {
6797 _appendLineBreak(fragment);
6801 listItem.parentNode.removeChild(listItem);
6804 while (listItem = (list.firstElementChild || list.firstChild)) {
6805 if (listItem.querySelector && listItem.querySelector("div, p, ul, ol, menu, blockquote, h1, h2, h3, h4, h5, h6")) {
6806 while (firstChild = listItem.firstChild) {
6807 fragment.appendChild(firstChild);
6810 paragraph = doc.createElement("p");
6811 while (firstChild = listItem.firstChild) {
6812 paragraph.appendChild(firstChild);
6814 fragment.appendChild(paragraph);
6816 listItem.parentNode.removeChild(listItem);
6820 list.parentNode.replaceChild(fragment, list);
6823 dom.resolveList = resolveList;
6826 * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
6828 * Browser Compatibility:
6829 * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
6830 * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
6832 * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
6833 * - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
6834 * - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
6835 * - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
6836 * - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
6837 * can do anything as if the sandbox attribute wasn't set
6839 * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
6840 * @param {Object} [config] Optional parameters
6843 * new wysihtml5.dom.Sandbox(function(sandbox) {
6844 * sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
6847 (function(wysihtml5) {
6849 * Default configuration
6853 * Properties to unset/protect on the window object
6855 windowProperties = [
6856 "parent", "top", "opener", "frameElement", "frames",
6857 "localStorage", "globalStorage", "sessionStorage", "indexedDB"
6860 * Properties on the window object which are set to an empty function
6862 windowProperties2 = [
6863 "open", "close", "openDialog", "showModalDialog",
6864 "alert", "confirm", "prompt",
6865 "openDatabase", "postMessage",
6866 "XMLHttpRequest", "XDomainRequest"
6869 * Properties to unset/protect on the document object
6871 documentProperties = [
6873 "write", "open", "close"
6876 wysihtml5.dom.Sandbox = Base.extend(
6877 /** @scope wysihtml5.dom.Sandbox.prototype */ {
6879 constructor: function(readyCallback, config) {
6880 this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;
6881 this.config = wysihtml5.lang.object({}).merge(config).get();
6882 this.editableArea = this._createIframe();
6885 insertInto: function(element) {
6886 if (typeof(element) === "string") {
6887 element = doc.getElementById(element);
6890 element.appendChild(this.editableArea);
6893 getIframe: function() {
6894 return this.editableArea;
6897 getWindow: function() {
6901 getDocument: function() {
6905 destroy: function() {
6906 var iframe = this.getIframe();
6907 iframe.parentNode.removeChild(iframe);
6910 _readyError: function() {
6911 throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");
6915 * Creates the sandbox iframe
6917 * Some important notes:
6918 * - We can't use HTML5 sandbox for now:
6919 * setting it causes that the iframe's dom can't be accessed from the outside
6920 * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
6921 * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
6922 * In order to make this happen we need to set the "allow-scripts" flag.
6923 * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
6924 * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
6925 * - IE needs to have the security="restricted" attribute set before the iframe is
6926 * inserted into the dom tree
6927 * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
6928 * though it supports it
6929 * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
6930 * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
6931 * on the onreadystatechange event
6933 _createIframe: function() {
6935 iframe = doc.createElement("iframe");
6936 iframe.className = "wysihtml5-sandbox";
6937 wysihtml5.dom.setAttributes({
6938 "security": "restricted",
6939 "allowtransparency": "true",
6947 // Setting the src like this prevents ssl warnings in IE6
6948 if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
6949 iframe.src = "javascript:'<html></html>'";
6952 iframe.onload = function() {
6953 iframe.onreadystatechange = iframe.onload = null;
6954 that._onLoadIframe(iframe);
6957 iframe.onreadystatechange = function() {
6958 if (/loaded|complete/.test(iframe.readyState)) {
6959 iframe.onreadystatechange = iframe.onload = null;
6960 that._onLoadIframe(iframe);
6968 * Callback for when the iframe has finished loading
6970 _onLoadIframe: function(iframe) {
6971 // don't resume when the iframe got unloaded (eg. by removing it from the dom)
6972 if (!wysihtml5.dom.contains(doc.documentElement, iframe)) {
6977 iframeWindow = iframe.contentWindow,
6978 iframeDocument = iframe.contentWindow.document,
6979 charset = doc.characterSet || doc.charset || "utf-8",
6980 sandboxHtml = this._getHtml({
6982 stylesheets: this.config.stylesheets
6985 // Create the basic dom tree including proper DOCTYPE and charset
6986 iframeDocument.open("text/html", "replace");
6987 iframeDocument.write(sandboxHtml);
6988 iframeDocument.close();
6990 this.getWindow = function() { return iframe.contentWindow; };
6991 this.getDocument = function() { return iframe.contentWindow.document; };
6993 // Catch js errors and pass them to the parent's onerror event
6994 // addEventListener("error") doesn't work properly in some browsers
6995 // TODO: apparently this doesn't work in IE9!
6996 iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
6997 throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber);
7000 if (!wysihtml5.browser.supportsSandboxedIframes()) {
7001 // Unset a bunch of sensitive variables
7002 // Please note: This isn't hack safe!
7003 // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
7004 // IE is secure though, which is the most important thing, since IE is the only browser, who
7005 // takes over scripts & styles into contentEditable elements when copied from external websites
7006 // or applications (Microsoft Word, ...)
7008 for (i=0, length=windowProperties.length; i<length; i++) {
7009 this._unset(iframeWindow, windowProperties[i]);
7011 for (i=0, length=windowProperties2.length; i<length; i++) {
7012 this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION);
7014 for (i=0, length=documentProperties.length; i<length; i++) {
7015 this._unset(iframeDocument, documentProperties[i]);
7017 // This doesn't work in Safari 5
7018 // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
7019 this._unset(iframeDocument, "cookie", "", true);
7024 // Trigger the callback
7025 setTimeout(function() { that.callback(that); }, 0);
7028 _getHtml: function(templateVars) {
7029 var stylesheets = templateVars.stylesheets,
7033 stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
7035 length = stylesheets.length;
7036 for (; i<length; i++) {
7037 html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
7040 templateVars.stylesheets = html;
7042 return wysihtml5.lang.string(
7043 '<!DOCTYPE html><html><head>'
7044 + '<meta charset="#{charset}">#{stylesheets}</head>'
7045 + '<body></body></html>'
7046 ).interpolate(templateVars);
7050 * Method to unset/override existing variables
7052 * // Make cookie unreadable and unwritable
7053 * this._unset(document, "cookie", "", true);
7055 _unset: function(object, property, value, setter) {
7056 try { object[property] = value; } catch(e) {}
7058 try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
7060 try { object.__defineSetter__(property, function() {}); } catch(e) {}
7063 if (!wysihtml5.browser.crashesWhenDefineProperty(property)) {
7066 get: function() { return value; }
7069 config.set = function() {};
7071 Object.defineProperty(object, property, config);
7077 ;(function(wysihtml5) {
7079 wysihtml5.dom.ContentEditableArea = Base.extend({
7080 getContentEditable: function() {
7081 return this.element;
7084 getWindow: function() {
7085 return this.element.ownerDocument.defaultView;
7088 getDocument: function() {
7089 return this.element.ownerDocument;
7092 constructor: function(readyCallback, config, contentEditable) {
7093 this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION;
7094 this.config = wysihtml5.lang.object({}).merge(config).get();
7095 if (contentEditable) {
7096 this.element = this._bindElement(contentEditable);
7098 this.element = this._createElement();
7102 // creates a new contenteditable and initiates it
7103 _createElement: function() {
7104 var element = doc.createElement("div");
7105 element.className = "wysihtml5-sandbox";
7106 this._loadElement(element);
7110 // initiates an allready existent contenteditable
7111 _bindElement: function(contentEditable) {
7112 contentEditable.className = (contentEditable.className && contentEditable.className != '') ? contentEditable.className + " wysihtml5-sandbox" : "wysihtml5-sandbox";
7113 this._loadElement(contentEditable, true);
7114 return contentEditable;
7117 _loadElement: function(element, contentExists) {
7119 if (!contentExists) {
7120 var sandboxHtml = this._getHtml();
7121 element.innerHTML = sandboxHtml;
7124 this.getWindow = function() { return element.ownerDocument.defaultView; };
7125 this.getDocument = function() { return element.ownerDocument; };
7127 // Catch js errors and pass them to the parent's onerror event
7128 // addEventListener("error") doesn't work properly in some browsers
7129 // TODO: apparently this doesn't work in IE9!
7130 // TODO: figure out and bind the errors logic for contenteditble mode
7131 /*iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
7132 throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber);
7136 // Trigger the callback
7137 setTimeout(function() { that.callback(that); }, 0);
7140 _getHtml: function(templateVars) {
7148 "className": "class"
7150 wysihtml5.dom.setAttributes = function(attributes) {
7152 on: function(element) {
7153 for (var i in attributes) {
7154 element.setAttribute(mapping[i] || i, attributes[i]);
7160 ;wysihtml5.dom.setStyles = function(styles) {
7162 on: function(element) {
7163 var style = element.style;
7164 if (typeof(styles) === "string") {
7165 style.cssText += ";" + styles;
7168 for (var i in styles) {
7169 if (i === "float") {
7170 style.cssFloat = styles[i];
7171 style.styleFloat = styles[i];
7173 style[i] = styles[i];
7180 * Simulate HTML5 placeholder attribute
7183 * - div[contentEditable] elements don't support it
7184 * - older browsers (such as IE8 and Firefox 3.6) don't support it at all
7186 * @param {Object} parent Instance of main wysihtml5.Editor class
7187 * @param {Element} view Instance of wysihtml5.views.* class
7188 * @param {String} placeholderText
7191 * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
7194 dom.simulatePlaceholder = function(editor, view, placeholderText) {
7195 var CLASS_NAME = "placeholder",
7196 unset = function() {
7197 var composerIsVisible = view.element.offsetWidth > 0 && view.element.offsetHeight > 0;
7198 if (view.hasPlaceholderSet()) {
7200 view.element.focus();
7201 if (composerIsVisible ) {
7202 setTimeout(function() {
7203 var sel = view.selection.getSelection();
7204 if (!sel.focusNode || !sel.anchorNode) {
7205 view.selection.selectNode(view.element.firstChild || view.element);
7210 view.placeholderSet = false;
7211 dom.removeClass(view.element, CLASS_NAME);
7214 if (view.isEmpty()) {
7215 view.placeholderSet = true;
7216 view.setValue(placeholderText);
7217 dom.addClass(view.element, CLASS_NAME);
7222 .on("set_placeholder", set)
7223 .on("unset_placeholder", unset)
7224 .on("focus:composer", unset)
7225 .on("paste:composer", unset)
7226 .on("blur:composer", set);
7232 var documentElement = document.documentElement;
7233 if ("textContent" in documentElement) {
7234 dom.setTextContent = function(element, text) {
7235 element.textContent = text;
7238 dom.getTextContent = function(element) {
7239 return element.textContent;
7241 } else if ("innerText" in documentElement) {
7242 dom.setTextContent = function(element, text) {
7243 element.innerText = text;
7246 dom.getTextContent = function(element) {
7247 return element.innerText;
7250 dom.setTextContent = function(element, text) {
7251 element.nodeValue = text;
7254 dom.getTextContent = function(element) {
7255 return element.nodeValue;
7261 * Get a set of attribute from one element
7263 * IE gives wrong results for hasAttribute/getAttribute, for example:
7264 * var td = document.createElement("td");
7265 * td.getAttribute("rowspan"); // => "1" in IE
7267 * Therefore we have to check the element's outerHTML for the attribute
7270 wysihtml5.dom.getAttribute = function(node, attributeName) {
7271 var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly();
7272 attributeName = attributeName.toLowerCase();
7273 var nodeName = node.nodeName;
7274 if (nodeName == "IMG" && attributeName == "src" && wysihtml5.dom.isLoadedImage(node) === true) {
7275 // Get 'src' attribute value via object property since this will always contain the
7276 // full absolute url (http://...)
7277 // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host
7278 // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)
7280 } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
7281 // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML
7282 var outerHTML = node.outerHTML.toLowerCase(),
7283 // TODO: This might not work for attributes without value: <input disabled>
7284 hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1;
7286 return hasAttribute ? node.getAttribute(attributeName) : null;
7288 return node.getAttribute(attributeName);
7292 * Get all attributes of an element
7294 * IE gives wrong results for hasAttribute/getAttribute, for example:
7295 * var td = document.createElement("td");
7296 * td.getAttribute("rowspan"); // => "1" in IE
7298 * Therefore we have to check the element's outerHTML for the attribute
7301 wysihtml5.dom.getAttributes = function(node) {
7302 var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(),
7303 nodeName = node.nodeName,
7307 for (attr in node.attributes) {
7308 if ((node.attributes.hasOwnProperty && node.attributes.hasOwnProperty(attr)) || (!node.attributes.hasOwnProperty && Object.prototype.hasOwnProperty.call(node.attributes, attr))) {
7309 if (node.attributes[attr].specified) {
7310 if (nodeName == "IMG" && node.attributes[attr].name.toLowerCase() == "src" && wysihtml5.dom.isLoadedImage(node) === true) {
7311 attributes['src'] = node.src;
7312 } else if (wysihtml5.lang.array(['rowspan', 'colspan']).contains(node.attributes[attr].name.toLowerCase()) && HAS_GET_ATTRIBUTE_BUG) {
7313 if (node.attributes[attr].value !== 1) {
7314 attributes[node.attributes[attr].name] = node.attributes[attr].value;
7317 attributes[node.attributes[attr].name] = node.attributes[attr].value;
7324 * Check whether the given node is a proper loaded image
7325 * FIXME: Returns undefined when unknown (Chrome, Safari)
7328 wysihtml5.dom.isLoadedImage = function (node) {
7330 return node.complete && !node.mozMatchesSelector(":-moz-broken");
7332 if (node.complete && node.readyState === "complete") {
7337 ;(function(wysihtml5) {
7339 var api = wysihtml5.dom;
7341 var MapCell = function(cell) {
7343 this.isColspan= false;
7344 this.isRowspan= false;
7345 this.firstCol= true;
7347 this.firstRow= true;
7350 this.spanCollection= [];
7351 this.modified = false;
7354 var TableModifyerByCell = function (cell, table) {
7357 this.table = api.getParentElement(cell, { nodeName: ["TABLE"] });
7360 this.cell = this.table.querySelectorAll('th, td')[0];
7364 function queryInList(list, query) {
7367 for (var e = 0, len = list.length; e < len; e++) {
7368 q = list[e].querySelectorAll(query);
7370 for(var i = q.length; i--; ret.unshift(q[i]));
7376 function removeElement(el) {
7377 el.parentNode.removeChild(el);
7380 function insertAfter(referenceNode, newNode) {
7381 referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
7384 function nextNode(node, tag) {
7385 var element = node.nextSibling;
7386 while (element.nodeType !=1) {
7387 element = element.nextSibling;
7388 if (!tag || tag == element.tagName.toLowerCase()) {
7395 TableModifyerByCell.prototype = {
7397 addSpannedCellToMap: function(cell, map, r, c, cspan, rspan) {
7398 var spanCollect = [],
7399 rmax = r + ((rspan) ? parseInt(rspan, 10) - 1 : 0),
7400 cmax = c + ((cspan) ? parseInt(cspan, 10) - 1 : 0);
7402 for (var rr = r; rr <= rmax; rr++) {
7403 if (typeof map[rr] == "undefined") { map[rr] = []; }
7404 for (var cc = c; cc <= cmax; cc++) {
7405 map[rr][cc] = new MapCell(cell);
7406 map[rr][cc].isColspan = (cspan && parseInt(cspan, 10) > 1);
7407 map[rr][cc].isRowspan = (rspan && parseInt(rspan, 10) > 1);
7408 map[rr][cc].firstCol = cc == c;
7409 map[rr][cc].lastCol = cc == cmax;
7410 map[rr][cc].firstRow = rr == r;
7411 map[rr][cc].lastRow = rr == rmax;
7412 map[rr][cc].isReal = cc == c && rr == r;
7413 map[rr][cc].spanCollection = spanCollect;
7415 spanCollect.push(map[rr][cc]);
7420 setCellAsModified: function(cell) {
7421 cell.modified = true;
7422 if (cell.spanCollection.length > 0) {
7423 for (var s = 0, smax = cell.spanCollection.length; s < smax; s++) {
7424 cell.spanCollection[s].modified = true;
7429 setTableMap: function() {
7431 var tableRows = this.getTableRows(),
7432 ridx, row, cells, cidx, cell,
7436 for (ridx = 0; ridx < tableRows.length; ridx++) {
7437 row = tableRows[ridx];
7438 cells = this.getRowCells(row);
7440 if (typeof map[ridx] == "undefined") { map[ridx] = []; }
7441 for (cidx = 0; cidx < cells.length; cidx++) {
7444 // If cell allready set means it is set by col or rowspan,
7445 // so increase cols index until free col is found
7446 while (typeof map[ridx][c] != "undefined") { c++; }
7448 cspan = api.getAttribute(cell, 'colspan');
7449 rspan = api.getAttribute(cell, 'rowspan');
7451 if (cspan || rspan) {
7452 this.addSpannedCellToMap(cell, map, ridx, c, cspan, rspan);
7453 c = c + ((cspan) ? parseInt(cspan, 10) : 1);
7455 map[ridx][c] = new MapCell(cell);
7464 getRowCells: function(row) {
7465 var inlineTables = this.table.querySelectorAll('table'),
7466 inlineCells = (inlineTables) ? queryInList(inlineTables, 'th, td') : [],
7467 allCells = row.querySelectorAll('th, td'),
7468 tableCells = (inlineCells.length > 0) ? wysihtml5.lang.array(allCells).without(inlineCells) : allCells;
7473 getTableRows: function() {
7474 var inlineTables = this.table.querySelectorAll('table'),
7475 inlineRows = (inlineTables) ? queryInList(inlineTables, 'tr') : [],
7476 allRows = this.table.querySelectorAll('tr'),
7477 tableRows = (inlineRows.length > 0) ? wysihtml5.lang.array(allRows).without(inlineRows) : allRows;
7482 getMapIndex: function(cell) {
7483 var r_length = this.map.length,
7484 c_length = (this.map && this.map[0]) ? this.map[0].length : 0;
7486 for (var r_idx = 0;r_idx < r_length; r_idx++) {
7487 for (var c_idx = 0;c_idx < c_length; c_idx++) {
7488 if (this.map[r_idx][c_idx].el === cell) {
7489 return {'row': r_idx, 'col': c_idx};
7496 getElementAtIndex: function(idx) {
7498 if (this.map[idx.row] && this.map[idx.row][idx.col] && this.map[idx.row][idx.col].el) {
7499 return this.map[idx.row][idx.col].el;
7504 getMapElsTo: function(to_cell) {
7507 this.idx_start = this.getMapIndex(this.cell);
7508 this.idx_end = this.getMapIndex(to_cell);
7510 // switch indexes if start is bigger than end
7511 if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) {
7512 var temp_idx = this.idx_start;
7513 this.idx_start = this.idx_end;
7514 this.idx_end = temp_idx;
7516 if (this.idx_start.col > this.idx_end.col) {
7517 var temp_cidx = this.idx_start.col;
7518 this.idx_start.col = this.idx_end.col;
7519 this.idx_end.col = temp_cidx;
7522 if (this.idx_start != null && this.idx_end != null) {
7523 for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) {
7524 for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) {
7525 els.push(this.map[row][col].el);
7532 orderSelectionEnds: function(secondcell) {
7534 this.idx_start = this.getMapIndex(this.cell);
7535 this.idx_end = this.getMapIndex(secondcell);
7537 // switch indexes if start is bigger than end
7538 if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) {
7539 var temp_idx = this.idx_start;
7540 this.idx_start = this.idx_end;
7541 this.idx_end = temp_idx;
7543 if (this.idx_start.col > this.idx_end.col) {
7544 var temp_cidx = this.idx_start.col;
7545 this.idx_start.col = this.idx_end.col;
7546 this.idx_end.col = temp_cidx;
7550 "start": this.map[this.idx_start.row][this.idx_start.col].el,
7551 "end": this.map[this.idx_end.row][this.idx_end.col].el
7555 createCells: function(tag, nr, attrs) {
7556 var doc = this.table.ownerDocument,
7557 frag = doc.createDocumentFragment(),
7559 for (var i = 0; i < nr; i++) {
7560 cell = doc.createElement(tag);
7563 for (var attr in attrs) {
7564 if (attrs.hasOwnProperty(attr)) {
7565 cell.setAttribute(attr, attrs[attr]);
7570 // add non breaking space
7571 cell.appendChild(document.createTextNode("\u00a0"));
7573 frag.appendChild(cell);
7578 // Returns next real cell (not part of spanned cell unless first) on row if selected index is not real. I no real cells -1 will be returned
7579 correctColIndexForUnreals: function(col, row) {
7580 var r = this.map[row],
7582 for (var i = 0, max = col; i < col; i++) {
7590 getLastNewCellOnRow: function(row, rowLimit) {
7591 var cells = this.getRowCells(row),
7594 for (var cidx = 0, cmax = cells.length; cidx < cmax; cidx++) {
7596 idx = this.getMapIndex(cell);
7597 if (idx === false || (typeof rowLimit != "undefined" && idx.row != rowLimit)) {
7604 removeEmptyTable: function() {
7605 var cells = this.table.querySelectorAll('td, th');
7606 if (!cells || cells.length == 0) {
7607 removeElement(this.table);
7614 // Splits merged cell on row to unique cells
7615 splitRowToCells: function(cell) {
7616 if (cell.isColspan) {
7617 var colspan = parseInt(api.getAttribute(cell.el, 'colspan') || 1, 10),
7618 cType = cell.el.tagName.toLowerCase();
7620 var newCells = this.createCells(cType, colspan -1);
7621 insertAfter(cell.el, newCells);
7623 cell.el.removeAttribute('colspan');
7627 getRealRowEl: function(force, idx) {
7631 idx = idx || this.idx;
7633 for (var cidx = 0, cmax = this.map[idx.row].length; cidx < cmax; cidx++) {
7634 c = this.map[idx.row][cidx];
7636 r = api.getParentElement(c.el, { nodeName: ["TR"] });
7643 if (r === null && force) {
7644 r = api.getParentElement(this.map[idx.row][idx.col].el, { nodeName: ["TR"] }) || null;
7650 injectRowAt: function(row, col, colspan, cType, c) {
7651 var r = this.getRealRowEl(false, {'row': row, 'col': col}),
7652 new_cells = this.createCells(cType, colspan);
7655 var n_cidx = this.correctColIndexForUnreals(col, row);
7657 insertAfter(this.getRowCells(r)[n_cidx], new_cells);
7659 r.insertBefore(new_cells, r.firstChild);
7662 var rr = this.table.ownerDocument.createElement('tr');
7663 rr.appendChild(new_cells);
7664 insertAfter(api.getParentElement(c.el, { nodeName: ["TR"] }), rr);
7668 canMerge: function(to) {
7671 this.idx_start = this.getMapIndex(this.cell);
7672 this.idx_end = this.getMapIndex(this.to);
7674 // switch indexes if start is bigger than end
7675 if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) {
7676 var temp_idx = this.idx_start;
7677 this.idx_start = this.idx_end;
7678 this.idx_end = temp_idx;
7680 if (this.idx_start.col > this.idx_end.col) {
7681 var temp_cidx = this.idx_start.col;
7682 this.idx_start.col = this.idx_end.col;
7683 this.idx_end.col = temp_cidx;
7686 for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) {
7687 for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) {
7688 if (this.map[row][col].isColspan || this.map[row][col].isRowspan) {
7696 decreaseCellSpan: function(cell, span) {
7697 var nr = parseInt(api.getAttribute(cell.el, span), 10) - 1;
7699 cell.el.setAttribute(span, nr);
7701 cell.el.removeAttribute(span);
7702 if (span == 'colspan') {
7703 cell.isColspan = false;
7705 if (span == 'rowspan') {
7706 cell.isRowspan = false;
7708 cell.firstCol = true;
7709 cell.lastCol = true;
7710 cell.firstRow = true;
7711 cell.lastRow = true;
7716 removeSurplusLines: function() {
7717 var row, cell, ridx, rmax, cidx, cmax, allRowspan;
7722 rmax = this.map.length;
7723 for (;ridx < rmax; ridx++) {
7724 row = this.map[ridx];
7728 for (; cidx < cmax; cidx++) {
7730 if (!(api.getAttribute(cell.el, "rowspan") && parseInt(api.getAttribute(cell.el, "rowspan"), 10) > 1 && cell.firstRow !== true)) {
7737 for (; cidx < cmax; cidx++) {
7738 this.decreaseCellSpan(row[cidx], 'rowspan');
7743 // remove rows without cells
7744 var tableRows = this.getTableRows();
7746 rmax = tableRows.length;
7747 for (;ridx < rmax; ridx++) {
7748 row = tableRows[ridx];
7749 if (row.childNodes.length == 0 && (/^\s*$/.test(row.textContent || row.innerText))) {
7756 fillMissingCells: function() {
7764 // find maximal dimensions of broken table
7765 r_max = this.map.length;
7766 for (var ridx = 0; ridx < r_max; ridx++) {
7767 if (this.map[ridx].length > c_max) { c_max = this.map[ridx].length; }
7770 for (var row = 0; row < r_max; row++) {
7771 for (var col = 0; col < c_max; col++) {
7772 if (this.map[row] && !this.map[row][col]) {
7774 this.map[row][col] = new MapCell(this.createCells('td', 1));
7775 prevcell = this.map[row][col-1];
7776 if (prevcell && prevcell.el && prevcell.el.parent) { // if parent does not exist element is removed from dom
7777 insertAfter(this.map[row][col-1].el, this.map[row][col].el);
7786 rectify: function() {
7787 if (!this.removeEmptyTable()) {
7788 this.removeSurplusLines();
7789 this.fillMissingCells();
7796 unmerge: function() {
7797 if (this.rectify()) {
7799 this.idx = this.getMapIndex(this.cell);
7802 var thisCell = this.map[this.idx.row][this.idx.col],
7803 colspan = (api.getAttribute(thisCell.el, "colspan")) ? parseInt(api.getAttribute(thisCell.el, "colspan"), 10) : 1,
7804 cType = thisCell.el.tagName.toLowerCase();
7806 if (thisCell.isRowspan) {
7807 var rowspan = parseInt(api.getAttribute(thisCell.el, "rowspan"), 10);
7809 for (var nr = 1, maxr = rowspan - 1; nr <= maxr; nr++){
7810 this.injectRowAt(this.idx.row + nr, this.idx.col, colspan, cType, thisCell);
7813 thisCell.el.removeAttribute('rowspan');
7815 this.splitRowToCells(thisCell);
7820 // merges cells from start cell (defined in creating obj) to "to" cell
7821 merge: function(to) {
7822 if (this.rectify()) {
7823 if (this.canMerge(to)) {
7824 var rowspan = this.idx_end.row - this.idx_start.row + 1,
7825 colspan = this.idx_end.col - this.idx_start.col + 1;
7827 for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) {
7828 for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) {
7830 if (row == this.idx_start.row && col == this.idx_start.col) {
7832 this.map[row][col].el.setAttribute('rowspan', rowspan);
7835 this.map[row][col].el.setAttribute('colspan', colspan);
7839 if (!(/^\s*<br\/?>\s*$/.test(this.map[row][col].el.innerHTML.toLowerCase()))) {
7840 this.map[this.idx_start.row][this.idx_start.col].el.innerHTML += ' ' + this.map[row][col].el.innerHTML;
7842 removeElement(this.map[row][col].el);
7848 if (window.console) {
7849 console.log('Do not know how to merge allready merged cells.');
7855 // Decreases rowspan of a cell if it is done on first cell of rowspan row (real cell)
7856 // Cell is moved to next row (if it is real)
7857 collapseCellToNextRow: function(cell) {
7858 var cellIdx = this.getMapIndex(cell.el),
7859 newRowIdx = cellIdx.row + 1,
7860 newIdx = {'row': newRowIdx, 'col': cellIdx.col};
7862 if (newRowIdx < this.map.length) {
7864 var row = this.getRealRowEl(false, newIdx);
7866 var n_cidx = this.correctColIndexForUnreals(newIdx.col, newIdx.row);
7868 insertAfter(this.getRowCells(row)[n_cidx], cell.el);
7870 var lastCell = this.getLastNewCellOnRow(row, newRowIdx);
7871 if (lastCell !== null) {
7872 insertAfter(lastCell, cell.el);
7874 row.insertBefore(cell.el, row.firstChild);
7877 if (parseInt(api.getAttribute(cell.el, 'rowspan'), 10) > 2) {
7878 cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) - 1);
7880 cell.el.removeAttribute('rowspan');
7886 // Removes a cell when removing a row
7887 // If is rowspan cell then decreases the rowspan
7888 // and moves cell to next row if needed (is first cell of rowspan)
7889 removeRowCell: function(cell) {
7891 if (cell.isRowspan) {
7892 this.collapseCellToNextRow(cell);
7894 removeElement(cell.el);
7897 if (parseInt(api.getAttribute(cell.el, 'rowspan'), 10) > 2) {
7898 cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) - 1);
7900 cell.el.removeAttribute('rowspan');
7905 getRowElementsByCell: function() {
7908 this.idx = this.getMapIndex(this.cell);
7909 if (this.idx !== false) {
7910 var modRow = this.map[this.idx.row];
7911 for (var cidx = 0, cmax = modRow.length; cidx < cmax; cidx++) {
7912 if (modRow[cidx].isReal) {
7913 cells.push(modRow[cidx].el);
7920 getColumnElementsByCell: function() {
7923 this.idx = this.getMapIndex(this.cell);
7924 if (this.idx !== false) {
7925 for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++) {
7926 if (this.map[ridx][this.idx.col] && this.map[ridx][this.idx.col].isReal) {
7927 cells.push(this.map[ridx][this.idx.col].el);
7934 // Removes the row of selected cell
7935 removeRow: function() {
7936 var oldRow = api.getParentElement(this.cell, { nodeName: ["TR"] });
7939 this.idx = this.getMapIndex(this.cell);
7940 if (this.idx !== false) {
7941 var modRow = this.map[this.idx.row];
7942 for (var cidx = 0, cmax = modRow.length; cidx < cmax; cidx++) {
7943 if (!modRow[cidx].modified) {
7944 this.setCellAsModified(modRow[cidx]);
7945 this.removeRowCell(modRow[cidx]);
7949 removeElement(oldRow);
7953 removeColCell: function(cell) {
7954 if (cell.isColspan) {
7955 if (parseInt(api.getAttribute(cell.el, 'colspan'), 10) > 2) {
7956 cell.el.setAttribute('colspan', parseInt(api.getAttribute(cell.el, 'colspan'), 10) - 1);
7958 cell.el.removeAttribute('colspan');
7960 } else if (cell.isReal) {
7961 removeElement(cell.el);
7965 removeColumn: function() {
7967 this.idx = this.getMapIndex(this.cell);
7968 if (this.idx !== false) {
7969 for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++) {
7970 if (!this.map[ridx][this.idx.col].modified) {
7971 this.setCellAsModified(this.map[ridx][this.idx.col]);
7972 this.removeColCell(this.map[ridx][this.idx.col]);
7978 // removes row or column by selected cell element
7979 remove: function(what) {
7980 if (this.rectify()) {
7986 this.removeColumn();
7993 addRow: function(where) {
7994 var doc = this.table.ownerDocument;
7997 this.idx = this.getMapIndex(this.cell);
7998 if (where == "below" && api.getAttribute(this.cell, 'rowspan')) {
7999 this.idx.row = this.idx.row + parseInt(api.getAttribute(this.cell, 'rowspan'), 10) - 1;
8002 if (this.idx !== false) {
8003 var modRow = this.map[this.idx.row],
8004 newRow = doc.createElement('tr');
8006 for (var ridx = 0, rmax = modRow.length; ridx < rmax; ridx++) {
8007 if (!modRow[ridx].modified) {
8008 this.setCellAsModified(modRow[ridx]);
8009 this.addRowCell(modRow[ridx], newRow, where);
8015 insertAfter(this.getRealRowEl(true), newRow);
8018 var cr = api.getParentElement(this.map[this.idx.row][this.idx.col].el, { nodeName: ["TR"] });
8020 cr.parentNode.insertBefore(newRow, cr);
8027 addRowCell: function(cell, row, where) {
8028 var colSpanAttr = (cell.isColspan) ? {"colspan" : api.getAttribute(cell.el, 'colspan')} : null;
8030 if (where != 'above' && cell.isRowspan) {
8031 cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el,'rowspan'), 10) + 1);
8033 row.appendChild(this.createCells('td', 1, colSpanAttr));
8036 if (where != 'above' && cell.isRowspan && cell.lastRow) {
8037 row.appendChild(this.createCells('td', 1, colSpanAttr));
8038 } else if (c.isRowspan) {
8039 cell.el.attr('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) + 1);
8044 add: function(where) {
8045 if (this.rectify()) {
8046 if (where == 'below' || where == 'above') {
8049 if (where == 'before' || where == 'after') {
8050 this.addColumn(where);
8055 addColCell: function (cell, ridx, where) {
8057 cType = cell.el.tagName.toLowerCase();
8059 // defines add cell vs expand cell conditions
8063 doAdd = (!cell.isColspan || cell.firstCol);
8066 doAdd = (!cell.isColspan || cell.lastCol || (cell.isColspan && c.el == this.cell));
8071 // adds a cell before or after current cell element
8074 cell.el.parentNode.insertBefore(this.createCells(cType, 1), cell.el);
8077 insertAfter(cell.el, this.createCells(cType, 1));
8081 // handles if cell has rowspan
8082 if (cell.isRowspan) {
8083 this.handleCellAddWithRowspan(cell, ridx+1, where);
8088 cell.el.setAttribute('colspan', parseInt(api.getAttribute(cell.el, 'colspan'), 10) + 1);
8092 addColumn: function(where) {
8096 this.idx = this.getMapIndex(this.cell);
8097 if (where == "after" && api.getAttribute(this.cell, 'colspan')) {
8098 this.idx.col = this.idx.col + parseInt(api.getAttribute(this.cell, 'colspan'), 10) - 1;
8101 if (this.idx !== false) {
8102 for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++ ) {
8103 row = this.map[ridx];
8104 if (row[this.idx.col]) {
8105 modCell = row[this.idx.col];
8106 if (!modCell.modified) {
8107 this.setCellAsModified(modCell);
8108 this.addColCell(modCell, ridx , where);
8115 handleCellAddWithRowspan: function (cell, ridx, where) {
8116 var addRowsNr = parseInt(api.getAttribute(this.cell, 'rowspan'), 10) - 1,
8117 crow = api.getParentElement(cell.el, { nodeName: ["TR"] }),
8118 cType = cell.el.tagName.toLowerCase(),
8120 doc = this.table.ownerDocument,
8123 for (var i = 0; i < addRowsNr; i++) {
8124 cidx = this.correctColIndexForUnreals(this.idx.col, (ridx + i));
8125 crow = nextNode(crow, 'tr');
8130 temp_r_cells = this.getRowCells(crow);
8131 if (cidx > 0 && this.map[ridx + i][this.idx.col].el != temp_r_cells[cidx] && cidx == temp_r_cells.length - 1) {
8132 insertAfter(temp_r_cells[cidx], this.createCells(cType, 1));
8134 temp_r_cells[cidx].parentNode.insertBefore(this.createCells(cType, 1), temp_r_cells[cidx]);
8139 insertAfter(this.getRowCells(crow)[cidx], this.createCells(cType, 1));
8143 crow.insertBefore(this.createCells(cType, 1), crow.firstChild);
8146 nrow = doc.createElement('tr');
8147 nrow.appendChild(this.createCells(cType, 1));
8148 this.table.appendChild(nrow);
8155 getCellsBetween: function(cell1, cell2) {
8156 var c1 = new TableModifyerByCell(cell1);
8157 return c1.getMapElsTo(cell2);
8160 addCells: function(cell, where) {
8161 var c = new TableModifyerByCell(cell);
8165 removeCells: function(cell, what) {
8166 var c = new TableModifyerByCell(cell);
8170 mergeCellsBetween: function(cell1, cell2) {
8171 var c1 = new TableModifyerByCell(cell1);
8175 unmergeCell: function(cell) {
8176 var c = new TableModifyerByCell(cell);
8180 orderSelectionEnds: function(cell, cell2) {
8181 var c = new TableModifyerByCell(cell);
8182 return c.orderSelectionEnds(cell2);
8185 indexOf: function(cell) {
8186 var c = new TableModifyerByCell(cell);
8188 return c.getMapIndex(cell);
8191 findCell: function(table, idx) {
8192 var c = new TableModifyerByCell(null, table);
8193 return c.getElementAtIndex(idx);
8196 findRowByCell: function(cell) {
8197 var c = new TableModifyerByCell(cell);
8198 return c.getRowElementsByCell();
8201 findColumnByCell: function(cell) {
8202 var c = new TableModifyerByCell(cell);
8203 return c.getColumnElementsByCell();
8206 canMerge: function(cell1, cell2) {
8207 var c = new TableModifyerByCell(cell1);
8208 return c.canMerge(cell2);
8215 ;// does a selector query on element or array of elements
8217 wysihtml5.dom.query = function(elements, query) {
8221 if (elements.nodeType) {
8222 elements = [elements];
8225 for (var e = 0, len = elements.length; e < len; e++) {
8226 q = elements[e].querySelectorAll(query);
8228 for(var i = q.length; i--; ret.unshift(q[i]));
8233 ;wysihtml5.dom.compareDocumentPosition = (function() {
8234 var documentElement = document.documentElement;
8235 if (documentElement.compareDocumentPosition) {
8236 return function(container, element) {
8237 return container.compareDocumentPosition(element);
8240 return function( container, element ) {
8241 // implementation borrowed from https://github.com/tmpvar/jsdom/blob/681a8524b663281a0f58348c6129c8c184efc62c/lib/jsdom/level3/core.js // MIT license
8242 var thisOwner, otherOwner;
8244 if( container.nodeType === 9) // Node.DOCUMENT_NODE
8245 thisOwner = container;
8247 thisOwner = container.ownerDocument;
8249 if( element.nodeType === 9) // Node.DOCUMENT_NODE
8250 otherOwner = element;
8252 otherOwner = element.ownerDocument;
8254 if( container === element ) return 0;
8255 if( container === element.ownerDocument ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
8256 if( container.ownerDocument === element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
8257 if( thisOwner !== otherOwner ) return 1; // Node.DOCUMENT_POSITION_DISCONNECTED;
8259 // Text nodes for attributes does not have a _parentNode. So we need to find them as attribute child.
8260 if( container.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && container.childNodes && wysihtml5.lang.array(container.childNodes).indexOf( element ) !== -1)
8261 return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
8263 if( element.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && element.childNodes && wysihtml5.lang.array(element.childNodes).indexOf( container ) !== -1)
8264 return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
8266 var point = container;
8268 var previous = null;
8270 if( point == element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
8271 parents.push( point );
8272 point = point.parentNode;
8277 if( point == container ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
8278 var location_index = wysihtml5.lang.array(parents).indexOf( point );
8279 if( location_index !== -1) {
8280 var smallest_common_ancestor = parents[ location_index ];
8281 var this_index = wysihtml5.lang.array(smallest_common_ancestor.childNodes).indexOf( parents[location_index - 1]);//smallest_common_ancestor.childNodes.toArray().indexOf( parents[location_index - 1] );
8282 var other_index = wysihtml5.lang.array(smallest_common_ancestor.childNodes).indexOf( previous ); //smallest_common_ancestor.childNodes.toArray().indexOf( previous );
8283 if( this_index > other_index ) {
8284 return 2; //Node.DOCUMENT_POSITION_PRECEDING;
8287 return 4; //Node.DOCUMENT_POSITION_FOLLOWING;
8291 point = point.parentNode;
8293 return 1; //Node.DOCUMENT_POSITION_DISCONNECTED;
8297 ;wysihtml5.dom.unwrap = function(node) {
8298 if (node.parentNode) {
8299 while (node.lastChild) {
8300 wysihtml5.dom.insert(node.lastChild).after(node);
8302 node.parentNode.removeChild(node);
8305 * Methods for fetching pasted html before it gets inserted into content
8308 /* Modern event.clipboardData driven approach.
8309 * Advantage is that it does not have to loose selection or modify dom to catch the data.
8310 * IE does not support though.
8312 wysihtml5.dom.getPastedHtml = function(event) {
8314 if (event.clipboardData) {
8315 if (wysihtml5.lang.array(event.clipboardData.types).contains('text/html')) {
8316 html = event.clipboardData.getData('text/html');
8317 } else if (wysihtml5.lang.array(event.clipboardData.types).contains('text/plain')) {
8318 html = wysihtml5.lang.string(event.clipboardData.getData('text/plain')).escapeHTML(true, true);
8324 /* Older temprorary contenteditable as paste source catcher method for fallbacks */
8325 wysihtml5.dom.getPastedHtmlWithDiv = function (composer, f) {
8326 var selBookmark = composer.selection.getBookmark(),
8327 doc = composer.element.ownerDocument,
8328 cleanerDiv = doc.createElement('DIV');
8330 doc.body.appendChild(cleanerDiv);
8332 cleanerDiv.style.width = "1px";
8333 cleanerDiv.style.height = "1px";
8334 cleanerDiv.style.overflow = "hidden";
8336 cleanerDiv.setAttribute('contenteditable', 'true');
8339 setTimeout(function () {
8340 composer.selection.setBookmark(selBookmark);
8341 f(cleanerDiv.innerHTML);
8342 cleanerDiv.parentNode.removeChild(cleanerDiv);
8345 * Fix most common html formatting misbehaviors of browsers implementation when inserting
8346 * content via copy & paste contentEditable
8348 * @author Christopher Blum
8350 wysihtml5.quirks.cleanPastedHTML = (function() {
8352 var styleToRegex = function (styleStr) {
8353 var trimmedStr = wysihtml5.lang.string(styleStr).trim(),
8354 escapedStr = trimmedStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
8356 return new RegExp("^((?!^" + escapedStr + "$).)*$", "i");
8359 var extendRulesWithStyleExceptions = function (rules, exceptStyles) {
8360 var newRules = wysihtml5.lang.object(rules).clone(true),
8363 for (tag in newRules.tags) {
8365 if (newRules.tags.hasOwnProperty(tag)) {
8366 if (newRules.tags[tag].keep_styles) {
8367 for (style in newRules.tags[tag].keep_styles) {
8368 if (newRules.tags[tag].keep_styles.hasOwnProperty(style)) {
8369 if (exceptStyles[style]) {
8370 newRules.tags[tag].keep_styles[style] = styleToRegex(exceptStyles[style]);
8381 var pickRuleset = function(ruleset, html) {
8382 var pickedSet, defaultSet;
8388 for (var i = 0, max = ruleset.length; i < max; i++) {
8389 if (!ruleset[i].condition) {
8390 defaultSet = ruleset[i].set;
8392 if (ruleset[i].condition && ruleset[i].condition.test(html)) {
8393 return ruleset[i].set;
8400 return function(html, options) {
8401 var exceptStyles = {
8402 'color': wysihtml5.dom.getStyle("color").from(options.referenceNode),
8403 'fontSize': wysihtml5.dom.getStyle("font-size").from(options.referenceNode)
8405 rules = extendRulesWithStyleExceptions(pickRuleset(options.rules, html) || {}, exceptStyles),
8408 newHtml = wysihtml5.dom.parse(html, {
8410 "cleanUp": true, // <span> elements, empty or without attributes, should be removed/replaced with their content
8411 "context": options.referenceNode.ownerDocument,
8412 "uneditableClass": options.uneditableClass,
8413 "clearInternals" : true, // don't paste temprorary selection and other markings
8414 "unjoinNbsps" : true
8421 * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
8423 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
8425 * wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
8427 wysihtml5.quirks.ensureProperClearing = (function() {
8428 var clearIfNecessary = function() {
8430 setTimeout(function() {
8431 var innerHTML = element.innerHTML.toLowerCase();
8432 if (innerHTML == "<p> </p>" ||
8433 innerHTML == "<p> </p><p> </p>") {
8434 element.innerHTML = "";
8439 return function(composer) {
8440 wysihtml5.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);
8443 ;// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
8446 // var d = document.createElement("div");
8447 // d.innerHTML ='<a href="~"></a>';
8450 // <a href="%7E"></a>
8452 (function(wysihtml5) {
8453 var TILDE_ESCAPED = "%7E";
8454 wysihtml5.quirks.getCorrectInnerHTML = function(element) {
8455 var innerHTML = element.innerHTML;
8456 if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {
8460 var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),
8465 for (i=0, length=elementsWithTilde.length; i<length; i++) {
8466 url = elementsWithTilde[i].href || elementsWithTilde[i].src;
8467 urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED);
8468 innerHTML = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url);
8474 * Force rerendering of a given element
8475 * Needed to fix display misbehaviors of IE
8477 * @param {Element} element The element object which needs to be rerendered
8479 * wysihtml5.quirks.redraw(document.body);
8481 (function(wysihtml5) {
8482 var CLASS_NAME = "wysihtml5-quirks-redraw";
8484 wysihtml5.quirks.redraw = function(element) {
8485 wysihtml5.dom.addClass(element, CLASS_NAME);
8486 wysihtml5.dom.removeClass(element, CLASS_NAME);
8488 // Following hack is needed for firefox to make sure that image resize handles are properly removed
8490 var doc = element.ownerDocument;
8491 doc.execCommand("italic", false, null);
8492 doc.execCommand("italic", false, null);
8496 ;wysihtml5.quirks.tableCellsSelection = function(editable, editor) {
8498 var dom = wysihtml5.dom,
8506 selection_class = "wysiwyg-tmp-selected-cell",
8512 dom.observe(editable, "mousedown", function(event) {
8513 var target = wysihtml5.dom.getParentElement(event.target, { nodeName: ["TD", "TH"] });
8515 handleSelectionMousedown(target);
8522 function handleSelectionMousedown (target) {
8523 select.start = target;
8524 select.end = target;
8525 select.cells = [target];
8526 select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] });
8529 removeCellSelections();
8530 dom.addClass(target, selection_class);
8531 moveHandler = dom.observe(editable, "mousemove", handleMouseMove);
8532 upHandler = dom.observe(editable, "mouseup", handleMouseUp);
8533 editor.fire("tableselectstart").fire("tableselectstart:composer");
8537 // remove all selection classes
8538 function removeCellSelections () {
8540 var selectedCells = editable.querySelectorAll('.' + selection_class);
8541 if (selectedCells.length > 0) {
8542 for (var i = 0; i < selectedCells.length; i++) {
8543 dom.removeClass(selectedCells[i], selection_class);
8549 function addSelections (cells) {
8550 for (var i = 0; i < cells.length; i++) {
8551 dom.addClass(cells[i], selection_class);
8555 function handleMouseMove (event) {
8556 var curTable = null,
8557 cell = dom.getParentElement(event.target, { nodeName: ["TD","TH"] }),
8560 if (cell && select.table && select.start) {
8561 curTable = dom.getParentElement(cell, { nodeName: ["TABLE"] });
8562 if (curTable && curTable === select.table) {
8563 removeCellSelections();
8564 oldEnd = select.end;
8566 select.cells = dom.table.getCellsBetween(select.start, cell);
8567 if (select.cells.length > 1) {
8568 editor.composer.selection.deselect();
8570 addSelections(select.cells);
8571 if (select.end !== oldEnd) {
8572 editor.fire("tableselectchange").fire("tableselectchange:composer");
8578 function handleMouseUp (event) {
8581 editor.fire("tableselect").fire("tableselect:composer");
8582 setTimeout(function() {
8587 function bindSideclick () {
8588 var sideClickHandler = dom.observe(editable.ownerDocument, "click", function(event) {
8589 sideClickHandler.stop();
8590 if (dom.getParentElement(event.target, { nodeName: ["TABLE"] }) != select.table) {
8591 removeCellSelections();
8592 select.table = null;
8593 select.start = null;
8595 editor.fire("tableunselect").fire("tableunselect:composer");
8600 function selectCells (start, end) {
8601 select.start = start;
8603 select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] });
8604 selectedCells = dom.table.getCellsBetween(select.start, select.end);
8605 addSelections(selectedCells);
8607 editor.fire("tableselect").fire("tableselect:composer");
8613 ;(function(wysihtml5) {
8614 var RGBA_REGEX = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d\.]+)\s*\)/i,
8615 RGB_REGEX = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i,
8616 HEX6_REGEX = /^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])/i,
8617 HEX3_REGEX = /^#([0-9a-f])([0-9a-f])([0-9a-f])/i;
8619 var param_REGX = function (p) {
8620 return new RegExp("(^|\\s|;)" + p + "\\s*:\\s*[^;$]+" , "gi");
8623 wysihtml5.quirks.styleParser = {
8625 parseColor: function(stylesStr, paramName) {
8626 var paramRegex = param_REGX(paramName),
8627 params = stylesStr.match(paramRegex),
8632 for (var i = params.length; i--;) {
8633 params[i] = wysihtml5.lang.string(params[i].split(':')[1]).trim();
8635 str = params[params.length-1];
8637 if (RGBA_REGEX.test(str)) {
8638 colorMatch = str.match(RGBA_REGEX);
8639 } else if (RGB_REGEX.test(str)) {
8640 colorMatch = str.match(RGB_REGEX);
8641 } else if (HEX6_REGEX.test(str)) {
8642 colorMatch = str.match(HEX6_REGEX);
8644 } else if (HEX3_REGEX.test(str)) {
8645 colorMatch = str.match(HEX3_REGEX);
8648 return wysihtml5.lang.array(colorMatch).map(function(d, idx) {
8649 return (idx < 3) ? (parseInt(d, 16) * 16) + parseInt(d, 16): parseFloat(d);
8655 if (!colorMatch[3]) {
8658 return wysihtml5.lang.array(colorMatch).map(function(d, idx) {
8659 return (idx < 3) ? parseInt(d, radix): parseFloat(d);
8666 unparseColor: function(val, props) {
8668 if (props == "hex") {
8669 return (val[0].toString(16).toUpperCase()) + (val[1].toString(16).toUpperCase()) + (val[2].toString(16).toUpperCase());
8670 } else if (props == "hash") {
8671 return "#" + (val[0].toString(16).toUpperCase()) + (val[1].toString(16).toUpperCase()) + (val[2].toString(16).toUpperCase());
8672 } else if (props == "rgb") {
8673 return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
8674 } else if (props == "rgba") {
8675 return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
8676 } else if (props == "csv") {
8677 return val[0] + "," + val[1] + "," + val[2] + "," + val[3];
8681 if (val[3] && val[3] !== 1) {
8682 return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
8684 return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
8688 parseFontSize: function(stylesStr) {
8689 var params = stylesStr.match(param_REGX('font-size'));
8691 return wysihtml5.lang.string(params[params.length - 1].split(':')[1]).trim();
8702 * var selection = new wysihtml5.Selection(editor);
8704 (function(wysihtml5) {
8705 var dom = wysihtml5.dom;
8707 function _getCumulativeOffsetTop(element) {
8709 if (element.parentNode) {
8711 top += element.offsetTop || 0;
8712 element = element.offsetParent;
8718 // Provides the depth of ``descendant`` relative to ``ancestor``
8719 function getDepth(ancestor, descendant) {
8721 while (descendant !== ancestor) {
8723 descendant = descendant.parentNode;
8725 throw new Error("not a descendant of ancestor!");
8730 // Should fix the obtained ranges that cannot surrond contents normally to apply changes upon
8731 // Being considerate to firefox that sets range start start out of span and end inside on doubleclick initiated selection
8732 function expandRangeToSurround(range) {
8733 if (range.canSurroundContents()) return;
8735 var common = range.commonAncestorContainer,
8736 start_depth = getDepth(common, range.startContainer),
8737 end_depth = getDepth(common, range.endContainer);
8739 while(!range.canSurroundContents()) {
8740 // In the following branches, we cannot just decrement the depth variables because the setStartBefore/setEndAfter may move the start or end of the range more than one level relative to ``common``. So we need to recompute the depth.
8741 if (start_depth > end_depth) {
8742 range.setStartBefore(range.startContainer);
8743 start_depth = getDepth(common, range.startContainer);
8746 range.setEndAfter(range.endContainer);
8747 end_depth = getDepth(common, range.endContainer);
8752 wysihtml5.Selection = Base.extend(
8753 /** @scope wysihtml5.Selection.prototype */ {
8754 constructor: function(editor, contain, unselectableClass) {
8755 // Make sure that our external range library is initialized
8756 window.rangy.init();
8758 this.editor = editor;
8759 this.composer = editor.composer;
8760 this.doc = this.composer.doc;
8761 this.contain = contain;
8762 this.unselectableClass = unselectableClass || false;
8766 * Get the current selection as a bookmark to be able to later restore it
8768 * @return {Object} An object that represents the current selection
8770 getBookmark: function() {
8771 var range = this.getRange();
8772 if (range) expandRangeToSurround(range);
8773 return range && range.cloneRange();
8777 * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark
8779 * @param {Object} bookmark An object that represents the current selection
8781 setBookmark: function(bookmark) {
8786 this.setSelection(bookmark);
8790 * Set the caret in front of the given node
8792 * @param {Object} node The element or text node where to position the caret in front of
8794 * selection.setBefore(myElement);
8796 setBefore: function(node) {
8797 var range = rangy.createRange(this.doc);
8798 range.setStartBefore(node);
8799 range.setEndBefore(node);
8800 return this.setSelection(range);
8804 * Set the caret after the given node
8806 * @param {Object} node The element or text node where to position the caret in front of
8808 * selection.setBefore(myElement);
8810 setAfter: function(node) {
8811 var range = rangy.createRange(this.doc);
8813 range.setStartAfter(node);
8814 range.setEndAfter(node);
8815 return this.setSelection(range);
8819 * Ability to select/mark nodes
8821 * @param {Element} node The node/element to select
8823 * selection.selectNode(document.getElementById("my-image"));
8825 selectNode: function(node, avoidInvisibleSpace) {
8826 var range = rangy.createRange(this.doc),
8827 isElement = node.nodeType === wysihtml5.ELEMENT_NODE,
8828 canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
8829 content = isElement ? node.innerHTML : node.data,
8830 isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE),
8831 displayStyle = dom.getStyle("display").from(node),
8832 isBlockElement = (displayStyle === "block" || displayStyle === "list-item");
8834 if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) {
8835 // Make sure that caret is visible in node by inserting a zero width no breaking space
8836 try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
8840 range.selectNodeContents(node);
8842 range.selectNode(node);
8845 if (canHaveHTML && isEmpty && isElement) {
8846 range.collapse(isBlockElement);
8847 } else if (canHaveHTML && isEmpty) {
8848 range.setStartAfter(node);
8849 range.setEndAfter(node);
8852 this.setSelection(range);
8856 * Get the node which contains the selection
8858 * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
8859 * @return {Object} The node that contains the caret
8861 * var nodeThatContainsCaret = selection.getSelectedNode();
8863 getSelectedNode: function(controlRange) {
8867 if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
8868 range = this.doc.selection.createRange();
8869 if (range && range.length) {
8870 return range.item(0);
8874 selection = this.getSelection(this.doc);
8875 if (selection.focusNode === selection.anchorNode) {
8876 return selection.focusNode;
8878 range = this.getRange(this.doc);
8879 return range ? range.commonAncestorContainer : this.doc.body;
8883 fixSelBorders: function() {
8884 var range = this.getRange();
8885 expandRangeToSurround(range);
8886 this.setSelection(range);
8889 getSelectedOwnNodes: function(controlRange) {
8891 ranges = this.getOwnRanges(),
8894 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
8895 ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body);
8900 findNodesInSelection: function(nodeTypes) {
8901 var ranges = this.getOwnRanges(),
8902 nodes = [], curNodes;
8903 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
8904 curNodes = ranges[i].getNodes([1], function(node) {
8905 return wysihtml5.lang.array(nodeTypes).contains(node.nodeName);
8907 nodes = nodes.concat(curNodes);
8912 containsUneditable: function() {
8913 var uneditables = this.getOwnUneditables(),
8914 selection = this.getSelection();
8916 for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
8917 if (selection.containsNode(uneditables[i])) {
8925 deleteContents: function() {
8926 var ranges = this.getOwnRanges();
8927 for (var i = ranges.length; i--;) {
8928 ranges[i].deleteContents();
8930 this.setSelection(ranges[0]);
8933 getPreviousNode: function(node, ignoreEmpty) {
8935 var selection = this.getSelection();
8936 node = selection.anchorNode;
8939 if (node === this.contain) {
8943 var ret = node.previousSibling,
8946 if (ret === this.contain) {
8950 if (ret && ret.nodeType !== 3 && ret.nodeType !== 1) {
8951 // do not count comments and other node types
8952 ret = this.getPreviousNode(ret, ignoreEmpty);
8953 } else if (ret && ret.nodeType === 3 && (/^\s*$/).test(ret.textContent)) {
8954 // do not count empty textnodes as previus nodes
8955 ret = this.getPreviousNode(ret, ignoreEmpty);
8956 } else if (ignoreEmpty && ret && ret.nodeType === 1 && !wysihtml5.lang.array(["BR", "HR", "IMG"]).contains(ret.nodeName) && (/^[\s]*$/).test(ret.innerHTML)) {
8957 // Do not count empty nodes if param set.
8958 // Contenteditable tends to bypass and delete these silently when deleting with caret
8959 ret = this.getPreviousNode(ret, ignoreEmpty);
8960 } else if (!ret && node !== this.contain) {
8961 parent = node.parentNode;
8962 if (parent !== this.contain) {
8963 ret = this.getPreviousNode(parent, ignoreEmpty);
8967 return (ret !== this.contain) ? ret : false;
8970 getSelectionParentsByTag: function(tagName) {
8971 var nodes = this.getSelectedOwnNodes(),
8972 curEl, parents = [];
8974 for (var i = 0, maxi = nodes.length; i < maxi; i++) {
8975 curEl = (nodes[i].nodeName && nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml5.dom.getParentElement(nodes[i], { nodeName: ['LI']}, false, this.contain);
8977 parents.push(curEl);
8980 return (parents.length) ? parents : null;
8983 getRangeToNodeEnd: function() {
8984 if (this.isCollapsed()) {
8985 var range = this.getRange(),
8986 sNode = range.startContainer,
8987 pos = range.startOffset,
8988 lastR = rangy.createRange(this.doc);
8990 lastR.selectNodeContents(sNode);
8991 lastR.setStart(sNode, pos);
8996 caretIsLastInSelection: function() {
8997 var r = rangy.createRange(this.doc),
8998 s = this.getSelection(),
8999 endc = this.getRangeToNodeEnd().cloneContents(),
9000 endtxt = endc.textContent;
9002 return (/^\s*$/).test(endtxt);
9005 caretIsFirstInSelection: function() {
9006 var r = rangy.createRange(this.doc),
9007 s = this.getSelection(),
9008 range = this.getRange(),
9009 startNode = range.startContainer;
9011 if (startNode.nodeType === wysihtml5.TEXT_NODE) {
9012 return this.isCollapsed() && (startNode.nodeType === wysihtml5.TEXT_NODE && (/^\s*$/).test(startNode.data.substr(0,range.startOffset)));
9014 r.selectNodeContents(this.getRange().commonAncestorContainer);
9016 return (this.isCollapsed() && (r.startContainer === s.anchorNode || r.endContainer === s.anchorNode) && r.startOffset === s.anchorOffset);
9020 caretIsInTheBeginnig: function(ofNode) {
9021 var selection = this.getSelection(),
9022 node = selection.anchorNode,
9023 offset = selection.anchorOffset;
9025 return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml5.dom.getParentElement(node.parentNode, { nodeName: ofNode }, 1)));
9027 return (offset === 0 && !this.getPreviousNode(node, true));
9031 caretIsBeforeUneditable: function() {
9032 var selection = this.getSelection(),
9033 node = selection.anchorNode,
9034 offset = selection.anchorOffset;
9037 var prevNode = this.getPreviousNode(node, true);
9039 var uneditables = this.getOwnUneditables();
9040 for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
9041 if (prevNode === uneditables[i]) {
9042 return uneditables[i];
9050 // TODO: Figure out a method from following 2 that would work universally
9051 executeAndRestoreRangy: function(method, restoreScrollPosition) {
9052 var win = this.doc.defaultView || this.doc.parentWindow,
9053 sel = rangy.saveSelection(win);
9061 setTimeout(function() { throw e; }, 0);
9064 rangy.restoreSelection(sel);
9067 // TODO: has problems in chrome 12. investigate block level and uneditable area inbetween
9068 executeAndRestore: function(method, restoreScrollPosition) {
9069 var body = this.doc.body,
9070 oldScrollTop = restoreScrollPosition && body.scrollTop,
9071 oldScrollLeft = restoreScrollPosition && body.scrollLeft,
9072 className = "_wysihtml5-temp-placeholder",
9073 placeholderHtml = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>',
9074 range = this.getRange(true),
9076 newCaretPlaceholder,
9077 nextSibling, prevSibling,
9078 node, node2, range2,
9081 // Nothing selected, execute and say goodbye
9087 if (!range.collapsed) {
9088 range2 = range.cloneRange();
9089 node2 = range2.createContextualFragment(placeholderHtml);
9090 range2.collapse(false);
9091 range2.insertNode(node2);
9095 node = range.createContextualFragment(placeholderHtml);
9096 range.insertNode(node);
9099 caretPlaceholder = this.contain.querySelectorAll("." + className);
9100 range.setStartBefore(caretPlaceholder[0]);
9101 range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]);
9103 this.setSelection(range);
9105 // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
9107 method(range.startContainer, range.endContainer);
9109 setTimeout(function() { throw e; }, 0);
9111 caretPlaceholder = this.contain.querySelectorAll("." + className);
9112 if (caretPlaceholder && caretPlaceholder.length) {
9113 newRange = rangy.createRange(this.doc);
9114 nextSibling = caretPlaceholder[0].nextSibling;
9115 if (caretPlaceholder.length > 1) {
9116 prevSibling = caretPlaceholder[caretPlaceholder.length -1].previousSibling;
9118 if (prevSibling && nextSibling) {
9119 newRange.setStartBefore(nextSibling);
9120 newRange.setEndAfter(prevSibling);
9122 newCaretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
9123 dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]);
9124 newRange.setStartBefore(newCaretPlaceholder);
9125 newRange.setEndAfter(newCaretPlaceholder);
9127 this.setSelection(newRange);
9128 for (var i = caretPlaceholder.length; i--;) {
9129 caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]);
9133 // fallback for when all hell breaks loose
9134 this.contain.focus();
9137 if (restoreScrollPosition) {
9138 body.scrollTop = oldScrollTop;
9139 body.scrollLeft = oldScrollLeft;
9142 // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
9144 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
9148 set: function(node, offset) {
9149 var newRange = rangy.createRange(this.doc);
9150 newRange.setStart(node, offset || 0);
9151 this.setSelection(newRange);
9155 * Insert html at the caret position and move the cursor after the inserted html
9157 * @param {String} html HTML string to insert
9159 * selection.insertHTML("<p>foobar</p>");
9161 insertHTML: function(html) {
9162 var range = rangy.createRange(this.doc),
9163 node = this.doc.createElement('DIV'),
9164 fragment = this.doc.createDocumentFragment(),
9167 node.innerHTML = html;
9168 lastChild = node.lastChild;
9170 while (node.firstChild) {
9171 fragment.appendChild(node.firstChild);
9173 this.insertNode(fragment);
9176 this.setAfter(lastChild);
9181 * Insert a node at the caret position and move the cursor behind it
9183 * @param {Object} node HTML string to insert
9185 * selection.insertNode(document.createTextNode("foobar"));
9187 insertNode: function(node) {
9188 var range = this.getRange();
9190 range.insertNode(node);
9195 * Wraps current selection with the given node
9197 * @param {Object} node The node to surround the selected elements with
9199 surround: function(nodeOptions) {
9200 var ranges = this.getOwnRanges(),
9202 if (ranges.length == 0) {
9206 for (var i = ranges.length; i--;) {
9207 node = this.doc.createElement(nodeOptions.nodeName);
9209 if (nodeOptions.className) {
9210 node.className = nodeOptions.className;
9212 if (nodeOptions.cssStyle) {
9213 node.setAttribute('style', nodeOptions.cssStyle);
9216 // This only works when the range boundaries are not overlapping other elements
9217 ranges[i].surroundContents(node);
9218 this.selectNode(node);
9221 node.appendChild(ranges[i].extractContents());
9222 ranges[i].insertNode(node);
9228 deblockAndSurround: function(nodeOptions) {
9229 var tempElement = this.doc.createElement('div'),
9230 range = rangy.createRange(this.doc),
9235 tempElement.className = nodeOptions.className;
9237 this.composer.commands.exec("formatBlock", nodeOptions.nodeName, nodeOptions.className);
9238 tempDivElements = this.contain.querySelectorAll("." + nodeOptions.className);
9239 if (tempDivElements[0]) {
9240 tempDivElements[0].parentNode.insertBefore(tempElement, tempDivElements[0]);
9242 range.setStartBefore(tempDivElements[0]);
9243 range.setEndAfter(tempDivElements[tempDivElements.length - 1]);
9244 tempElements = range.extractContents();
9246 while (tempElements.firstChild) {
9247 firstChild = tempElements.firstChild;
9248 if (firstChild.nodeType == 1 && wysihtml5.dom.hasClass(firstChild, nodeOptions.className)) {
9249 while (firstChild.firstChild) {
9250 tempElement.appendChild(firstChild.firstChild);
9252 if (firstChild.nodeName !== "BR") { tempElement.appendChild(this.doc.createElement('br')); }
9253 tempElements.removeChild(firstChild);
9255 tempElement.appendChild(firstChild);
9266 * Scroll the current caret position into the view
9267 * FIXME: This is a bit hacky, there might be a smarter way of doing this
9270 * selection.scrollIntoView();
9272 scrollIntoView: function() {
9274 tolerance = 5, // px
9275 hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
9276 tempElement = doc._wysihtml5ScrollIntoViewElement = doc._wysihtml5ScrollIntoViewElement || (function() {
9277 var element = doc.createElement("span");
9278 // The element needs content in order to be able to calculate it's position properly
9279 element.innerHTML = wysihtml5.INVISIBLE_SPACE;
9284 if (hasScrollBars) {
9285 this.insertNode(tempElement);
9286 offsetTop = _getCumulativeOffsetTop(tempElement);
9287 tempElement.parentNode.removeChild(tempElement);
9288 if (offsetTop >= (doc.body.scrollTop + doc.documentElement.offsetHeight - tolerance)) {
9289 doc.body.scrollTop = offsetTop;
9295 * Select line where the caret is in
9297 selectLine: function() {
9298 if (wysihtml5.browser.supportsSelectionModify()) {
9299 this._selectLine_W3C();
9300 } else if (this.doc.selection) {
9301 this._selectLine_MSIE();
9306 * See https://developer.mozilla.org/en/DOM/Selection/modify
9308 _selectLine_W3C: function() {
9309 var win = this.doc.defaultView,
9310 selection = win.getSelection();
9311 selection.modify("move", "left", "lineboundary");
9312 selection.modify("extend", "right", "lineboundary");
9315 _selectLine_MSIE: function() {
9316 var range = this.doc.selection.createRange(),
9317 rangeTop = range.boundingTop,
9318 scrollWidth = this.doc.body.scrollWidth,
9325 if (!range.moveToPoint) {
9329 if (rangeTop === 0) {
9330 // Don't know why, but when the selection ends at the end of a line
9331 // range.boundingTop is 0
9332 measureNode = this.doc.createElement("span");
9333 this.insertNode(measureNode);
9334 rangeTop = measureNode.offsetTop;
9335 measureNode.parentNode.removeChild(measureNode);
9340 for (i=-10; i<scrollWidth; i+=2) {
9342 range.moveToPoint(i, rangeTop);
9347 // Investigate the following in order to handle multi line selections
9348 // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0);
9349 rangeBottom = rangeTop;
9350 rangeEnd = this.doc.selection.createRange();
9351 for (j=scrollWidth; j>=0; j--) {
9353 rangeEnd.moveToPoint(j, rangeBottom);
9358 range.setEndPoint("EndToEnd", rangeEnd);
9362 getText: function() {
9363 var selection = this.getSelection();
9364 return selection ? selection.toString() : "";
9367 getNodes: function(nodeType, filter) {
9368 var range = this.getRange();
9370 return range.getNodes([nodeType], filter);
9376 fixRangeOverflow: function(range) {
9377 if (this.contain && this.contain.firstChild && range) {
9378 var containment = range.compareNode(this.contain);
9379 if (containment !== 2) {
9380 if (containment === 1) {
9381 range.setStartBefore(this.contain.firstChild);
9383 if (containment === 0) {
9384 range.setEndAfter(this.contain.lastChild);
9386 if (containment === 3) {
9387 range.setStartBefore(this.contain.firstChild);
9388 range.setEndAfter(this.contain.lastChild);
9390 } else if (this._detectInlineRangeProblems(range)) {
9391 var previousElementSibling = range.endContainer.previousElementSibling;
9392 if (previousElementSibling) {
9393 range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling));
9399 _endOffsetForNode: function(node) {
9400 var range = document.createRange();
9401 range.selectNodeContents(node);
9402 return range.endOffset;
9405 _detectInlineRangeProblems: function(range) {
9406 var position = dom.compareDocumentPosition(range.startContainer, range.endContainer);
9408 range.endOffset == 0 &&
9409 position & 4 //Node.DOCUMENT_POSITION_FOLLOWING
9413 getRange: function(dontFix) {
9414 var selection = this.getSelection(),
9415 range = selection && selection.rangeCount && selection.getRangeAt(0);
9417 if (dontFix !== true) {
9418 this.fixRangeOverflow(range);
9424 getOwnUneditables: function() {
9425 var allUneditables = dom.query(this.contain, '.' + this.unselectableClass),
9426 deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass);
9428 return wysihtml5.lang.array(allUneditables).without(deepUneditables);
9431 // Returns an array of ranges that belong only to this editable
9432 // Needed as uneditable block in contenteditabel can split range into pieces
9433 // If manipulating content reverse loop is usually needed as manipulation can shift subsequent ranges
9434 getOwnRanges: function() {
9436 r = this.getRange(),
9439 if (r) { ranges.push(r); }
9441 if (this.unselectableClass && this.contain && r) {
9442 var uneditables = this.getOwnUneditables(),
9444 if (uneditables.length > 0) {
9445 for (var i = 0, imax = uneditables.length; i < imax; i++) {
9447 for (var j = 0, jmax = ranges.length; j < jmax; j++) {
9449 switch (ranges[j].compareNode(uneditables[i])) {
9451 // all selection inside uneditable. remove
9454 //section begins before and ends after uneditable. spilt
9455 tmpRange = ranges[j].cloneRange();
9456 tmpRange.setEndBefore(uneditables[i]);
9457 tmpRanges.push(tmpRange);
9459 tmpRange = ranges[j].cloneRange();
9460 tmpRange.setStartAfter(uneditables[i]);
9461 tmpRanges.push(tmpRange);
9464 // in all other cases uneditable does not touch selection. dont modify
9465 tmpRanges.push(ranges[j]);
9476 getSelection: function() {
9477 return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow);
9480 setSelection: function(range) {
9481 var win = this.doc.defaultView || this.doc.parentWindow,
9482 selection = rangy.getSelection(win);
9483 return selection.setSingleRange(range);
9486 createRange: function() {
9487 return rangy.createRange(this.doc);
9490 isCollapsed: function() {
9491 return this.getSelection().isCollapsed;
9494 getHtml: function() {
9495 return this.getSelection().toHtml();
9498 isEndToEndInNode: function(nodeNames) {
9499 var range = this.getRange(),
9500 parentElement = range.commonAncestorContainer,
9501 startNode = range.startContainer,
9502 endNode = range.endContainer;
9505 if (parentElement.nodeType === wysihtml5.TEXT_NODE) {
9506 parentElement = parentElement.parentNode;
9509 if (startNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) {
9513 if (endNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) {
9517 while (startNode && startNode !== parentElement) {
9518 if (startNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, startNode)) {
9521 if (wysihtml5.dom.domNode(startNode).prev({ignoreBlankTexts: true})) {
9524 startNode = startNode.parentNode;
9527 while (endNode && endNode !== parentElement) {
9528 if (endNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, endNode)) {
9531 if (wysihtml5.dom.domNode(endNode).next({ignoreBlankTexts: true})) {
9534 endNode = endNode.parentNode;
9537 return (wysihtml5.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false;
9540 deselect: function() {
9541 var sel = this.getSelection();
9542 sel && sel.removeAllRanges();
9548 * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license.
9549 * http://code.google.com/p/rangy/
9551 * changed in order to be able ...
9552 * - to use custom tags
9553 * - to detect and replace similar css classes via reg exp
9555 (function(wysihtml5, rangy) {
9556 var defaultTagName = "span";
9558 var REG_EXP_WHITE_SPACE = /\s+/g;
9560 function hasClass(el, cssClass, regExp) {
9561 if (!el.className) {
9565 var matchingClassNames = el.className.match(regExp) || [];
9566 return matchingClassNames[matchingClassNames.length - 1] === cssClass;
9569 function hasStyleAttr(el, regExp) {
9570 if (!el.getAttribute || !el.getAttribute('style')) {
9573 var matchingStyles = el.getAttribute('style').match(regExp);
9574 return (el.getAttribute('style').match(regExp)) ? true : false;
9577 function addStyle(el, cssStyle, regExp) {
9578 if (el.getAttribute('style')) {
9579 removeStyle(el, regExp);
9580 if (el.getAttribute('style') && !(/^\s*$/).test(el.getAttribute('style'))) {
9581 el.setAttribute('style', cssStyle + ";" + el.getAttribute('style'));
9583 el.setAttribute('style', cssStyle);
9586 el.setAttribute('style', cssStyle);
9590 function addClass(el, cssClass, regExp) {
9592 removeClass(el, regExp);
9593 el.className += " " + cssClass;
9595 el.className = cssClass;
9599 function removeClass(el, regExp) {
9601 el.className = el.className.replace(regExp, "");
9605 function removeStyle(el, regExp) {
9608 if (el.getAttribute('style')) {
9609 s = el.getAttribute('style').split(';');
9610 for (var i = s.length; i--;) {
9611 if (!s[i].match(regExp) && !(/^\s*$/).test(s[i])) {
9616 el.setAttribute('style', s2.join(';'));
9618 el.removeAttribute('style');
9623 function getMatchingStyleRegexp(el, style) {
9625 sSplit = style.split(';'),
9626 elStyle = el.getAttribute('style');
9629 elStyle = elStyle.replace(/\s/gi, '').toLowerCase();
9630 regexes.push(new RegExp("(^|\\s|;)" + style.replace(/\s/gi, '').replace(/([\(\)])/gi, "\\$1").toLowerCase().replace(";", ";?").replace(/rgb\\\((\d+),(\d+),(\d+)\\\)/gi, "\\s?rgb\\($1,\\s?$2,\\s?$3\\)"), "gi"));
9632 for (var i = sSplit.length; i-- > 0;) {
9633 if (!(/^\s*$/).test(sSplit[i])) {
9634 regexes.push(new RegExp("(^|\\s|;)" + sSplit[i].replace(/\s/gi, '').replace(/([\(\)])/gi, "\\$1").toLowerCase().replace(";", ";?").replace(/rgb\\\((\d+),(\d+),(\d+)\\\)/gi, "\\s?rgb\\($1,\\s?$2,\\s?$3\\)"), "gi"));
9637 for (var j = 0, jmax = regexes.length; j < jmax; j++) {
9638 if (elStyle.match(regexes[j])) {
9647 function isMatchingAllready(node, tags, style, className) {
9649 return getMatchingStyleRegexp(node, style);
9650 } else if (className) {
9651 return wysihtml5.dom.hasClass(node, className);
9653 return rangy.dom.arrayContains(tags, node.tagName.toLowerCase());
9657 function areMatchingAllready(nodes, tags, style, className) {
9658 for (var i = nodes.length; i--;) {
9659 if (!isMatchingAllready(nodes[i], tags, style, className)) {
9663 return nodes.length ? true : false;
9666 function removeOrChangeStyle(el, style, regExp) {
9668 var exactRegex = getMatchingStyleRegexp(el, style);
9670 // adding same style value on property again removes style
9671 removeStyle(el, exactRegex);
9674 // adding new style value changes value
9675 addStyle(el, style, regExp);
9680 function hasSameClasses(el1, el2) {
9681 return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " ");
9684 function replaceWithOwnChildren(el) {
9685 var parent = el.parentNode;
9686 while (el.firstChild) {
9687 parent.insertBefore(el.firstChild, el);
9689 parent.removeChild(el);
9692 function elementsHaveSameNonClassAttributes(el1, el2) {
9693 if (el1.attributes.length != el2.attributes.length) {
9696 for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
9697 attr1 = el1.attributes[i];
9699 if (name != "class") {
9700 attr2 = el2.attributes.getNamedItem(name);
9701 if (attr1.specified != attr2.specified) {
9704 if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) {
9712 function isSplitPoint(node, offset) {
9713 if (rangy.dom.isCharacterDataNode(node)) {
9715 return !!node.previousSibling;
9716 } else if (offset == node.length) {
9717 return !!node.nextSibling;
9723 return offset > 0 && offset < node.childNodes.length;
9726 function splitNodeAt(node, descendantNode, descendantOffset, container) {
9728 if (rangy.dom.isCharacterDataNode(descendantNode)) {
9729 if (descendantOffset == 0) {
9730 descendantOffset = rangy.dom.getNodeIndex(descendantNode);
9731 descendantNode = descendantNode.parentNode;
9732 } else if (descendantOffset == descendantNode.length) {
9733 descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1;
9734 descendantNode = descendantNode.parentNode;
9736 newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset);
9740 if (!container || descendantNode !== container) {
9742 newNode = descendantNode.cloneNode(false);
9744 newNode.removeAttribute("id");
9747 while ((child = descendantNode.childNodes[descendantOffset])) {
9748 newNode.appendChild(child);
9750 rangy.dom.insertAfter(newNode, descendantNode);
9754 return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode), container);
9757 function Merge(firstNode) {
9758 this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE);
9759 this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
9760 this.textNodes = [this.firstTextNode];
9764 doMerge: function() {
9765 var textBits = [], textNode, parent, text;
9766 for (var i = 0, len = this.textNodes.length; i < len; ++i) {
9767 textNode = this.textNodes[i];
9768 parent = textNode.parentNode;
9769 textBits[i] = textNode.data;
9771 parent.removeChild(textNode);
9772 if (!parent.hasChildNodes()) {
9773 parent.parentNode.removeChild(parent);
9777 this.firstTextNode.data = text = textBits.join("");
9781 getLength: function() {
9782 var i = this.textNodes.length, len = 0;
9784 len += this.textNodes[i].length;
9789 toString: function() {
9791 for (var i = 0, len = this.textNodes.length; i < len; ++i) {
9792 textBits[i] = "'" + this.textNodes[i].data + "'";
9794 return "[Merge(" + textBits.join(",") + ")]";
9798 function HTMLApplier(tagNames, cssClass, similarClassRegExp, normalize, cssStyle, similarStyleRegExp, container) {
9799 this.tagNames = tagNames || [defaultTagName];
9800 this.cssClass = cssClass || ((cssClass === false) ? false : "");
9801 this.similarClassRegExp = similarClassRegExp;
9802 this.cssStyle = cssStyle || "";
9803 this.similarStyleRegExp = similarStyleRegExp;
9804 this.normalize = normalize;
9805 this.applyToAnyTagName = false;
9806 this.container = container;
9809 HTMLApplier.prototype = {
9810 getAncestorWithClass: function(node) {
9813 cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : (this.cssStyle !== "") ? false : true;
9814 if (node.nodeType == wysihtml5.ELEMENT_NODE && node.getAttribute("contenteditable") != "false" && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) {
9817 node = node.parentNode;
9822 // returns parents of node with given style attribute
9823 getAncestorWithStyle: function(node) {
9826 cssStyleMatch = this.cssStyle ? hasStyleAttr(node, this.similarStyleRegExp) : false;
9828 if (node.nodeType == wysihtml5.ELEMENT_NODE && node.getAttribute("contenteditable") != "false" && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssStyleMatch) {
9831 node = node.parentNode;
9836 getMatchingAncestor: function(node) {
9837 var ancestor = this.getAncestorWithClass(node),
9841 ancestor = this.getAncestorWithStyle(node);
9843 matchType = "style";
9846 if (this.cssStyle) {
9847 matchType = "class";
9852 "element": ancestor,
9857 // Normalizes nodes after applying a CSS class to a Range.
9858 postApply: function(textNodes, range) {
9859 var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
9861 var merges = [], currentMerge;
9863 var rangeStartNode = firstNode, rangeEndNode = lastNode;
9864 var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
9866 var textNode, precedingTextNode;
9868 for (var i = 0, len = textNodes.length; i < len; ++i) {
9869 textNode = textNodes[i];
9870 precedingTextNode = null;
9871 if (textNode && textNode.parentNode) {
9872 precedingTextNode = this.getAdjacentMergeableTextNode(textNode.parentNode, false);
9874 if (precedingTextNode) {
9875 if (!currentMerge) {
9876 currentMerge = new Merge(precedingTextNode);
9877 merges.push(currentMerge);
9879 currentMerge.textNodes.push(textNode);
9880 if (textNode === firstNode) {
9881 rangeStartNode = currentMerge.firstTextNode;
9882 rangeStartOffset = rangeStartNode.length;
9884 if (textNode === lastNode) {
9885 rangeEndNode = currentMerge.firstTextNode;
9886 rangeEndOffset = currentMerge.getLength();
9889 currentMerge = null;
9892 // Test whether the first node after the range needs merging
9893 if(lastNode && lastNode.parentNode) {
9894 var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);
9896 if (!currentMerge) {
9897 currentMerge = new Merge(lastNode);
9898 merges.push(currentMerge);
9900 currentMerge.textNodes.push(nextTextNode);
9904 if (merges.length) {
9905 for (i = 0, len = merges.length; i < len; ++i) {
9906 merges[i].doMerge();
9908 // Set the range boundaries
9909 range.setStart(rangeStartNode, rangeStartOffset);
9910 range.setEnd(rangeEndNode, rangeEndOffset);
9914 getAdjacentMergeableTextNode: function(node, forward) {
9915 var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
9916 var el = isTextNode ? node.parentNode : node;
9918 var propName = forward ? "nextSibling" : "previousSibling";
9920 // Can merge if the node's previous/next sibling is a text node
9921 adjacentNode = node[propName];
9922 if (adjacentNode && adjacentNode.nodeType == wysihtml5.TEXT_NODE) {
9923 return adjacentNode;
9926 // Compare element with its sibling
9927 adjacentNode = el[propName];
9928 if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {
9929 return adjacentNode[forward ? "firstChild" : "lastChild"];
9935 areElementsMergeable: function(el1, el2) {
9936 return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase())
9937 && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase())
9938 && hasSameClasses(el1, el2)
9939 && elementsHaveSameNonClassAttributes(el1, el2);
9942 createContainer: function(doc) {
9943 var el = doc.createElement(this.tagNames[0]);
9944 if (this.cssClass) {
9945 el.className = this.cssClass;
9947 if (this.cssStyle) {
9948 el.setAttribute('style', this.cssStyle);
9953 applyToTextNode: function(textNode) {
9954 var parent = textNode.parentNode;
9955 if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
9957 if (this.cssClass) {
9958 addClass(parent, this.cssClass, this.similarClassRegExp);
9960 if (this.cssStyle) {
9961 addStyle(parent, this.cssStyle, this.similarStyleRegExp);
9964 var el = this.createContainer(rangy.dom.getDocument(textNode));
9965 textNode.parentNode.insertBefore(el, textNode);
9966 el.appendChild(textNode);
9970 isRemovable: function(el) {
9971 return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) &&
9972 wysihtml5.lang.string(el.className).trim() === "" &&
9974 !el.getAttribute('style') ||
9975 wysihtml5.lang.string(el.getAttribute('style')).trim() === ""
9979 undoToTextNode: function(textNode, range, ancestorWithClass, ancestorWithStyle) {
9980 var styleMode = (ancestorWithClass) ? false : true,
9981 ancestor = ancestorWithClass || ancestorWithStyle,
9982 styleChanged = false;
9983 if (!range.containsNode(ancestor)) {
9984 // Split out the portion of the ancestor from which we can remove the CSS class
9985 var ancestorRange = range.cloneRange();
9986 ancestorRange.selectNode(ancestor);
9988 if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) {
9989 splitNodeAt(ancestor, range.endContainer, range.endOffset, this.container);
9990 range.setEndAfter(ancestor);
9992 if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {
9993 ancestor = splitNodeAt(ancestor, range.startContainer, range.startOffset, this.container);
9997 if (!styleMode && this.similarClassRegExp) {
9998 removeClass(ancestor, this.similarClassRegExp);
10001 if (styleMode && this.similarStyleRegExp) {
10002 styleChanged = (removeOrChangeStyle(ancestor, this.cssStyle, this.similarStyleRegExp) === "change");
10004 if (this.isRemovable(ancestor) && !styleChanged) {
10005 replaceWithOwnChildren(ancestor);
10009 applyToRange: function(range) {
10011 for (var ri = range.length; ri--;) {
10012 textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10014 if (!textNodes.length) {
10016 var node = this.createContainer(range[ri].endContainer.ownerDocument);
10017 range[ri].surroundContents(node);
10018 this.selectNode(range[ri], node);
10023 range[ri].splitBoundaries();
10024 textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10025 if (textNodes.length) {
10028 for (var i = 0, len = textNodes.length; i < len; ++i) {
10029 textNode = textNodes[i];
10030 if (!this.getMatchingAncestor(textNode).element) {
10031 this.applyToTextNode(textNode);
10035 range[ri].setStart(textNodes[0], 0);
10036 textNode = textNodes[textNodes.length - 1];
10037 range[ri].setEnd(textNode, textNode.length);
10039 if (this.normalize) {
10040 this.postApply(textNodes, range[ri]);
10047 undoToRange: function(range) {
10048 var textNodes, textNode, ancestorWithClass, ancestorWithStyle, ancestor;
10049 for (var ri = range.length; ri--;) {
10051 textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10052 if (textNodes.length) {
10053 range[ri].splitBoundaries();
10054 textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10056 var doc = range[ri].endContainer.ownerDocument,
10057 node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
10058 range[ri].insertNode(node);
10059 range[ri].selectNode(node);
10060 textNodes = [node];
10063 for (var i = 0, len = textNodes.length; i < len; ++i) {
10064 if (range[ri].isValid()) {
10065 textNode = textNodes[i];
10067 ancestor = this.getMatchingAncestor(textNode);
10068 if (ancestor.type === "style") {
10069 this.undoToTextNode(textNode, range[ri], false, ancestor.element);
10070 } else if (ancestor.element) {
10071 this.undoToTextNode(textNode, range[ri], ancestor.element);
10077 this.selectNode(range[ri], textNodes[0]);
10079 range[ri].setStart(textNodes[0], 0);
10080 textNode = textNodes[textNodes.length - 1];
10081 range[ri].setEnd(textNode, textNode.length);
10083 if (this.normalize) {
10084 this.postApply(textNodes, range[ri]);
10091 selectNode: function(range, node) {
10092 var isElement = node.nodeType === wysihtml5.ELEMENT_NODE,
10093 canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true,
10094 content = isElement ? node.innerHTML : node.data,
10095 isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE);
10097 if (isEmpty && isElement && canHaveHTML) {
10098 // Make sure that caret is visible in node by inserting a zero width no breaking space
10099 try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {}
10101 range.selectNodeContents(node);
10102 if (isEmpty && isElement) {
10103 range.collapse(false);
10104 } else if (isEmpty) {
10105 range.setStartAfter(node);
10106 range.setEndAfter(node);
10110 getTextSelectedByRange: function(textNode, range) {
10111 var textRange = range.cloneRange();
10112 textRange.selectNodeContents(textNode);
10114 var intersectionRange = textRange.intersection(range);
10115 var text = intersectionRange ? intersectionRange.toString() : "";
10116 textRange.detach();
10121 isAppliedToRange: function(range) {
10122 var ancestors = [],
10123 appliedType = "full",
10124 ancestor, styleAncestor, textNodes;
10126 for (var ri = range.length; ri--;) {
10128 textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10129 if (!textNodes.length) {
10130 ancestor = this.getMatchingAncestor(range[ri].startContainer).element;
10132 return (ancestor) ? {
10133 "elements": [ancestor],
10134 "coverage": appliedType
10138 for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) {
10139 selectedText = this.getTextSelectedByRange(textNodes[i], range[ri]);
10140 ancestor = this.getMatchingAncestor(textNodes[i]).element;
10141 if (ancestor && selectedText != "") {
10142 ancestors.push(ancestor);
10144 if (wysihtml5.dom.getTextNodes(ancestor, true).length === 1) {
10145 appliedType = "full";
10146 } else if (appliedType === "full") {
10147 appliedType = "inline";
10149 } else if (!ancestor) {
10150 appliedType = "partial";
10156 return (ancestors.length) ? {
10157 "elements": ancestors,
10158 "coverage": appliedType
10162 toggleRange: function(range) {
10163 var isApplied = this.isAppliedToRange(range),
10167 if (isApplied.coverage === "full") {
10168 this.undoToRange(range);
10169 } else if (isApplied.coverage === "inline") {
10170 parentsExactMatch = areMatchingAllready(isApplied.elements, this.tagNames, this.cssStyle, this.cssClass);
10171 this.undoToRange(range);
10172 if (!parentsExactMatch) {
10173 this.applyToRange(range);
10177 if (!areMatchingAllready(isApplied.elements, this.tagNames, this.cssStyle, this.cssClass)) {
10178 this.undoToRange(range);
10180 this.applyToRange(range);
10183 this.applyToRange(range);
10188 wysihtml5.selection.HTMLApplier = HTMLApplier;
10190 })(wysihtml5, rangy);
10192 * Rich Text Query/Formatting Commands
10195 * var commands = new wysihtml5.Commands(editor);
10197 wysihtml5.Commands = Base.extend(
10198 /** @scope wysihtml5.Commands.prototype */ {
10199 constructor: function(editor) {
10200 this.editor = editor;
10201 this.composer = editor.composer;
10202 this.doc = this.composer.doc;
10206 * Check whether the browser supports the given command
10208 * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
10210 * commands.supports("createLink");
10212 support: function(command) {
10213 return wysihtml5.browser.supportsCommand(this.doc, command);
10217 * Check whether the browser supports the given command
10219 * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")
10220 * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)
10222 * commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
10224 exec: function(command, value) {
10225 var obj = wysihtml5.commands[command],
10226 args = wysihtml5.lang.array(arguments).get(),
10227 method = obj && obj.exec,
10230 this.editor.fire("beforecommand:composer");
10233 args.unshift(this.composer);
10234 result = method.apply(obj, args);
10237 // try/catch for buggy firefox
10238 result = this.doc.execCommand(command, false, value);
10242 this.editor.fire("aftercommand:composer");
10247 * Check whether the current command is active
10248 * If the caret is within a bold text, then calling this with command "bold" should return true
10250 * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
10251 * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)
10252 * @return {Boolean} Whether the command is active
10254 * var isCurrentSelectionBold = commands.state("bold");
10256 state: function(command, commandValue) {
10257 var obj = wysihtml5.commands[command],
10258 args = wysihtml5.lang.array(arguments).get(),
10259 method = obj && obj.state;
10261 args.unshift(this.composer);
10262 return method.apply(obj, args);
10265 // try/catch for buggy firefox
10266 return this.doc.queryCommandState(command);
10273 /* Get command state parsed value if command has stateValue parsing function */
10274 stateValue: function(command) {
10275 var obj = wysihtml5.commands[command],
10276 args = wysihtml5.lang.array(arguments).get(),
10277 method = obj && obj.stateValue;
10279 args.unshift(this.composer);
10280 return method.apply(obj, args);
10286 ;wysihtml5.commands.bold = {
10287 exec: function(composer, command) {
10288 wysihtml5.commands.formatInline.execWithToggle(composer, command, "b");
10291 state: function(composer, command) {
10292 // element.ownerDocument.queryCommandState("bold") results:
10293 // firefox: only <b>
10294 // chrome: <b>, <strong>, <h1>, <h2>, ...
10295 // ie: <b>, <strong>
10296 // opera: <b>, <strong>
10297 return wysihtml5.commands.formatInline.state(composer, command, "b");
10301 ;(function(wysihtml5) {
10304 dom = wysihtml5.dom;
10306 function _format(composer, attributes) {
10307 var doc = composer.doc,
10308 tempClass = "_wysihtml5-temp-" + (+new Date()),
10309 tempClassRegExp = /non-matching-class/g,
10316 elementToSetCaretAfter,
10320 wysihtml5.commands.formatInline.exec(composer, undef, NODE_NAME, tempClass, tempClassRegExp, undef, undef, true, true);
10321 anchors = doc.querySelectorAll(NODE_NAME + "." + tempClass);
10322 length = anchors.length;
10323 for (; i<length; i++) {
10324 anchor = anchors[i];
10325 anchor.removeAttribute("class");
10326 for (j in attributes) {
10327 // Do not set attribute "text" as it is meant for setting string value if created link has no textual data
10328 if (j !== "text") {
10329 anchor.setAttribute(j, attributes[j]);
10334 elementToSetCaretAfter = anchor;
10335 if (length === 1) {
10336 textContent = dom.getTextContent(anchor);
10337 hasElementChild = !!anchor.querySelector("*");
10338 isEmpty = textContent === "" || textContent === wysihtml5.INVISIBLE_SPACE;
10339 if (!hasElementChild && isEmpty) {
10340 dom.setTextContent(anchor, attributes.text || anchor.href);
10341 whiteSpace = doc.createTextNode(" ");
10342 composer.selection.setAfter(anchor);
10343 dom.insert(whiteSpace).after(anchor);
10344 elementToSetCaretAfter = whiteSpace;
10347 composer.selection.setAfter(elementToSetCaretAfter);
10350 // Changes attributes of links
10351 function _changeLinks(composer, anchors, attributes) {
10353 for (var a = anchors.length; a--;) {
10355 // Remove all old attributes
10356 oldAttrs = anchors[a].attributes;
10357 for (var oa = oldAttrs.length; oa--;) {
10358 anchors[a].removeAttribute(oldAttrs.item(oa).name);
10361 // Set new attributes
10362 for (var j in attributes) {
10363 if (attributes.hasOwnProperty(j)) {
10364 anchors[a].setAttribute(j, attributes[j]);
10371 wysihtml5.commands.createLink = {
10373 * TODO: Use HTMLApplier or formatInline here
10375 * Turns selection into a link
10376 * If selection is already a link, it just changes the attributes
10380 * wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de");
10382 * wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" });
10384 exec: function(composer, command, value) {
10385 var anchors = this.state(composer, command);
10387 // Selection contains links then change attributes of these links
10388 composer.selection.executeAndRestore(function() {
10389 _changeLinks(composer, anchors, value);
10393 value = typeof(value) === "object" ? value : { href: value };
10394 _format(composer, value);
10398 state: function(composer, command) {
10399 return wysihtml5.commands.formatInline.state(composer, command, "A");
10403 ;(function(wysihtml5) {
10404 var dom = wysihtml5.dom;
10406 function _removeFormat(composer, anchors) {
10407 var length = anchors.length,
10412 for (; i<length; i++) {
10413 anchor = anchors[i];
10414 codeElement = dom.getParentElement(anchor, { nodeName: "code" });
10415 textContent = dom.getTextContent(anchor);
10417 // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking
10418 // else replace <a> with its childNodes
10419 if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) {
10420 // <code> element is used to prevent later auto-linking of the content
10421 codeElement = dom.renameElement(anchor, "code");
10423 dom.replaceWithChildNodes(anchor);
10428 wysihtml5.commands.removeLink = {
10430 * If selection is a link, it removes the link and wraps it with a <code> element
10431 * The <code> element is needed to avoid auto linking
10434 * wysihtml5.commands.createLink.exec(composer, "removeLink");
10437 exec: function(composer, command) {
10438 var anchors = this.state(composer, command);
10440 composer.selection.executeAndRestore(function() {
10441 _removeFormat(composer, anchors);
10446 state: function(composer, command) {
10447 return wysihtml5.commands.formatInline.state(composer, command, "A");
10452 * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags
10453 * which we don't want
10454 * Instead we set a css class
10456 (function(wysihtml5) {
10457 var REG_EXP = /wysiwyg-font-size-[0-9a-z\-]+/g;
10459 wysihtml5.commands.fontSize = {
10460 exec: function(composer, command, size) {
10461 wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
10464 state: function(composer, command, size) {
10465 return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
10469 ;/* In case font size adjustment to any number defined by user is preferred, we cannot use classes and must use inline styles. */
10470 (function(wysihtml5) {
10471 var REG_EXP = /(\s|^)font-size\s*:\s*[^;\s]+;?/gi;
10473 wysihtml5.commands.fontSizeStyle = {
10474 exec: function(composer, command, size) {
10475 size = (typeof(size) == "object") ? size.size : size;
10476 if (!(/^\s*$/).test(size)) {
10477 wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, "font-size:" + size, REG_EXP);
10481 state: function(composer, command, size) {
10482 return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "font-size", REG_EXP);
10485 stateValue: function(composer, command) {
10486 var st = this.state(composer, command),
10487 styleStr, fontsizeMatches,
10490 if (st && wysihtml5.lang.object(st).isArray()) {
10494 styleStr = st.getAttribute('style');
10496 return wysihtml5.quirks.styleParser.parseFontSize(styleStr);
10504 * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
10505 * which we don't want
10506 * Instead we set a css class
10508 (function(wysihtml5) {
10509 var REG_EXP = /wysiwyg-color-[0-9a-z]+/g;
10511 wysihtml5.commands.foreColor = {
10512 exec: function(composer, command, color) {
10513 wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
10516 state: function(composer, command, color) {
10517 return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
10522 * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags
10523 * which we don't want
10524 * Instead we set a css class
10526 (function(wysihtml5) {
10527 var REG_EXP = /(\s|^)color\s*:\s*[^;\s]+;?/gi;
10529 wysihtml5.commands.foreColorStyle = {
10530 exec: function(composer, command, color) {
10531 var colorVals = wysihtml5.quirks.styleParser.parseColor((typeof(color) == "object") ? "color:" + color.color : "color:" + color, "color"),
10535 colString = "color: rgb(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ');';
10536 if (colorVals[3] !== 1) {
10537 colString += "color: rgba(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ',' + colorVals[3] + ');';
10539 wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, colString, REG_EXP);
10543 state: function(composer, command) {
10544 return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "color", REG_EXP);
10547 stateValue: function(composer, command, props) {
10548 var st = this.state(composer, command),
10551 if (st && wysihtml5.lang.object(st).isArray()) {
10556 colorStr = st.getAttribute('style');
10559 val = wysihtml5.quirks.styleParser.parseColor(colorStr, "color");
10560 return wysihtml5.quirks.styleParser.unparseColor(val, props);
10569 ;/* In case background adjustment to any color defined by user is preferred, we cannot use classes and must use inline styles. */
10570 (function(wysihtml5) {
10571 var REG_EXP = /(\s|^)background-color\s*:\s*[^;\s]+;?/gi;
10573 wysihtml5.commands.bgColorStyle = {
10574 exec: function(composer, command, color) {
10575 var colorVals = wysihtml5.quirks.styleParser.parseColor((typeof(color) == "object") ? "background-color:" + color.color : "background-color:" + color, "background-color"),
10579 colString = "background-color: rgb(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ');';
10580 if (colorVals[3] !== 1) {
10581 colString += "background-color: rgba(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ',' + colorVals[3] + ');';
10583 wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, colString, REG_EXP);
10587 state: function(composer, command) {
10588 return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "background-color", REG_EXP);
10591 stateValue: function(composer, command, props) {
10592 var st = this.state(composer, command),
10596 if (st && wysihtml5.lang.object(st).isArray()) {
10601 colorStr = st.getAttribute('style');
10603 val = wysihtml5.quirks.styleParser.parseColor(colorStr, "background-color");
10604 return wysihtml5.quirks.styleParser.unparseColor(val, props);
10612 ;(function(wysihtml5) {
10613 var dom = wysihtml5.dom,
10614 // Following elements are grouped
10615 // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
10616 // instead of creating a H4 within a H1 which would result in semantically invalid html
10617 BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "PRE", "DIV"];
10620 * Remove similiar classes (based on classRegExp)
10621 * and add the desired class name
10623 function _addClass(element, className, classRegExp) {
10624 if (element.className) {
10625 _removeClass(element, classRegExp);
10626 element.className = wysihtml5.lang.string(element.className + " " + className).trim();
10628 element.className = className;
10632 function _addStyle(element, cssStyle, styleRegExp) {
10633 _removeStyle(element, styleRegExp);
10634 if (element.getAttribute('style')) {
10635 element.setAttribute('style', wysihtml5.lang.string(element.getAttribute('style') + " " + cssStyle).trim());
10637 element.setAttribute('style', cssStyle);
10641 function _removeClass(element, classRegExp) {
10642 var ret = classRegExp.test(element.className);
10643 element.className = element.className.replace(classRegExp, "");
10644 if (wysihtml5.lang.string(element.className).trim() == '') {
10645 element.removeAttribute('class');
10650 function _removeStyle(element, styleRegExp) {
10651 var ret = styleRegExp.test(element.getAttribute('style'));
10652 element.setAttribute('style', (element.getAttribute('style') || "").replace(styleRegExp, ""));
10653 if (wysihtml5.lang.string(element.getAttribute('style') || "").trim() == '') {
10654 element.removeAttribute('style');
10659 function _removeLastChildIfLineBreak(node) {
10660 var lastChild = node.lastChild;
10661 if (lastChild && _isLineBreak(lastChild)) {
10662 lastChild.parentNode.removeChild(lastChild);
10666 function _isLineBreak(node) {
10667 return node.nodeName === "BR";
10671 * Execute native query command
10672 * and if necessary modify the inserted node's className
10674 function _execCommand(doc, composer, command, nodeName, className) {
10675 var ranges = composer.selection.getOwnRanges();
10676 for (var i = ranges.length; i--;){
10677 composer.selection.getSelection().removeAllRanges();
10678 composer.selection.setSelection(ranges[i]);
10680 var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) {
10681 var target = event.target,
10683 if (target.nodeType !== wysihtml5.ELEMENT_NODE) {
10686 displayStyle = dom.getStyle("display").from(target);
10687 if (displayStyle.substr(0, 6) !== "inline") {
10688 // Make sure that only block elements receive the given class
10689 target.className += " " + className;
10693 doc.execCommand(command, false, nodeName);
10695 if (eventListener) {
10696 eventListener.stop();
10701 function _selectionWrap(composer, options) {
10702 if (composer.selection.isCollapsed()) {
10703 composer.selection.selectLine();
10706 var surroundedNodes = composer.selection.surround(options);
10707 for (var i = 0, imax = surroundedNodes.length; i < imax; i++) {
10708 wysihtml5.dom.lineBreaks(surroundedNodes[i]).remove();
10709 _removeLastChildIfLineBreak(surroundedNodes[i]);
10712 // rethink restoring selection
10713 // composer.selection.selectNode(element, wysihtml5.browser.displaysCaretInEmptyContentEditableCorrectly());
10716 function _hasClasses(element) {
10717 return !!wysihtml5.lang.string(element.className).trim();
10720 function _hasStyles(element) {
10721 return !!wysihtml5.lang.string(element.getAttribute('style') || '').trim();
10724 wysihtml5.commands.formatBlock = {
10725 exec: function(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp) {
10726 var doc = composer.doc,
10727 blockElements = this.state(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp),
10728 useLineBreaks = composer.config.useLineBreaks,
10729 defaultNodeName = useLineBreaks ? "DIV" : "P",
10730 selectedNodes, classRemoveAction, blockRenameFound, styleRemoveAction, blockElement;
10731 nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
10733 if (blockElements.length) {
10734 composer.selection.executeAndRestoreRangy(function() {
10735 for (var b = blockElements.length; b--;) {
10737 classRemoveAction = _removeClass(blockElements[b], classRegExp);
10740 styleRemoveAction = _removeStyle(blockElements[b], styleRegExp);
10743 if ((styleRemoveAction || classRemoveAction) && nodeName === null && blockElements[b].nodeName != defaultNodeName) {
10744 // dont rename or remove element when just setting block formating class or style
10748 var hasClasses = _hasClasses(blockElements[b]),
10749 hasStyles = _hasStyles(blockElements[b]);
10751 if (!hasClasses && !hasStyles && (useLineBreaks || nodeName === "P")) {
10752 // Insert a line break afterwards and beforewards when there are siblings
10753 // that are not of type line break or block element
10754 wysihtml5.dom.lineBreaks(blockElements[b]).add();
10755 dom.replaceWithChildNodes(blockElements[b]);
10757 // Make sure that styling is kept by renaming the element to a <div> or <p> and copying over the class name
10758 dom.renameElement(blockElements[b], nodeName === "P" ? "DIV" : defaultNodeName);
10766 // Find similiar block element and rename it (<h2 class="foo"></h2> => <h1 class="foo"></h1>)
10767 if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) {
10768 selectedNodes = composer.selection.findNodesInSelection(BLOCK_ELEMENTS_GROUP).concat(composer.selection.getSelectedOwnNodes());
10769 composer.selection.executeAndRestoreRangy(function() {
10770 for (var n = selectedNodes.length; n--;) {
10771 blockElement = dom.getParentElement(selectedNodes[n], {
10772 nodeName: BLOCK_ELEMENTS_GROUP
10774 if (blockElement == composer.element) {
10775 blockElement = null;
10777 if (blockElement) {
10778 // Rename current block element to new block element and add class
10780 blockElement = dom.renameElement(blockElement, nodeName);
10783 _addClass(blockElement, className, classRegExp);
10786 _addStyle(blockElement, cssStyle, styleRegExp);
10788 blockRenameFound = true;
10794 if (blockRenameFound) {
10799 _selectionWrap(composer, {
10800 "nodeName": (nodeName || defaultNodeName),
10801 "className": className || null,
10802 "cssStyle": cssStyle || null
10806 state: function(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp) {
10807 var nodes = composer.selection.getSelectedOwnNodes(),
10811 nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
10813 //var selectedNode = composer.selection.getSelectedNode();
10814 for (var i = 0, maxi = nodes.length; i < maxi; i++) {
10815 parent = dom.getParentElement(nodes[i], {
10816 nodeName: nodeName,
10817 className: className,
10818 classRegExp: classRegExp,
10819 cssStyle: cssStyle,
10820 styleRegExp: styleRegExp
10822 if (parent && wysihtml5.lang.array(parents).indexOf(parent) == -1) {
10823 parents.push(parent);
10826 if (parents.length == 0) {
10835 ;/* Formats block for as a <pre><code class="classname"></code></pre> block
10836 * Useful in conjuction for sytax highlight utility: highlight.js
10840 * editorInstance.composer.commands.exec("formatCode", "language-html");
10843 wysihtml5.commands.formatCode = {
10845 exec: function(composer, command, classname) {
10846 var pre = this.state(composer),
10847 code, range, selectedNodes;
10849 // caret is already within a <pre><code>...</code></pre>
10850 composer.selection.executeAndRestore(function() {
10851 code = pre.querySelector("code");
10852 wysihtml5.dom.replaceWithChildNodes(pre);
10854 wysihtml5.dom.replaceWithChildNodes(code);
10858 // Wrap in <pre><code>...</code></pre>
10859 range = composer.selection.getRange();
10860 selectedNodes = range.extractContents();
10861 pre = composer.doc.createElement("pre");
10862 code = composer.doc.createElement("code");
10865 code.className = classname;
10868 pre.appendChild(code);
10869 code.appendChild(selectedNodes);
10870 range.insertNode(pre);
10871 composer.selection.selectNode(pre);
10875 state: function(composer) {
10876 var selectedNode = composer.selection.getSelectedNode();
10877 if (selectedNode && selectedNode.nodeName && selectedNode.nodeName == "PRE"&&
10878 selectedNode.firstChild && selectedNode.firstChild.nodeName && selectedNode.firstChild.nodeName == "CODE") {
10879 return selectedNode;
10881 return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "CODE" }) && wysihtml5.dom.getParentElement(selectedNode, { nodeName: "PRE" });
10885 * formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
10887 * #1 caret in unformatted text:
10892 * #2 unformatted text selected:
10897 * #3 unformatted text selected across boundaries:
10898 * ab|c <span>defg|h</span>
10900 * ab<b>|c </b><span><b>defg</b>|h</span>
10902 * #4 formatted text entirely selected
10907 * #5 formatted text partially selected
10912 * #6 formatted text selected across boundaries
10913 * <span>ab|c</span> <b>de|fgh</b>
10915 * <span>ab|c</span> de|<b>fgh</b>
10917 (function(wysihtml5) {
10918 var // Treat <b> as <strong> and vice versa
10927 function _getTagNames(tagName) {
10928 var alias = ALIAS_MAPPING[tagName];
10929 return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];
10932 function _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, container) {
10933 var identifier = tagName;
10936 identifier += ":" + className;
10939 identifier += ":" + cssStyle;
10942 if (!htmlApplier[identifier]) {
10943 htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true, cssStyle, styleRegExp, container);
10946 return htmlApplier[identifier];
10949 wysihtml5.commands.formatInline = {
10950 exec: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, dontRestoreSelect, noCleanup) {
10951 var range = composer.selection.createRange(),
10952 ownRanges = composer.selection.getOwnRanges();
10954 if (!ownRanges || ownRanges.length == 0) {
10957 composer.selection.getSelection().removeAllRanges();
10959 _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, composer.element).toggleRange(ownRanges);
10961 if (!dontRestoreSelect) {
10962 range.setStart(ownRanges[0].startContainer, ownRanges[0].startOffset);
10964 ownRanges[ownRanges.length - 1].endContainer,
10965 ownRanges[ownRanges.length - 1].endOffset
10967 composer.selection.setSelection(range);
10968 composer.selection.executeAndRestore(function() {
10970 composer.cleanUp();
10973 } else if (!noCleanup) {
10974 composer.cleanUp();
10978 // Executes so that if collapsed caret is in a state and executing that state it should unformat that state
10979 // It is achieved by selecting the entire state element before executing.
10980 // This works on built in contenteditable inline format commands
10981 execWithToggle: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) {
10984 if (this.state(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) &&
10985 composer.selection.isCollapsed() &&
10986 !composer.selection.caretIsLastInSelection() &&
10987 !composer.selection.caretIsFirstInSelection()
10989 var state_element = that.state(composer, command, tagName, className, classRegExp)[0];
10990 composer.selection.executeAndRestoreRangy(function() {
10991 var parent = state_element.parentNode;
10992 composer.selection.selectNode(state_element, true);
10993 wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, true, true);
10996 if (this.state(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) && !composer.selection.isCollapsed()) {
10997 composer.selection.executeAndRestoreRangy(function() {
10998 wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, true, true);
11001 wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp);
11006 state: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) {
11007 var doc = composer.doc,
11008 aliasTagName = ALIAS_MAPPING[tagName] || tagName,
11009 ownRanges, isApplied;
11011 // Check whether the document contains a node with the desired tagName
11012 if (!wysihtml5.dom.hasElementWithTagName(doc, tagName) &&
11013 !wysihtml5.dom.hasElementWithTagName(doc, aliasTagName)) {
11017 // Check whether the document contains a node with the desired className
11018 if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {
11022 ownRanges = composer.selection.getOwnRanges();
11024 if (!ownRanges || ownRanges.length === 0) {
11028 isApplied = _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, composer.element).isAppliedToRange(ownRanges);
11030 return (isApplied && isApplied.elements) ? isApplied.elements : false;
11034 ;(function(wysihtml5) {
11036 wysihtml5.commands.insertBlockQuote = {
11037 exec: function(composer, command) {
11038 var state = this.state(composer, command),
11039 endToEndParent = composer.selection.isEndToEndInNode(['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P']),
11040 prevNode, nextNode;
11042 composer.selection.executeAndRestore(function() {
11044 if (composer.config.useLineBreaks) {
11045 wysihtml5.dom.lineBreaks(state).add();
11047 wysihtml5.dom.unwrap(state);
11049 if (composer.selection.isCollapsed()) {
11050 composer.selection.selectLine();
11053 if (endToEndParent) {
11054 var qouteEl = endToEndParent.ownerDocument.createElement('blockquote');
11055 wysihtml5.dom.insert(qouteEl).after(endToEndParent);
11056 qouteEl.appendChild(endToEndParent);
11058 composer.selection.surround({nodeName: "blockquote"});
11063 state: function(composer, command) {
11064 var selectedNode = composer.selection.getSelectedNode(),
11065 node = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "BLOCKQUOTE" }, false, composer.element);
11067 return (node) ? node : false;
11071 })(wysihtml5);;wysihtml5.commands.insertHTML = {
11072 exec: function(composer, command, html) {
11073 if (composer.commands.support(command)) {
11074 composer.doc.execCommand(command, false, html);
11076 composer.selection.insertHTML(html);
11080 state: function() {
11084 ;(function(wysihtml5) {
11085 var NODE_NAME = "IMG";
11087 wysihtml5.commands.insertImage = {
11090 * If selection is already an image link, it removes it
11094 * wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg");
11096 * wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" });
11098 exec: function(composer, command, value) {
11099 value = typeof(value) === "object" ? value : { src: value };
11101 var doc = composer.doc,
11102 image = this.state(composer),
11107 // Image already selected, set the caret before it and delete it
11108 composer.selection.setBefore(image);
11109 parent = image.parentNode;
11110 parent.removeChild(image);
11112 // and it's parent <a> too if it hasn't got any other relevant child nodes
11113 wysihtml5.dom.removeEmptyTextNodes(parent);
11114 if (parent.nodeName === "A" && !parent.firstChild) {
11115 composer.selection.setAfter(parent);
11116 parent.parentNode.removeChild(parent);
11119 // firefox and ie sometimes don't remove the image handles, even though the image got removed
11120 wysihtml5.quirks.redraw(composer.element);
11124 image = doc.createElement(NODE_NAME);
11126 for (var i in value) {
11127 image.setAttribute(i === "className" ? "class" : i, value[i]);
11130 composer.selection.insertNode(image);
11131 if (wysihtml5.browser.hasProblemsSettingCaretAfterImg()) {
11132 textNode = doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
11133 composer.selection.insertNode(textNode);
11134 composer.selection.setAfter(textNode);
11136 composer.selection.setAfter(image);
11140 state: function(composer) {
11141 var doc = composer.doc,
11146 if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) {
11150 selectedNode = composer.selection.getSelectedNode();
11151 if (!selectedNode) {
11155 if (selectedNode.nodeName === NODE_NAME) {
11156 // This works perfectly in IE
11157 return selectedNode;
11160 if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {
11164 text = composer.selection.getText();
11165 text = wysihtml5.lang.string(text).trim();
11170 imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) {
11171 return node.nodeName === "IMG";
11174 if (imagesInSelection.length !== 1) {
11178 return imagesInSelection[0];
11182 ;(function(wysihtml5) {
11183 var LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : "");
11185 wysihtml5.commands.insertLineBreak = {
11186 exec: function(composer, command) {
11187 if (composer.commands.support(command)) {
11188 composer.doc.execCommand(command, false, null);
11189 if (!wysihtml5.browser.autoScrollsToCaret()) {
11190 composer.selection.scrollIntoView();
11193 composer.commands.exec("insertHTML", LINE_BREAK);
11197 state: function() {
11202 ;wysihtml5.commands.insertOrderedList = {
11203 exec: function(composer, command) {
11204 wysihtml5.commands.insertList.exec(composer, command, "OL");
11207 state: function(composer, command) {
11208 return wysihtml5.commands.insertList.state(composer, command, "OL");
11211 ;wysihtml5.commands.insertUnorderedList = {
11212 exec: function(composer, command) {
11213 wysihtml5.commands.insertList.exec(composer, command, "UL");
11216 state: function(composer, command) {
11217 return wysihtml5.commands.insertList.state(composer, command, "UL");
11220 ;wysihtml5.commands.insertList = (function(wysihtml5) {
11222 var isNode = function(node, name) {
11223 if (node && node.nodeName) {
11224 if (typeof name === 'string') {
11227 for (var n = name.length; n--;) {
11228 if (node.nodeName === name[n]) {
11236 var findListEl = function(node, nodeName, composer) {
11243 var parentLi = wysihtml5.dom.getParentElement(node, { nodeName: "LI" }),
11244 otherNodeName = (nodeName === "UL") ? "OL" : "UL";
11246 if (isNode(node, nodeName)) {
11248 } else if (isNode(node, otherNodeName)) {
11253 } else if (parentLi) {
11254 if (isNode(parentLi.parentNode, nodeName)) {
11255 ret.el = parentLi.parentNode;
11256 } else if (isNode(parentLi.parentNode, otherNodeName)) {
11258 el : parentLi.parentNode,
11265 // do not count list elements outside of composer
11266 if (ret.el && !composer.element.contains(ret.el)) {
11273 var handleSameTypeList = function(el, nodeName, composer) {
11274 var otherNodeName = (nodeName === "UL") ? "OL" : "UL",
11275 otherLists, innerLists;
11277 // <ul><li>foo</li><li>bar</li></ul>
11280 composer.selection.executeAndRestore(function() {
11281 var otherLists = getListsInSelection(otherNodeName, composer);
11282 if (otherLists.length) {
11283 for (var l = otherLists.length; l--;) {
11284 wysihtml5.dom.renameElement(otherLists[l], nodeName.toLowerCase());
11287 innerLists = getListsInSelection(['OL', 'UL'], composer);
11288 for (var i = innerLists.length; i--;) {
11289 wysihtml5.dom.resolveList(innerLists[i], composer.config.useLineBreaks);
11291 wysihtml5.dom.resolveList(el, composer.config.useLineBreaks);
11296 var handleOtherTypeList = function(el, nodeName, composer) {
11297 var otherNodeName = (nodeName === "UL") ? "OL" : "UL";
11298 // Turn an ordered list into an unordered list
11299 // <ol><li>foo</li><li>bar</li></ol>
11301 // <ul><li>foo</li><li>bar</li></ul>
11302 // Also rename other lists in selection
11303 composer.selection.executeAndRestore(function() {
11304 var renameLists = [el].concat(getListsInSelection(otherNodeName, composer));
11306 // All selection inner lists get renamed too
11307 for (var l = renameLists.length; l--;) {
11308 wysihtml5.dom.renameElement(renameLists[l], nodeName.toLowerCase());
11313 var getListsInSelection = function(nodeName, composer) {
11314 var ranges = composer.selection.getOwnRanges(),
11317 for (var r = ranges.length; r--;) {
11318 renameLists = renameLists.concat(ranges[r].getNodes([1], function(node) {
11319 return isNode(node, nodeName);
11323 return renameLists;
11326 var createListFallback = function(nodeName, composer) {
11327 // Fallback for Create list
11328 composer.selection.executeAndRestoreRangy(function() {
11329 var tempClassName = "_wysihtml5-temp-" + new Date().getTime(),
11330 tempElement = composer.selection.deblockAndSurround({
11332 "className": tempClassName
11336 // This space causes new lists to never break on enter
11337 var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g;
11338 tempElement.innerHTML = tempElement.innerHTML.replace(INVISIBLE_SPACE_REG_EXP, "");
11341 isEmpty = wysihtml5.lang.array(["", "<br>", wysihtml5.INVISIBLE_SPACE]).contains(tempElement.innerHTML);
11342 list = wysihtml5.dom.convertToList(tempElement, nodeName.toLowerCase(), composer.parent.config.uneditableContainerClassname);
11344 composer.selection.selectNode(list.querySelector("li"), true);
11351 exec: function(composer, command, nodeName) {
11352 var doc = composer.doc,
11353 cmd = (nodeName === "OL") ? "insertOrderedList" : "insertUnorderedList",
11354 selectedNode = composer.selection.getSelectedNode(),
11355 list = findListEl(selectedNode, nodeName, composer);
11358 if (composer.commands.support(cmd)) {
11359 doc.execCommand(cmd, false, null);
11361 createListFallback(nodeName, composer);
11363 } else if (list.other) {
11364 handleOtherTypeList(list.el, nodeName, composer);
11366 handleSameTypeList(list.el, nodeName, composer);
11370 state: function(composer, command, nodeName) {
11371 var selectedNode = composer.selection.getSelectedNode(),
11372 list = findListEl(selectedNode, nodeName, composer);
11374 return (list.el && !list.other) ? list.el : false;
11378 })(wysihtml5);;wysihtml5.commands.italic = {
11379 exec: function(composer, command) {
11380 wysihtml5.commands.formatInline.execWithToggle(composer, command, "i");
11383 state: function(composer, command) {
11384 // element.ownerDocument.queryCommandState("italic") results:
11385 // firefox: only <i>
11386 // chrome: <i>, <em>, <blockquote>, ...
11389 return wysihtml5.commands.formatInline.state(composer, command, "i");
11392 ;(function(wysihtml5) {
11393 var CLASS_NAME = "wysiwyg-text-align-center",
11394 REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
11396 wysihtml5.commands.justifyCenter = {
11397 exec: function(composer, command) {
11398 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11401 state: function(composer, command) {
11402 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11406 ;(function(wysihtml5) {
11407 var CLASS_NAME = "wysiwyg-text-align-left",
11408 REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
11410 wysihtml5.commands.justifyLeft = {
11411 exec: function(composer, command) {
11412 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11415 state: function(composer, command) {
11416 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11420 ;(function(wysihtml5) {
11421 var CLASS_NAME = "wysiwyg-text-align-right",
11422 REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
11424 wysihtml5.commands.justifyRight = {
11425 exec: function(composer, command) {
11426 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11429 state: function(composer, command) {
11430 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11434 ;(function(wysihtml5) {
11435 var CLASS_NAME = "wysiwyg-text-align-justify",
11436 REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
11438 wysihtml5.commands.justifyFull = {
11439 exec: function(composer, command) {
11440 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11443 state: function(composer, command) {
11444 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11448 ;(function(wysihtml5) {
11449 var STYLE_STR = "text-align: right;",
11450 REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
11452 wysihtml5.commands.alignRightStyle = {
11453 exec: function(composer, command) {
11454 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11457 state: function(composer, command) {
11458 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11462 ;(function(wysihtml5) {
11463 var STYLE_STR = "text-align: left;",
11464 REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
11466 wysihtml5.commands.alignLeftStyle = {
11467 exec: function(composer, command) {
11468 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11471 state: function(composer, command) {
11472 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11476 ;(function(wysihtml5) {
11477 var STYLE_STR = "text-align: center;",
11478 REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
11480 wysihtml5.commands.alignCenterStyle = {
11481 exec: function(composer, command) {
11482 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11485 state: function(composer, command) {
11486 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11490 ;wysihtml5.commands.redo = {
11491 exec: function(composer) {
11492 return composer.undoManager.redo();
11495 state: function(composer) {
11499 ;wysihtml5.commands.underline = {
11500 exec: function(composer, command) {
11501 wysihtml5.commands.formatInline.execWithToggle(composer, command, "u");
11504 state: function(composer, command) {
11505 return wysihtml5.commands.formatInline.state(composer, command, "u");
11508 ;wysihtml5.commands.undo = {
11509 exec: function(composer) {
11510 return composer.undoManager.undo();
11513 state: function(composer) {
11517 ;wysihtml5.commands.createTable = {
11518 exec: function(composer, command, value) {
11519 var col, row, html;
11520 if (value && value.cols && value.rows && parseInt(value.cols, 10) > 0 && parseInt(value.rows, 10) > 0) {
11521 if (value.tableStyle) {
11522 html = "<table style=\"" + value.tableStyle + "\">";
11527 for (row = 0; row < value.rows; row ++) {
11529 for (col = 0; col < value.cols; col ++) {
11530 html += "<td> </td>";
11534 html += "</tbody></table>";
11535 composer.commands.exec("insertHTML", html);
11536 //composer.selection.insertHTML(html);
11542 state: function(composer, command) {
11546 ;wysihtml5.commands.mergeTableCells = {
11547 exec: function(composer, command) {
11548 if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) {
11549 if (this.state(composer, command)) {
11550 wysihtml5.dom.table.unmergeCell(composer.tableSelection.start);
11552 wysihtml5.dom.table.mergeCellsBetween(composer.tableSelection.start, composer.tableSelection.end);
11557 state: function(composer, command) {
11558 if (composer.tableSelection) {
11559 var start = composer.tableSelection.start,
11560 end = composer.tableSelection.end;
11561 if (start && end && start == end &&
11563 wysihtml5.dom.getAttribute(start, "colspan") &&
11564 parseInt(wysihtml5.dom.getAttribute(start, "colspan"), 10) > 1
11566 wysihtml5.dom.getAttribute(start, "rowspan") &&
11567 parseInt(wysihtml5.dom.getAttribute(start, "rowspan"), 10) > 1
11576 ;wysihtml5.commands.addTableCells = {
11577 exec: function(composer, command, value) {
11578 if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) {
11580 // switches start and end if start is bigger than end (reverse selection)
11581 var tableSelect = wysihtml5.dom.table.orderSelectionEnds(composer.tableSelection.start, composer.tableSelection.end);
11582 if (value == "before" || value == "above") {
11583 wysihtml5.dom.table.addCells(tableSelect.start, value);
11584 } else if (value == "after" || value == "below") {
11585 wysihtml5.dom.table.addCells(tableSelect.end, value);
11587 setTimeout(function() {
11588 composer.tableSelection.select(tableSelect.start, tableSelect.end);
11593 state: function(composer, command) {
11597 ;wysihtml5.commands.deleteTableCells = {
11598 exec: function(composer, command, value) {
11599 if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) {
11600 var tableSelect = wysihtml5.dom.table.orderSelectionEnds(composer.tableSelection.start, composer.tableSelection.end),
11601 idx = wysihtml5.dom.table.indexOf(tableSelect.start),
11603 table = composer.tableSelection.table;
11605 wysihtml5.dom.table.removeCells(tableSelect.start, value);
11606 setTimeout(function() {
11607 // move selection to next or previous if not present
11608 selCell = wysihtml5.dom.table.findCell(table, idx);
11611 if (value == "row") {
11612 selCell = wysihtml5.dom.table.findCell(table, {
11613 "row": idx.row - 1,
11618 if (value == "column") {
11619 selCell = wysihtml5.dom.table.findCell(table, {
11626 composer.tableSelection.select(selCell, selCell);
11633 state: function(composer, command) {
11637 ;wysihtml5.commands.indentList = {
11638 exec: function(composer, command, value) {
11639 var listEls = composer.selection.getSelectionParentsByTag('LI');
11641 return this.tryToPushLiLevel(listEls, composer.selection);
11646 state: function(composer, command) {
11650 tryToPushLiLevel: function(liNodes, selection) {
11651 var listTag, list, prevLi, liNode, prevLiList,
11654 selection.executeAndRestoreRangy(function() {
11656 for (var i = liNodes.length; i--;) {
11657 liNode = liNodes[i];
11658 listTag = (liNode.parentNode.nodeName === 'OL') ? 'OL' : 'UL';
11659 list = liNode.ownerDocument.createElement(listTag);
11660 prevLi = wysihtml5.dom.domNode(liNode).prev({nodeTypes: [wysihtml5.ELEMENT_NODE]});
11661 prevLiList = (prevLi) ? prevLi.querySelector('ul, ol') : null;
11665 prevLiList.appendChild(liNode);
11667 list.appendChild(liNode);
11668 prevLi.appendChild(list);
11678 ;wysihtml5.commands.outdentList = {
11679 exec: function(composer, command, value) {
11680 var listEls = composer.selection.getSelectionParentsByTag('LI');
11682 return this.tryToPullLiLevel(listEls, composer);
11687 state: function(composer, command) {
11691 tryToPullLiLevel: function(liNodes, composer) {
11692 var listNode, outerListNode, outerLiNode, list, prevLi, liNode, afterList,
11696 composer.selection.executeAndRestoreRangy(function() {
11698 for (var i = liNodes.length; i--;) {
11699 liNode = liNodes[i];
11700 if (liNode.parentNode) {
11701 listNode = liNode.parentNode;
11703 if (listNode.tagName === 'OL' || listNode.tagName === 'UL') {
11706 outerListNode = wysihtml5.dom.getParentElement(listNode.parentNode, { nodeName: ['OL', 'UL']}, false, composer.element);
11707 outerLiNode = wysihtml5.dom.getParentElement(listNode.parentNode, { nodeName: ['LI']}, false, composer.element);
11709 if (outerListNode && outerLiNode) {
11711 if (liNode.nextSibling) {
11712 afterList = that.getAfterList(listNode, liNode);
11713 liNode.appendChild(afterList);
11715 outerListNode.insertBefore(liNode, outerLiNode.nextSibling);
11719 if (liNode.nextSibling) {
11720 afterList = that.getAfterList(listNode, liNode);
11721 liNode.appendChild(afterList);
11724 for (var j = liNode.childNodes.length; j--;) {
11725 listNode.parentNode.insertBefore(liNode.childNodes[j], listNode.nextSibling);
11728 listNode.parentNode.insertBefore(document.createElement('br'), listNode.nextSibling);
11729 liNode.parentNode.removeChild(liNode);
11734 if (listNode.childNodes.length === 0) {
11735 listNode.parentNode.removeChild(listNode);
11745 getAfterList: function(listNode, liNode) {
11746 var nodeName = listNode.nodeName,
11747 newList = document.createElement(nodeName);
11749 while (liNode.nextSibling) {
11750 newList.appendChild(liNode.nextSibling);
11756 * Undo Manager for wysihtml5
11757 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
11759 (function(wysihtml5) {
11764 MAX_HISTORY_ENTRIES = 25,
11765 DATA_ATTR_NODE = "data-wysihtml5-selection-node",
11766 DATA_ATTR_OFFSET = "data-wysihtml5-selection-offset",
11767 UNDO_HTML = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
11768 REDO_HTML = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>',
11769 dom = wysihtml5.dom;
11771 function cleanTempElements(doc) {
11773 while (tempElement = doc.querySelector("._wysihtml5-temp")) {
11774 tempElement.parentNode.removeChild(tempElement);
11778 wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend(
11779 /** @scope wysihtml5.UndoManager.prototype */ {
11780 constructor: function(editor) {
11781 this.editor = editor;
11782 this.composer = editor.composer;
11783 this.element = this.composer.element;
11786 this.historyStr = [];
11787 this.historyDom = [];
11794 _observe: function() {
11796 doc = this.composer.sandbox.getDocument(),
11799 // Catch CTRL+Z and CTRL+Y
11800 dom.observe(this.element, "keydown", function(event) {
11801 if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
11805 var keyCode = event.keyCode,
11806 isUndo = keyCode === Z_KEY && !event.shiftKey,
11807 isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
11811 event.preventDefault();
11812 } else if (isRedo) {
11814 event.preventDefault();
11818 // Catch delete and backspace
11819 dom.observe(this.element, "keydown", function(event) {
11820 var keyCode = event.keyCode;
11821 if (keyCode === lastKey) {
11827 if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
11833 .on("newword:composer", function() {
11837 .on("beforecommand:composer", function() {
11842 transact: function() {
11843 var previousHtml = this.historyStr[this.position - 1],
11844 currentHtml = this.composer.getValue(false, false),
11845 composerIsVisible = this.element.offsetWidth > 0 && this.element.offsetHeight > 0,
11846 range, node, offset, element, position;
11848 if (currentHtml === previousHtml) {
11852 var length = this.historyStr.length = this.historyDom.length = this.position;
11853 if (length > MAX_HISTORY_ENTRIES) {
11854 this.historyStr.shift();
11855 this.historyDom.shift();
11861 if (composerIsVisible) {
11862 // Do not start saving selection if composer is not visible
11863 range = this.composer.selection.getRange();
11864 node = (range && range.startContainer) ? range.startContainer : this.element;
11865 offset = (range && range.startOffset) ? range.startOffset : 0;
11867 if (node.nodeType === wysihtml5.ELEMENT_NODE) {
11870 element = node.parentNode;
11871 position = this.getChildNodeIndex(element, node);
11874 element.setAttribute(DATA_ATTR_OFFSET, offset);
11875 if (typeof(position) !== "undefined") {
11876 element.setAttribute(DATA_ATTR_NODE, position);
11880 var clone = this.element.cloneNode(!!currentHtml);
11881 this.historyDom.push(clone);
11882 this.historyStr.push(currentHtml);
11885 element.removeAttribute(DATA_ATTR_OFFSET);
11886 element.removeAttribute(DATA_ATTR_NODE);
11894 if (!this.undoPossible()) {
11898 this.set(this.historyDom[--this.position - 1]);
11899 this.editor.fire("undo:composer");
11903 if (!this.redoPossible()) {
11907 this.set(this.historyDom[++this.position - 1]);
11908 this.editor.fire("redo:composer");
11911 undoPossible: function() {
11912 return this.position > 1;
11915 redoPossible: function() {
11916 return this.position < this.historyStr.length;
11919 set: function(historyEntry) {
11920 this.element.innerHTML = "";
11923 childNodes = historyEntry.childNodes,
11924 length = historyEntry.childNodes.length;
11926 for (; i<length; i++) {
11927 this.element.appendChild(childNodes[i].cloneNode(true));
11930 // Restore selection
11935 if (historyEntry.hasAttribute(DATA_ATTR_OFFSET)) {
11936 offset = historyEntry.getAttribute(DATA_ATTR_OFFSET);
11937 position = historyEntry.getAttribute(DATA_ATTR_NODE);
11938 node = this.element;
11940 node = this.element.querySelector("[" + DATA_ATTR_OFFSET + "]") || this.element;
11941 offset = node.getAttribute(DATA_ATTR_OFFSET);
11942 position = node.getAttribute(DATA_ATTR_NODE);
11943 node.removeAttribute(DATA_ATTR_OFFSET);
11944 node.removeAttribute(DATA_ATTR_NODE);
11947 if (position !== null) {
11948 node = this.getChildNodeByIndex(node, +position);
11951 this.composer.selection.set(node, offset);
11954 getChildNodeIndex: function(parent, child) {
11956 childNodes = parent.childNodes,
11957 length = childNodes.length;
11958 for (; i<length; i++) {
11959 if (childNodes[i] === child) {
11965 getChildNodeByIndex: function(parent, index) {
11966 return parent.childNodes[index];
11971 * TODO: the following methods still need unit test coverage
11973 wysihtml5.views.View = Base.extend(
11974 /** @scope wysihtml5.views.View.prototype */ {
11975 constructor: function(parent, textareaElement, config) {
11976 this.parent = parent;
11977 this.element = textareaElement;
11978 this.config = config;
11979 if (!this.config.noTextarea) {
11980 this._observeViewChange();
11984 _observeViewChange: function() {
11986 this.parent.on("beforeload", function() {
11987 that.parent.on("change_view", function(view) {
11988 if (view === that.name) {
11989 that.parent.currentView = that;
11991 // Using tiny delay here to make sure that the placeholder is set before focusing
11992 setTimeout(function() { that.focus(); }, 0);
12000 focus: function() {
12001 if (this.element.ownerDocument.querySelector(":focus") === this.element) {
12005 try { this.element.focus(); } catch(e) {}
12009 this.element.style.display = "none";
12013 this.element.style.display = "";
12016 disable: function() {
12017 this.element.setAttribute("disabled", "disabled");
12020 enable: function() {
12021 this.element.removeAttribute("disabled");
12024 ;(function(wysihtml5) {
12025 var dom = wysihtml5.dom,
12026 browser = wysihtml5.browser;
12028 wysihtml5.views.Composer = wysihtml5.views.View.extend(
12029 /** @scope wysihtml5.views.Composer.prototype */ {
12032 // Needed for firefox in order to display a proper caret in an empty contentEditable
12033 CARET_HACK: "<br>",
12035 constructor: function(parent, editableElement, config) {
12036 this.base(parent, editableElement, config);
12037 if (!this.config.noTextarea) {
12038 this.textarea = this.parent.textarea;
12040 this.editableArea = editableElement;
12042 if (this.config.contentEditableMode) {
12043 this._initContentEditableArea();
12045 this._initSandbox();
12049 clear: function() {
12050 this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK;
12053 getValue: function(parse, clearInternals) {
12054 var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element);
12055 if (parse !== false) {
12056 value = this.parent.parse(value, (clearInternals === false) ? false : true);
12062 setValue: function(html, parse) {
12064 html = this.parent.parse(html);
12068 this.element.innerHTML = html;
12070 this.element.innerText = html;
12074 cleanUp: function() {
12075 this.parent.parse(this.element);
12079 this.editableArea.style.display = this._displayStyle || "";
12081 if (!this.config.noTextarea && !this.textarea.element.disabled) {
12082 // Firefox needs this, otherwise contentEditable becomes uneditable
12089 this._displayStyle = dom.getStyle("display").from(this.editableArea);
12090 if (this._displayStyle === "none") {
12091 this._displayStyle = null;
12093 this.editableArea.style.display = "none";
12096 disable: function() {
12097 this.parent.fire("disable:composer");
12098 this.element.removeAttribute("contentEditable");
12101 enable: function() {
12102 this.parent.fire("enable:composer");
12103 this.element.setAttribute("contentEditable", "true");
12106 focus: function(setToEnd) {
12107 // IE 8 fires the focus event after .focus()
12108 // This is needed by our simulate_placeholder.js to work
12109 // therefore we clear it ourselves this time
12110 if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
12116 var lastChild = this.element.lastChild;
12117 if (setToEnd && lastChild && this.selection) {
12118 if (lastChild.nodeName === "BR") {
12119 this.selection.setBefore(this.element.lastChild);
12121 this.selection.setAfter(this.element.lastChild);
12126 getTextContent: function() {
12127 return dom.getTextContent(this.element);
12130 hasPlaceholderSet: function() {
12131 return this.getTextContent() == ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")) && this.placeholderSet;
12134 isEmpty: function() {
12135 var innerHTML = this.element.innerHTML.toLowerCase();
12136 return (/^(\s|<br>|<\/br>|<p>|<\/p>)*$/i).test(innerHTML) ||
12137 innerHTML === "" ||
12138 innerHTML === "<br>" ||
12139 innerHTML === "<p></p>" ||
12140 innerHTML === "<p><br></p>" ||
12141 this.hasPlaceholderSet();
12144 _initContentEditableArea: function() {
12147 if (this.config.noTextarea) {
12148 this.sandbox = new dom.ContentEditableArea(function() {
12150 }, {}, this.editableArea);
12152 this.sandbox = new dom.ContentEditableArea(function() {
12155 this.editableArea = this.sandbox.getContentEditable();
12156 dom.insert(this.editableArea).after(this.textarea.element);
12157 this._createWysiwygFormField();
12161 _initSandbox: function() {
12164 this.sandbox = new dom.Sandbox(function() {
12167 stylesheets: this.config.stylesheets
12169 this.editableArea = this.sandbox.getIframe();
12171 var textareaElement = this.textarea.element;
12172 dom.insert(this.editableArea).after(textareaElement);
12174 this._createWysiwygFormField();
12177 // Creates hidden field which tells the server after submit, that the user used an wysiwyg editor
12178 _createWysiwygFormField: function() {
12179 if (this.textarea.element.form) {
12180 var hiddenField = document.createElement("input");
12181 hiddenField.type = "hidden";
12182 hiddenField.name = "_wysihtml5_mode";
12183 hiddenField.value = 1;
12184 dom.insert(hiddenField).after(this.textarea.element);
12188 _create: function() {
12190 this.doc = this.sandbox.getDocument();
12191 this.element = (this.config.contentEditableMode) ? this.sandbox.getContentEditable() : this.doc.body;
12192 if (!this.config.noTextarea) {
12193 this.textarea = this.parent.textarea;
12194 this.element.innerHTML = this.textarea.getValue(true, false);
12196 this.cleanUp(); // cleans contenteditable on initiation as it may contain html
12199 // Make sure our selection handler is ready
12200 this.selection = new wysihtml5.Selection(this.parent, this.element, this.config.uneditableContainerClassname);
12202 // Make sure commands dispatcher is ready
12203 this.commands = new wysihtml5.Commands(this.parent);
12205 if (!this.config.noTextarea) {
12206 dom.copyAttributes([
12207 "className", "spellcheck", "title", "lang", "dir", "accessKey"
12208 ]).from(this.textarea.element).to(this.element);
12211 dom.addClass(this.element, this.config.composerClassName);
12213 // Make the editor look like the original textarea, by syncing styles
12214 if (this.config.style && !this.config.contentEditableMode) {
12220 var name = this.config.name;
12222 dom.addClass(this.element, name);
12223 if (!this.config.contentEditableMode) { dom.addClass(this.editableArea, name); }
12228 if (!this.config.noTextarea && this.textarea.element.disabled) {
12232 // Simulate html5 placeholder attribute on contentEditable element
12233 var placeholderText = typeof(this.config.placeholder) === "string"
12234 ? this.config.placeholder
12235 : ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder"));
12236 if (placeholderText) {
12237 dom.simulatePlaceholder(this.parent, this, placeholderText);
12240 // Make sure that the browser avoids using inline styles whenever possible
12241 this.commands.exec("styleWithCSS", false);
12243 this._initAutoLinking();
12244 this._initObjectResizing();
12245 this._initUndoManager();
12246 this._initLineBreaking();
12248 // Simulate html5 autofocus on contentEditable element
12249 // This doesn't work on IOS (5.1.1)
12250 if (!this.config.noTextarea && (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) && !browser.isIos()) {
12251 setTimeout(function() { that.focus(true); }, 100);
12254 // IE sometimes leaves a single paragraph, which can't be removed by the user
12255 if (!browser.clearsContentEditableCorrectly()) {
12256 wysihtml5.quirks.ensureProperClearing(this);
12259 // Set up a sync that makes sure that textarea and editor have the same content
12260 if (this.initSync && this.config.sync) {
12264 // Okay hide the textarea, we are ready to go
12265 if (!this.config.noTextarea) { this.textarea.hide(); }
12267 // Fire global (before-)load event
12268 this.parent.fire("beforeload").fire("load");
12271 _initAutoLinking: function() {
12273 supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
12274 supportsAutoLinking = browser.doesAutoLinkingInContentEditable();
12275 if (supportsDisablingOfAutoLinking) {
12276 this.commands.exec("autoUrlDetect", false);
12279 if (!this.config.autoLink) {
12283 // Only do the auto linking by ourselves when the browser doesn't support auto linking
12284 // OR when he supports auto linking but we were able to turn it off (IE9+)
12285 if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
12286 this.parent.on("newword:composer", function() {
12287 if (dom.getTextContent(that.element).match(dom.autoLink.URL_REG_EXP)) {
12288 that.selection.executeAndRestore(function(startContainer, endContainer) {
12289 var uneditables = that.element.querySelectorAll("." + that.config.uneditableContainerClassname),
12290 isInUneditable = false;
12292 for (var i = uneditables.length; i--;) {
12293 if (wysihtml5.dom.contains(uneditables[i], endContainer)) {
12294 isInUneditable = true;
12298 if (!isInUneditable) dom.autoLink(endContainer.parentNode, [that.config.uneditableContainerClassname]);
12303 dom.observe(this.element, "blur", function() {
12304 dom.autoLink(that.element, [that.config.uneditableContainerClassname]);
12308 // Assuming we have the following:
12309 // <a href="http://www.google.de">http://www.google.de</a>
12310 // If a user now changes the url in the innerHTML we want to make sure that
12311 // it's synchronized with the href attribute (as long as the innerHTML is still a url)
12312 var // Use a live NodeList to check whether there are any links in the document
12313 links = this.sandbox.getDocument().getElementsByTagName("a"),
12314 // The autoLink helper method reveals a reg exp to detect correct urls
12315 urlRegExp = dom.autoLink.URL_REG_EXP,
12316 getTextContent = function(element) {
12317 var textContent = wysihtml5.lang.string(dom.getTextContent(element)).trim();
12318 if (textContent.substr(0, 4) === "www.") {
12319 textContent = "http://" + textContent;
12321 return textContent;
12324 dom.observe(this.element, "keydown", function(event) {
12325 if (!links.length) {
12329 var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
12330 link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4),
12337 textContent = getTextContent(link);
12338 // keydown is fired before the actual content is changed
12339 // therefore we set a timeout to change the href
12340 setTimeout(function() {
12341 var newTextContent = getTextContent(link);
12342 if (newTextContent === textContent) {
12346 // Only set href when new href looks like a valid url
12347 if (newTextContent.match(urlRegExp)) {
12348 link.setAttribute("href", newTextContent);
12354 _initObjectResizing: function() {
12355 this.commands.exec("enableObjectResizing", true);
12357 // IE sets inline styles after resizing objects
12358 // The following lines make sure that the width/height css properties
12359 // are copied over to the width/height attributes
12360 if (browser.supportsEvent("resizeend")) {
12361 var properties = ["width", "height"],
12362 propertiesLength = properties.length,
12363 element = this.element;
12365 dom.observe(element, "resizeend", function(event) {
12366 var target = event.target || event.srcElement,
12367 style = target.style,
12371 if (target.nodeName !== "IMG") {
12375 for (; i<propertiesLength; i++) {
12376 property = properties[i];
12377 if (style[property]) {
12378 target.setAttribute(property, parseInt(style[property], 10));
12379 style[property] = "";
12383 // After resizing IE sometimes forgets to remove the old resize handles
12384 wysihtml5.quirks.redraw(element);
12389 _initUndoManager: function() {
12390 this.undoManager = new wysihtml5.UndoManager(this.parent);
12393 _initLineBreaking: function() {
12395 USE_NATIVE_LINE_BREAK_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"],
12396 LIST_TAGS = ["UL", "OL", "MENU"];
12398 function adjust(selectedNode) {
12399 var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2);
12400 if (parentElement && dom.contains(that.element, parentElement)) {
12401 that.selection.executeAndRestore(function() {
12402 if (that.config.useLineBreaks) {
12403 dom.replaceWithChildNodes(parentElement);
12404 } else if (parentElement.nodeName !== "P") {
12405 dom.renameElement(parentElement, "p");
12411 if (!this.config.useLineBreaks) {
12412 dom.observe(this.element, ["focus", "keydown"], function() {
12413 if (that.isEmpty()) {
12414 var paragraph = that.doc.createElement("P");
12415 that.element.innerHTML = "";
12416 that.element.appendChild(paragraph);
12417 if (!browser.displaysCaretInEmptyContentEditableCorrectly()) {
12418 paragraph.innerHTML = "<br>";
12419 that.selection.setBefore(paragraph.firstChild);
12421 that.selection.selectNode(paragraph, true);
12427 // Under certain circumstances Chrome + Safari create nested <p> or <hX> tags after paste
12428 // Inserting an invisible white space in front of it fixes the issue
12429 // This is too hacky and causes selection not to replace content on paste in chrome
12430 /* if (browser.createsNestedInvalidMarkupAfterPaste()) {
12431 dom.observe(this.element, "paste", function(event) {
12432 var invisibleSpace = that.doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
12433 that.selection.insertNode(invisibleSpace);
12438 dom.observe(this.element, "keydown", function(event) {
12439 var keyCode = event.keyCode;
12441 if (event.shiftKey) {
12445 if (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY) {
12448 var blockElement = dom.getParentElement(that.selection.getSelectedNode(), { nodeName: USE_NATIVE_LINE_BREAK_INSIDE_TAGS }, 4);
12449 if (blockElement) {
12450 setTimeout(function() {
12451 // Unwrap paragraph after leaving a list or a H1-6
12452 var selectedNode = that.selection.getSelectedNode(),
12455 if (blockElement.nodeName === "LI") {
12456 if (!selectedNode) {
12460 list = dom.getParentElement(selectedNode, { nodeName: LIST_TAGS }, 2);
12463 adjust(selectedNode);
12467 if (keyCode === wysihtml5.ENTER_KEY && blockElement.nodeName.match(/^H[1-6]$/)) {
12468 adjust(selectedNode);
12474 if (that.config.useLineBreaks && keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) {
12475 event.preventDefault();
12476 that.commands.exec("insertLineBreak");
12483 ;(function(wysihtml5) {
12484 var dom = wysihtml5.dom,
12487 HOST_TEMPLATE = doc.createElement("div"),
12489 * Styles to copy from textarea to the composer element
12491 TEXT_FORMATTING = [
12492 "background-color",
12494 "font-family", "font-size", "font-style", "font-variant", "font-weight",
12495 "line-height", "letter-spacing",
12496 "text-align", "text-decoration", "text-indent", "text-rendering",
12497 "word-break", "word-wrap", "word-spacing"
12500 * Styles to copy from textarea to the iframe
12503 "background-color",
12505 "border-bottom-color", "border-bottom-style", "border-bottom-width",
12506 "border-left-color", "border-left-style", "border-left-width",
12507 "border-right-color", "border-right-style", "border-right-width",
12508 "border-top-color", "border-top-style", "border-top-width",
12509 "clear", "display", "float",
12510 "margin-bottom", "margin-left", "margin-right", "margin-top",
12511 "outline-color", "outline-offset", "outline-width", "outline-style",
12512 "padding-left", "padding-right", "padding-top", "padding-bottom",
12513 "position", "top", "left", "right", "bottom", "z-index",
12514 "vertical-align", "text-align",
12515 "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing",
12516 "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow",
12517 "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius",
12518 "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius",
12519 "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius",
12520 "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius",
12523 ADDITIONAL_CSS_RULES = [
12524 "html { height: 100%; }",
12525 "body { height: 100%; padding: 1px 0 0 0; margin: -1px 0 0 0; }",
12526 "body > p:first-child { margin-top: 0; }",
12527 "._wysihtml5-temp { display: none; }",
12528 wysihtml5.browser.isGecko ?
12529 "body.placeholder { color: graytext !important; }" :
12530 "body.placeholder { color: #a9a9a9 !important; }",
12531 // Ensure that user see's broken images and can delete them
12532 "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
12536 * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
12537 * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
12539 * Other browsers need a more hacky way: (pssst don't tell my mama)
12540 * In order to prevent the element being scrolled into view when focusing it, we simply
12541 * move it out of the scrollable area, focus it, and reset it's position
12543 var focusWithoutScrolling = function(element) {
12544 if (element.setActive) {
12545 // Following line could cause a js error when the textarea is invisible
12546 // See https://github.com/xing/wysihtml5/issues/9
12547 try { element.setActive(); } catch(e) {}
12549 var elementStyle = element.style,
12550 originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
12551 originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
12553 position: elementStyle.position,
12554 top: elementStyle.top,
12555 left: elementStyle.left,
12556 WebkitUserSelect: elementStyle.WebkitUserSelect
12560 position: "absolute",
12563 // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
12564 WebkitUserSelect: "none"
12569 dom.setStyles(originalStyles).on(element);
12571 if (win.scrollTo) {
12572 // Some browser extensions unset this method to prevent annoyances
12573 // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
12574 // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
12575 win.scrollTo(originalScrollLeft, originalScrollTop);
12581 wysihtml5.views.Composer.prototype.style = function() {
12583 originalActiveElement = doc.querySelector(":focus"),
12584 textareaElement = this.textarea.element,
12585 hasPlaceholder = textareaElement.hasAttribute("placeholder"),
12586 originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"),
12587 originalDisplayValue = textareaElement.style.display,
12588 originalDisabled = textareaElement.disabled,
12589 displayValueForCopying;
12591 this.focusStylesHost = HOST_TEMPLATE.cloneNode(false);
12592 this.blurStylesHost = HOST_TEMPLATE.cloneNode(false);
12593 this.disabledStylesHost = HOST_TEMPLATE.cloneNode(false);
12595 // Remove placeholder before copying (as the placeholder has an affect on the computed style)
12596 if (hasPlaceholder) {
12597 textareaElement.removeAttribute("placeholder");
12600 if (textareaElement === originalActiveElement) {
12601 textareaElement.blur();
12604 // enable for copying styles
12605 textareaElement.disabled = false;
12607 // set textarea to display="none" to get cascaded styles via getComputedStyle
12608 textareaElement.style.display = displayValueForCopying = "none";
12610 if ((textareaElement.getAttribute("rows") && dom.getStyle("height").from(textareaElement) === "auto") ||
12611 (textareaElement.getAttribute("cols") && dom.getStyle("width").from(textareaElement) === "auto")) {
12612 textareaElement.style.display = displayValueForCopying = originalDisplayValue;
12615 // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
12616 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.editableArea).andTo(this.blurStylesHost);
12618 // --------- editor styles ---------
12619 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
12621 // --------- apply standard rules ---------
12622 dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
12624 // --------- :disabled styles ---------
12625 textareaElement.disabled = true;
12626 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
12627 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
12628 textareaElement.disabled = originalDisabled;
12630 // --------- :focus styles ---------
12631 textareaElement.style.display = originalDisplayValue;
12632 focusWithoutScrolling(textareaElement);
12633 textareaElement.style.display = displayValueForCopying;
12635 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
12636 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
12639 textareaElement.style.display = originalDisplayValue;
12641 dom.copyStyles(["display"]).from(textareaElement).to(this.editableArea);
12643 // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
12644 // this is needed for when the change_view event is fired where the iframe is hidden and then
12645 // the blur event fires and re-displays it
12646 var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]);
12648 // --------- restore focus ---------
12649 if (originalActiveElement) {
12650 originalActiveElement.focus();
12652 textareaElement.blur();
12655 // --------- restore placeholder ---------
12656 if (hasPlaceholder) {
12657 textareaElement.setAttribute("placeholder", originalPlaceholder);
12660 // --------- Sync focus/blur styles ---------
12661 this.parent.on("focus:composer", function() {
12662 dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.editableArea);
12663 dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element);
12666 this.parent.on("blur:composer", function() {
12667 dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
12668 dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element);
12671 this.parent.observe("disable:composer", function() {
12672 dom.copyStyles(boxFormattingStyles) .from(that.disabledStylesHost).to(that.editableArea);
12673 dom.copyStyles(TEXT_FORMATTING) .from(that.disabledStylesHost).to(that.element);
12676 this.parent.observe("enable:composer", function() {
12677 dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
12678 dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element);
12685 * Taking care of events
12686 * - Simulating 'change' event on contentEditable element
12687 * - Handling drag & drop logic
12688 * - Catch paste events
12689 * - Dispatch proprietary newword:composer event
12690 * - Keyboard shortcuts
12692 (function(wysihtml5) {
12693 var dom = wysihtml5.dom,
12694 browser = wysihtml5.browser,
12696 * Map keyCodes to query commands
12700 "73": "italic", // I
12701 "85": "underline" // U
12704 var deleteAroundEditable = function(selection, uneditable, element) {
12705 // merge node with previous node from uneditable
12706 var prevNode = selection.getPreviousNode(uneditable, true),
12707 curNode = selection.getSelectedNode();
12709 if (curNode.nodeType !== 1 && curNode.parentNode !== element) { curNode = curNode.parentNode; }
12711 if (curNode.nodeType == 1) {
12712 var first = curNode.firstChild;
12714 if (prevNode.nodeType == 1) {
12715 while (curNode.firstChild) {
12716 prevNode.appendChild(curNode.firstChild);
12719 while (curNode.firstChild) {
12720 uneditable.parentNode.insertBefore(curNode.firstChild, uneditable);
12723 if (curNode.parentNode) {
12724 curNode.parentNode.removeChild(curNode);
12726 selection.setBefore(first);
12728 if (prevNode.nodeType == 1) {
12729 prevNode.appendChild(curNode);
12731 uneditable.parentNode.insertBefore(curNode, uneditable);
12733 selection.setBefore(curNode);
12738 var handleDeleteKeyPress = function(event, selection, element, composer) {
12739 if (selection.isCollapsed()) {
12740 if (selection.caretIsInTheBeginnig('LI')) {
12741 event.preventDefault();
12742 composer.commands.exec('outdentList');
12743 } else if (selection.caretIsInTheBeginnig()) {
12744 event.preventDefault();
12747 if (selection.caretIsFirstInSelection() &&
12748 selection.getPreviousNode() &&
12749 selection.getPreviousNode().nodeName &&
12750 (/^H\d$/gi).test(selection.getPreviousNode().nodeName)
12752 var prevNode = selection.getPreviousNode();
12753 event.preventDefault();
12754 if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) {
12755 // heading is empty
12756 prevNode.parentNode.removeChild(prevNode);
12758 var range = prevNode.ownerDocument.createRange();
12759 range.selectNodeContents(prevNode);
12760 range.collapse(false);
12761 selection.setSelection(range);
12765 var beforeUneditable = selection.caretIsBeforeUneditable();
12766 // Do a special delete if caret would delete uneditable
12767 if (beforeUneditable) {
12768 event.preventDefault();
12769 deleteAroundEditable(selection, beforeUneditable, element);
12773 if (selection.containsUneditable()) {
12774 event.preventDefault();
12775 selection.deleteContents();
12780 var handleTabKeyDown = function(composer, element) {
12781 if (!composer.selection.isCollapsed()) {
12782 composer.selection.deleteContents();
12783 } else if (composer.selection.caretIsInTheBeginnig('LI')) {
12784 if (composer.commands.exec('indentList')) return;
12787 // Is   close enough to tab. Could not find enough counter arguments for now.
12788 composer.commands.exec("insertHTML", " ");
12791 wysihtml5.views.Composer.prototype.observe = function() {
12793 state = this.getValue(false, false),
12794 container = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(),
12795 element = this.element,
12796 focusBlurElement = (browser.supportsEventsInIframeCorrectly() || this.sandbox.getContentEditable) ? element : this.sandbox.getWindow(),
12797 pasteEvents = ["drop", "paste", "beforepaste"],
12798 interactionEvents = ["drop", "paste", "mouseup", "focus", "keyup"];
12800 // --------- destroy:composer event ---------
12801 dom.observe(container, "DOMNodeRemoved", function() {
12802 clearInterval(domNodeRemovedInterval);
12803 that.parent.fire("destroy:composer");
12806 // DOMNodeRemoved event is not supported in IE 8
12807 if (!browser.supportsMutationEvents()) {
12808 var domNodeRemovedInterval = setInterval(function() {
12809 if (!dom.contains(document.documentElement, container)) {
12810 clearInterval(domNodeRemovedInterval);
12811 that.parent.fire("destroy:composer");
12816 // --------- User interaction tracking --
12818 dom.observe(focusBlurElement, interactionEvents, function() {
12819 setTimeout(function() {
12820 that.parent.fire("interaction").fire("interaction:composer");
12825 if (this.config.handleTables) {
12826 if(!this.tableClickHandle && this.doc.execCommand && wysihtml5.browser.supportsCommand(this.doc, "enableObjectResizing") && wysihtml5.browser.supportsCommand(this.doc, "enableInlineTableEditing")) {
12827 if (this.sandbox.getIframe) {
12828 this.tableClickHandle = dom.observe(container , ["focus", "mouseup", "mouseover"], function() {
12829 that.doc.execCommand("enableObjectResizing", false, "false");
12830 that.doc.execCommand("enableInlineTableEditing", false, "false");
12831 that.tableClickHandle.stop();
12834 setTimeout(function() {
12835 that.doc.execCommand("enableObjectResizing", false, "false");
12836 that.doc.execCommand("enableInlineTableEditing", false, "false");
12840 this.tableSelection = wysihtml5.quirks.tableCellsSelection(element, that.parent);
12843 // --------- Focus & blur logic ---------
12844 dom.observe(focusBlurElement, "focus", function(event) {
12845 that.parent.fire("focus", event).fire("focus:composer", event);
12847 // Delay storing of state until all focus handler are fired
12848 // especially the one which resets the placeholder
12849 setTimeout(function() { state = that.getValue(false, false); }, 0);
12852 dom.observe(focusBlurElement, "blur", function(event) {
12853 if (state !== that.getValue(false, false)) {
12854 //create change event if supported (all except IE8)
12855 var changeevent = event;
12856 if(typeof Object.create == 'function') {
12857 changeevent = Object.create(event, { type: { value: 'change' } });
12859 that.parent.fire("change", changeevent).fire("change:composer", changeevent);
12861 that.parent.fire("blur", event).fire("blur:composer", event);
12864 // --------- Drag & Drop logic ---------
12865 dom.observe(element, "dragenter", function() {
12866 that.parent.fire("unset_placeholder");
12869 dom.observe(element, pasteEvents, function(event) {
12870 that.parent.fire(event.type, event).fire(event.type + ":composer", event);
12874 if (this.config.copyedFromMarking) {
12875 // If supported the copied source is based directly on selection
12876 // Very useful for webkit based browsers where copy will otherwise contain a lot of code and styles based on whatever and not actually in selection.
12877 dom.observe(element, "copy", function(event) {
12878 if (event.clipboardData) {
12879 event.clipboardData.setData("text/html", that.config.copyedFromMarking + that.selection.getHtml());
12880 event.preventDefault();
12882 that.parent.fire(event.type, event).fire(event.type + ":composer", event);
12886 // --------- neword event ---------
12887 dom.observe(element, "keyup", function(event) {
12888 var keyCode = event.keyCode;
12889 if (keyCode === wysihtml5.SPACE_KEY || keyCode === wysihtml5.ENTER_KEY) {
12890 that.parent.fire("newword:composer");
12894 this.parent.on("paste:composer", function() {
12895 setTimeout(function() { that.parent.fire("newword:composer"); }, 0);
12898 // --------- Make sure that images are selected when clicking on them ---------
12899 if (!browser.canSelectImagesInContentEditable()) {
12900 dom.observe(element, "mousedown", function(event) {
12901 var target = event.target;
12902 var allImages = element.querySelectorAll('img'),
12903 notMyImages = element.querySelectorAll('.' + that.config.uneditableContainerClassname + ' img'),
12904 myImages = wysihtml5.lang.array(allImages).without(notMyImages);
12906 if (target.nodeName === "IMG" && wysihtml5.lang.array(myImages).contains(target)) {
12907 that.selection.selectNode(target);
12912 if (!browser.canSelectImagesInContentEditable()) {
12913 dom.observe(element, "drop", function(event) {
12914 // TODO: if I knew how to get dropped elements list from event I could limit it to only IMG element case
12915 setTimeout(function() {
12916 that.selection.getSelection().removeAllRanges();
12921 if (browser.hasHistoryIssue() && browser.supportsSelectionModify()) {
12922 dom.observe(element, "keydown", function(event) {
12923 if (!event.metaKey && !event.ctrlKey) {
12927 var keyCode = event.keyCode,
12928 win = element.ownerDocument.defaultView,
12929 selection = win.getSelection();
12931 if (keyCode === 37 || keyCode === 39) {
12932 if (keyCode === 37) {
12933 selection.modify("extend", "left", "lineboundary");
12934 if (!event.shiftKey) {
12935 selection.collapseToStart();
12938 if (keyCode === 39) {
12939 selection.modify("extend", "right", "lineboundary");
12940 if (!event.shiftKey) {
12941 selection.collapseToEnd();
12944 event.preventDefault();
12949 // --------- Shortcut logic ---------
12950 dom.observe(element, "keydown", function(event) {
12951 var keyCode = event.keyCode,
12952 command = shortcuts[keyCode];
12953 if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
12954 that.commands.exec(command);
12955 event.preventDefault();
12957 if (keyCode === 8) {
12959 handleDeleteKeyPress(event, that.selection, element, that);
12960 } else if (that.config.handleTabKey && keyCode === 9) {
12961 event.preventDefault();
12962 handleTabKeyDown(that, element);
12966 // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor ---------
12967 dom.observe(element, "keydown", function(event) {
12968 var target = that.selection.getSelectedNode(true),
12969 keyCode = event.keyCode,
12971 if (target && target.nodeName === "IMG" && (keyCode === wysihtml5.BACKSPACE_KEY || keyCode === wysihtml5.DELETE_KEY)) { // 8 => backspace, 46 => delete
12972 parent = target.parentNode;
12973 // delete the <img>
12974 parent.removeChild(target);
12975 // and it's parent <a> too if it hasn't got any other child nodes
12976 if (parent.nodeName === "A" && !parent.firstChild) {
12977 parent.parentNode.removeChild(parent);
12980 setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0);
12981 event.preventDefault();
12985 // --------- IE 8+9 focus the editor when the iframe is clicked (without actually firing the 'focus' event on the <body>) ---------
12986 if (!this.config.contentEditableMode && browser.hasIframeFocusIssue()) {
12987 dom.observe(container, "focus", function() {
12988 setTimeout(function() {
12989 if (that.doc.querySelector(":focus") !== that.element) {
12995 dom.observe(this.element, "blur", function() {
12996 setTimeout(function() {
12997 that.selection.getSelection().removeAllRanges();
13002 // --------- Show url in tooltip when hovering links or images ---------
13003 var titlePrefixes = {
13008 dom.observe(element, "mouseover", function(event) {
13009 var target = event.target,
13010 nodeName = target.nodeName,
13012 if (nodeName !== "A" && nodeName !== "IMG") {
13015 var hasTitle = target.hasAttribute("title");
13017 title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));
13018 target.setAttribute("title", title);
13024 * Class that takes care that the value of the composer and the textarea is always in sync
13026 (function(wysihtml5) {
13027 var INTERVAL = 400;
13029 wysihtml5.views.Synchronizer = Base.extend(
13030 /** @scope wysihtml5.views.Synchronizer.prototype */ {
13032 constructor: function(editor, textarea, composer) {
13033 this.editor = editor;
13034 this.textarea = textarea;
13035 this.composer = composer;
13041 * Sync html from composer to textarea
13042 * Takes care of placeholders
13043 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
13045 fromComposerToTextarea: function(shouldParseHtml) {
13046 this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue(false, false)).trim(), shouldParseHtml);
13050 * Sync value of textarea to composer
13051 * Takes care of placeholders
13052 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
13054 fromTextareaToComposer: function(shouldParseHtml) {
13055 var textareaValue = this.textarea.getValue(false, false);
13056 if (textareaValue) {
13057 this.composer.setValue(textareaValue, shouldParseHtml);
13059 this.composer.clear();
13060 this.editor.fire("set_placeholder");
13065 * Invoke syncing based on view state
13066 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
13068 sync: function(shouldParseHtml) {
13069 if (this.editor.currentView.name === "textarea") {
13070 this.fromTextareaToComposer(shouldParseHtml);
13072 this.fromComposerToTextarea(shouldParseHtml);
13077 * Initializes interval-based syncing
13078 * also makes sure that on-submit the composer's content is synced with the textarea
13079 * immediately when the form gets submitted
13081 _observe: function() {
13084 form = this.textarea.element.form,
13085 startInterval = function() {
13086 interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
13088 stopInterval = function() {
13089 clearInterval(interval);
13096 // If the textarea is in a form make sure that after onreset and onsubmit the composer
13097 // has the correct state
13098 wysihtml5.dom.observe(form, "submit", function() {
13101 wysihtml5.dom.observe(form, "reset", function() {
13102 setTimeout(function() { that.fromTextareaToComposer(); }, 0);
13106 this.editor.on("change_view", function(view) {
13107 if (view === "composer" && !interval) {
13108 that.fromTextareaToComposer(true);
13110 } else if (view === "textarea") {
13111 that.fromComposerToTextarea(true);
13116 this.editor.on("destroy:composer", stopInterval);
13120 ;wysihtml5.views.Textarea = wysihtml5.views.View.extend(
13121 /** @scope wysihtml5.views.Textarea.prototype */ {
13124 constructor: function(parent, textareaElement, config) {
13125 this.base(parent, textareaElement, config);
13130 clear: function() {
13131 this.element.value = "";
13134 getValue: function(parse) {
13135 var value = this.isEmpty() ? "" : this.element.value;
13136 if (parse !== false) {
13137 value = this.parent.parse(value);
13142 setValue: function(html, parse) {
13144 html = this.parent.parse(html);
13146 this.element.value = html;
13149 cleanUp: function() {
13150 var html = this.parent.parse(this.element.value);
13151 this.element.value = html;
13154 hasPlaceholderSet: function() {
13155 var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),
13156 placeholderText = this.element.getAttribute("placeholder") || null,
13157 value = this.element.value,
13159 return (supportsPlaceholder && isEmpty) || (value === placeholderText);
13162 isEmpty: function() {
13163 return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
13166 _observe: function() {
13167 var element = this.element,
13168 parent = this.parent,
13174 * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
13175 * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
13177 events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
13179 parent.on("beforeload", function() {
13180 wysihtml5.dom.observe(element, events, function(event) {
13181 var eventName = eventMapping[event.type] || event.type;
13182 parent.fire(eventName).fire(eventName + ":textarea");
13185 wysihtml5.dom.observe(element, ["paste", "drop"], function() {
13186 setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
13194 * @param {Element} editableElement Reference to the textarea which should be turned into a rich text interface
13195 * @param {Object} [config] See defaultConfig object below for explanation of each individual config option
13199 * beforeload (for internal use only)
13216 * beforecommand:composer
13217 * aftercommand:composer
13222 (function(wysihtml5) {
13225 var defaultConfig = {
13226 // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body
13228 // Whether the editor should look like the textarea (by adopting styles)
13230 // Id of the toolbar element, pass falsey value if you don't want any toolbar logic
13232 // Whether toolbar is displayed after init by script automatically.
13233 // Can be set to false if toolobar is set to display only on editable area focus
13234 showToolbarAfterInit: true,
13235 // Whether urls, entered by the user should automatically become clickable-links
13237 // Includes table editing events and cell selection tracking
13238 handleTables: true,
13239 // Tab key inserts tab into text as default behaviour. It can be disabled to regain keyboard navigation
13240 handleTabKey: true,
13241 // Object which includes parser rules to apply when html gets cleaned
13242 // See parser_rules/*.js for examples
13243 parserRules: { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} },
13244 // Object which includes parser when the user inserts content via copy & paste. If null parserRules will be used instead
13245 pasteParserRulesets: null,
13246 // Parser method to use when the user inserts content
13247 parser: wysihtml5.dom.parse,
13248 // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
13249 composerClassName: "wysihtml5-editor",
13250 // Class name to add to the body when the wysihtml5 editor is supported
13251 bodyClassName: "wysihtml5-supported",
13252 // By default wysihtml5 will insert a <br> for line breaks, set this to false to use <p>
13253 useLineBreaks: true,
13254 // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
13256 // Placeholder text to use, defaults to the placeholder attribute on the textarea element
13257 placeholderText: undef,
13258 // Whether the rich text editor should be rendered on touch devices (wysihtml5 >= 0.3.0 comes with basic support for iOS 5)
13259 supportTouchDevices: true,
13260 // Whether senseless <span> elements (empty or without attributes) should be removed/replaced with their content
13262 // Whether to use div instead of secure iframe
13263 contentEditableMode: false,
13264 // Classname of container that editor should not touch and pass through
13265 // Pass false to disable
13266 uneditableContainerClassname: "wysihtml5-uneditable-container",
13267 // Browsers that support copied source handling will get a marking of the origin of the copied source (for determinig code cleanup rules on paste)
13268 // Also copied source is based directly on selection -
13269 // (very useful for webkit based browsers where copy will otherwise contain a lot of code and styles based on whatever and not actually in selection).
13270 // If falsy value is passed source override is also disabled
13271 copyedFromMarking: '<meta name="copied-from" content="wysihtml5">'
13274 wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend(
13275 /** @scope wysihtml5.Editor.prototype */ {
13276 constructor: function(editableElement, config) {
13277 this.editableElement = typeof(editableElement) === "string" ? document.getElementById(editableElement) : editableElement;
13278 this.config = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get();
13279 this._isCompatible = wysihtml5.browser.supported();
13281 if (this.editableElement.nodeName.toLowerCase() != "textarea") {
13282 this.config.contentEditableMode = true;
13283 this.config.noTextarea = true;
13285 if (!this.config.noTextarea) {
13286 this.textarea = new wysihtml5.views.Textarea(this, this.editableElement, this.config);
13287 this.currentView = this.textarea;
13290 // Sort out unsupported/unwanted browsers here
13291 if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) {
13293 setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
13297 // Add class name to body, to indicate that the editor is supported
13298 wysihtml5.dom.addClass(document.body, this.config.bodyClassName);
13300 this.composer = new wysihtml5.views.Composer(this, this.editableElement, this.config);
13301 this.currentView = this.composer;
13303 if (typeof(this.config.parser) === "function") {
13304 this._initParser();
13307 this.on("beforeload", this.handleBeforeLoad);
13310 handleBeforeLoad: function() {
13311 if (!this.config.noTextarea) {
13312 this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer);
13314 if (this.config.toolbar) {
13315 this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar, this.config.showToolbarAfterInit);
13319 isCompatible: function() {
13320 return this._isCompatible;
13323 clear: function() {
13324 this.currentView.clear();
13328 getValue: function(parse, clearInternals) {
13329 return this.currentView.getValue(parse, clearInternals);
13332 setValue: function(html, parse) {
13333 this.fire("unset_placeholder");
13336 return this.clear();
13339 this.currentView.setValue(html, parse);
13343 cleanUp: function() {
13344 this.currentView.cleanUp();
13347 focus: function(setToEnd) {
13348 this.currentView.focus(setToEnd);
13353 * Deactivate editor (make it readonly)
13355 disable: function() {
13356 this.currentView.disable();
13363 enable: function() {
13364 this.currentView.enable();
13368 isEmpty: function() {
13369 return this.currentView.isEmpty();
13372 hasPlaceholderSet: function() {
13373 return this.currentView.hasPlaceholderSet();
13376 parse: function(htmlOrElement, clearInternals) {
13377 var parseContext = (this.config.contentEditableMode) ? document : ((this.composer) ? this.composer.sandbox.getDocument() : null);
13378 var returnValue = this.config.parser(htmlOrElement, {
13379 "rules": this.config.parserRules,
13380 "cleanUp": this.config.cleanUp,
13381 "context": parseContext,
13382 "uneditableClass": this.config.uneditableContainerClassname,
13383 "clearInternals" : clearInternals
13385 if (typeof(htmlOrElement) === "object") {
13386 wysihtml5.quirks.redraw(htmlOrElement);
13388 return returnValue;
13392 * Prepare html parser logic
13393 * - Observes for paste and drop
13395 _initParser: function() {
13400 if (wysihtml5.browser.supportsModenPaste()) {
13401 this.on("paste:composer", function(event) {
13402 event.preventDefault();
13403 oldHtml = wysihtml5.dom.getPastedHtml(event);
13405 that._cleanAndPaste(oldHtml);
13410 this.on("beforepaste:composer", function(event) {
13411 event.preventDefault();
13412 wysihtml5.dom.getPastedHtmlWithDiv(that.composer, function(pastedHTML) {
13414 that._cleanAndPaste(pastedHTML);
13422 _cleanAndPaste: function (oldHtml) {
13423 var cleanHtml = wysihtml5.quirks.cleanPastedHTML(oldHtml, {
13424 "referenceNode": this.composer.element,
13425 "rules": this.config.pasteParserRulesets || [{"set": this.config.parserRules}],
13426 "uneditableClass": this.config.uneditableContainerClassname
13428 this.composer.selection.deleteContents();
13429 this.composer.selection.insertHTML(cleanHtml);
13436 * @param {Element} link The toolbar link which causes the dialog to show up
13437 * @param {Element} container The dialog container
13440 * <!-- Toolbar link -->
13441 * <a data-wysihtml5-command="insertImage">insert an image</a>
13444 * <div data-wysihtml5-dialog="insertImage" style="display: none;">
13446 * URL: <input data-wysihtml5-dialog-field="src" value="http://">
13449 * Alternative text: <input data-wysihtml5-dialog-field="alt" value="">
13454 * var dialog = new wysihtml5.toolbar.Dialog(
13455 * document.querySelector("[data-wysihtml5-command='insertImage']"),
13456 * document.querySelector("[data-wysihtml5-dialog='insertImage']")
13458 * dialog.observe("save", function(attributes) {
13463 (function(wysihtml5) {
13464 var dom = wysihtml5.dom,
13465 CLASS_NAME_OPENED = "wysihtml5-command-dialog-opened",
13466 SELECTOR_FORM_ELEMENTS = "input, select, textarea",
13467 SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]",
13468 ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field";
13471 wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend(
13472 /** @scope wysihtml5.toolbar.Dialog.prototype */ {
13473 constructor: function(link, container) {
13475 this.container = container;
13478 _observe: function() {
13479 if (this._observed) {
13484 callbackWrapper = function(event) {
13485 var attributes = that._serialize();
13486 if (attributes == that.elementToChange) {
13487 that.fire("edit", attributes);
13489 that.fire("save", attributes);
13492 event.preventDefault();
13493 event.stopPropagation();
13496 dom.observe(that.link, "click", function() {
13497 if (dom.hasClass(that.link, CLASS_NAME_OPENED)) {
13498 setTimeout(function() { that.hide(); }, 0);
13502 dom.observe(this.container, "keydown", function(event) {
13503 var keyCode = event.keyCode;
13504 if (keyCode === wysihtml5.ENTER_KEY) {
13505 callbackWrapper(event);
13507 if (keyCode === wysihtml5.ESCAPE_KEY) {
13508 that.fire("cancel");
13513 dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper);
13515 dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) {
13516 that.fire("cancel");
13518 event.preventDefault();
13519 event.stopPropagation();
13522 var formElements = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS),
13524 length = formElements.length,
13525 _clearInterval = function() { clearInterval(that.interval); };
13526 for (; i<length; i++) {
13527 dom.observe(formElements[i], "change", _clearInterval);
13530 this._observed = true;
13534 * Grabs all fields in the dialog and puts them in key=>value style in an object which
13535 * then gets returned
13537 _serialize: function() {
13538 var data = this.elementToChange || {},
13539 fields = this.container.querySelectorAll(SELECTOR_FIELDS),
13540 length = fields.length,
13543 for (; i<length; i++) {
13544 data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
13550 * Takes the attributes of the "elementToChange"
13551 * and inserts them in their corresponding dialog input fields
13553 * Assume the "elementToChange" looks like this:
13554 * <a href="http://www.google.com" target="_blank">foo</a>
13556 * and we have the following dialog:
13557 * <input type="text" data-wysihtml5-dialog-field="href" value="">
13558 * <input type="text" data-wysihtml5-dialog-field="target" value="">
13560 * after calling _interpolate() the dialog will look like this
13561 * <input type="text" data-wysihtml5-dialog-field="href" value="http://www.google.com">
13562 * <input type="text" data-wysihtml5-dialog-field="target" value="_blank">
13564 * Basically it adopted the attribute values into the corresponding input fields
13567 _interpolate: function(avoidHiddenFields) {
13571 focusedElement = document.querySelector(":focus"),
13572 fields = this.container.querySelectorAll(SELECTOR_FIELDS),
13573 length = fields.length,
13575 for (; i<length; i++) {
13578 // Never change elements where the user is currently typing in
13579 if (field === focusedElement) {
13583 // Don't update hidden fields
13584 // See https://github.com/xing/wysihtml5/pull/14
13585 if (avoidHiddenFields && field.type === "hidden") {
13589 fieldName = field.getAttribute(ATTRIBUTE_FIELDS);
13590 newValue = (this.elementToChange && typeof(this.elementToChange) !== 'boolean') ? (this.elementToChange.getAttribute(fieldName) || "") : field.defaultValue;
13591 field.value = newValue;
13596 * Show the dialog element
13598 show: function(elementToChange) {
13599 if (dom.hasClass(this.link, CLASS_NAME_OPENED)) {
13604 firstField = this.container.querySelector(SELECTOR_FORM_ELEMENTS);
13605 this.elementToChange = elementToChange;
13607 this._interpolate();
13608 if (elementToChange) {
13609 this.interval = setInterval(function() { that._interpolate(true); }, 500);
13611 dom.addClass(this.link, CLASS_NAME_OPENED);
13612 this.container.style.display = "";
13614 if (firstField && !elementToChange) {
13616 firstField.focus();
13622 * Hide the dialog element
13625 clearInterval(this.interval);
13626 this.elementToChange = null;
13627 dom.removeClass(this.link, CLASS_NAME_OPENED);
13628 this.container.style.display = "none";
13634 * Converts speech-to-text and inserts this into the editor
13635 * As of now (2011/03/25) this only is supported in Chrome >= 11
13637 * Note that it sends the recorded audio to the google speech recognition api:
13638 * http://stackoverflow.com/questions/4361826/does-chrome-have-buil-in-speech-recognition-for-input-type-text-x-webkit-speec
13640 * Current HTML5 draft can be found here
13641 * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html
13643 * "Accessing Google Speech API Chrome 11"
13644 * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
13646 (function(wysihtml5) {
13647 var dom = wysihtml5.dom;
13650 position: "relative"
13653 var wrapperStyles = {
13657 overflow: "hidden",
13659 position: "absolute",
13664 var inputStyles = {
13668 marginTop: "-25px",
13671 position: "absolute",
13676 var inputAttributes = {
13677 "x-webkit-speech": "",
13681 wysihtml5.toolbar.Speech = function(parent, link) {
13682 var input = document.createElement("input");
13683 if (!wysihtml5.browser.supportsSpeechApiOn(input)) {
13684 link.style.display = "none";
13687 var lang = parent.editor.textarea.element.getAttribute("lang");
13689 inputAttributes.lang = lang;
13692 var wrapper = document.createElement("div");
13694 wysihtml5.lang.object(wrapperStyles).merge({
13695 width: link.offsetWidth + "px",
13696 height: link.offsetHeight + "px"
13699 dom.insert(input).into(wrapper);
13700 dom.insert(wrapper).into(link);
13702 dom.setStyles(inputStyles).on(input);
13703 dom.setAttributes(inputAttributes).on(input);
13705 dom.setStyles(wrapperStyles).on(wrapper);
13706 dom.setStyles(linkStyles).on(link);
13708 var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange";
13709 dom.observe(input, eventName, function() {
13710 parent.execCommand("insertText", input.value);
13714 dom.observe(input, "click", function(event) {
13715 if (dom.hasClass(link, "wysihtml5-command-disabled")) {
13716 event.preventDefault();
13719 event.stopPropagation();
13726 * @param {Object} parent Reference to instance of Editor instance
13727 * @param {Element} container Reference to the toolbar container element
13730 * <div id="toolbar">
13731 * <a data-wysihtml5-command="createLink">insert link</a>
13732 * <a data-wysihtml5-command="formatBlock" data-wysihtml5-command-value="h1">insert h1</a>
13736 * var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar"));
13739 (function(wysihtml5) {
13740 var CLASS_NAME_COMMAND_DISABLED = "wysihtml5-command-disabled",
13741 CLASS_NAME_COMMANDS_DISABLED = "wysihtml5-commands-disabled",
13742 CLASS_NAME_COMMAND_ACTIVE = "wysihtml5-command-active",
13743 CLASS_NAME_ACTION_ACTIVE = "wysihtml5-action-active",
13744 dom = wysihtml5.dom;
13746 wysihtml5.toolbar.Toolbar = Base.extend(
13747 /** @scope wysihtml5.toolbar.Toolbar.prototype */ {
13748 constructor: function(editor, container, showOnInit) {
13749 this.editor = editor;
13750 this.container = typeof(container) === "string" ? document.getElementById(container) : container;
13751 this.composer = editor.composer;
13753 this._getLinks("command");
13754 this._getLinks("action");
13757 if (showOnInit) { this.show(); }
13759 if (editor.config.classNameCommandDisabled != null) {
13760 CLASS_NAME_COMMAND_DISABLED = editor.config.classNameCommandDisabled;
13762 if (editor.config.classNameCommandsDisabled != null) {
13763 CLASS_NAME_COMMANDS_DISABLED = editor.config.classNameCommandsDisabled;
13765 if (editor.config.classNameCommandActive != null) {
13766 CLASS_NAME_COMMAND_ACTIVE = editor.config.classNameCommandActive;
13768 if (editor.config.classNameActionActive != null) {
13769 CLASS_NAME_ACTION_ACTIVE = editor.config.classNameActionActive;
13772 var speechInputLinks = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"),
13773 length = speechInputLinks.length,
13775 for (; i<length; i++) {
13776 new wysihtml5.toolbar.Speech(this, speechInputLinks[i]);
13780 _getLinks: function(type) {
13781 var links = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("[data-wysihtml5-" + type + "]")).get(),
13782 length = links.length,
13784 mapping = this[type + "Mapping"] = {},
13790 for (; i<length; i++) {
13792 name = link.getAttribute("data-wysihtml5-" + type);
13793 value = link.getAttribute("data-wysihtml5-" + type + "-value");
13794 group = this.container.querySelector("[data-wysihtml5-" + type + "-group='" + name + "']");
13795 dialog = this._getDialog(link, name);
13797 mapping[name + ":" + value] = {
13808 _getDialog: function(link, command) {
13810 dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"),
13814 if (dialogElement) {
13815 if (wysihtml5.toolbar["Dialog_" + command]) {
13816 dialog = new wysihtml5.toolbar["Dialog_" + command](link, dialogElement);
13818 dialog = new wysihtml5.toolbar.Dialog(link, dialogElement);
13821 dialog.on("show", function() {
13822 caretBookmark = that.composer.selection.getBookmark();
13824 that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
13827 dialog.on("save", function(attributes) {
13828 if (caretBookmark) {
13829 that.composer.selection.setBookmark(caretBookmark);
13831 that._execCommand(command, attributes);
13833 that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
13836 dialog.on("cancel", function() {
13837 that.editor.focus(false);
13838 that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
13846 * var toolbar = new wysihtml5.Toolbar();
13847 * // Insert a <blockquote> element or wrap current selection in <blockquote>
13848 * toolbar.execCommand("formatBlock", "blockquote");
13850 execCommand: function(command, commandValue) {
13851 if (this.commandsDisabled) {
13855 var commandObj = this.commandMapping[command + ":" + commandValue];
13857 // Show dialog when available
13858 if (commandObj && commandObj.dialog && !commandObj.state) {
13859 commandObj.dialog.show();
13861 this._execCommand(command, commandValue);
13865 _execCommand: function(command, commandValue) {
13866 // Make sure that composer is focussed (false => don't move caret to the end)
13867 this.editor.focus(false);
13869 this.composer.commands.exec(command, commandValue);
13870 this._updateLinkStates();
13873 execAction: function(action) {
13874 var editor = this.editor;
13875 if (action === "change_view") {
13876 if (editor.textarea) {
13877 if (editor.currentView === editor.textarea) {
13878 editor.fire("change_view", "composer");
13880 editor.fire("change_view", "textarea");
13884 if (action == "showSource") {
13885 editor.fire("showSource");
13889 _observe: function() {
13891 editor = this.editor,
13892 container = this.container,
13893 links = this.commandLinks.concat(this.actionLinks),
13894 length = links.length,
13897 for (; i<length; i++) {
13898 // 'javascript:;' and unselectable=on Needed for IE, but done in all browsers to make sure that all get the same css applied
13899 // (you know, a:link { ... } doesn't match anchors with missing href attribute)
13900 if (links[i].nodeName === "A") {
13901 dom.setAttributes({
13902 href: "javascript:;",
13906 dom.setAttributes({ unselectable: "on" }).on(links[i]);
13910 // Needed for opera and chrome
13911 dom.delegate(container, "[data-wysihtml5-command], [data-wysihtml5-action]", "mousedown", function(event) { event.preventDefault(); });
13913 dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) {
13915 command = link.getAttribute("data-wysihtml5-command"),
13916 commandValue = link.getAttribute("data-wysihtml5-command-value");
13917 that.execCommand(command, commandValue);
13918 event.preventDefault();
13921 dom.delegate(container, "[data-wysihtml5-action]", "click", function(event) {
13922 var action = this.getAttribute("data-wysihtml5-action");
13923 that.execAction(action);
13924 event.preventDefault();
13927 editor.on("interaction:composer", function() {
13928 that._updateLinkStates();
13931 editor.on("focus:composer", function() {
13932 that.bookmark = null;
13935 if (this.editor.config.handleTables) {
13936 editor.on("tableselect:composer", function() {
13937 that.container.querySelectorAll('[data-wysihtml5-hiddentools="table"]')[0].style.display = "";
13939 editor.on("tableunselect:composer", function() {
13940 that.container.querySelectorAll('[data-wysihtml5-hiddentools="table"]')[0].style.display = "none";
13944 editor.on("change_view", function(currentView) {
13945 // Set timeout needed in order to let the blur event fire first
13946 if (editor.textarea) {
13947 setTimeout(function() {
13948 that.commandsDisabled = (currentView !== "composer");
13949 that._updateLinkStates();
13950 if (that.commandsDisabled) {
13951 dom.addClass(container, CLASS_NAME_COMMANDS_DISABLED);
13953 dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED);
13960 _updateLinkStates: function() {
13962 var commandMapping = this.commandMapping,
13963 actionMapping = this.actionMapping,
13968 // every millisecond counts... this is executed quite often
13969 for (i in commandMapping) {
13970 command = commandMapping[i];
13971 if (this.commandsDisabled) {
13973 dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
13974 if (command.group) {
13975 dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
13977 if (command.dialog) {
13978 command.dialog.hide();
13981 state = this.composer.commands.state(command.name, command.value);
13982 dom.removeClass(command.link, CLASS_NAME_COMMAND_DISABLED);
13983 if (command.group) {
13984 dom.removeClass(command.group, CLASS_NAME_COMMAND_DISABLED);
13987 if (command.state === state) {
13991 command.state = state;
13993 dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
13994 if (command.group) {
13995 dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
13997 if (command.dialog) {
13998 if (typeof(state) === "object" || wysihtml5.lang.object(state).isArray()) {
14000 if (!command.dialog.multiselect && wysihtml5.lang.object(state).isArray()) {
14001 // Grab first and only object/element in state array, otherwise convert state into boolean
14002 // to avoid showing a dialog for multiple selected elements which may have different attributes
14003 // eg. when two links with different href are selected, the state will be an array consisting of both link elements
14004 // but the dialog interface can only update one
14005 state = state.length === 1 ? state[0] : true;
14006 command.state = state;
14008 command.dialog.show(state);
14010 command.dialog.hide();
14014 dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
14015 if (command.group) {
14016 dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
14018 if (command.dialog) {
14019 command.dialog.hide();
14024 for (i in actionMapping) {
14025 action = actionMapping[i];
14027 if (action.name === "change_view") {
14028 action.state = this.editor.currentView === this.editor.textarea;
14029 if (action.state) {
14030 dom.addClass(action.link, CLASS_NAME_ACTION_ACTIVE);
14032 dom.removeClass(action.link, CLASS_NAME_ACTION_ACTIVE);
14039 this.container.style.display = "";
14043 this.container.style.display = "none";
14048 ;(function(wysihtml5) {
14049 wysihtml5.toolbar.Dialog_createTable = wysihtml5.toolbar.Dialog.extend({
14050 show: function(elementToChange) {
14051 this.base(elementToChange);
14057 ;(function(wysihtml5) {
14058 var dom = wysihtml5.dom,
14059 SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]",
14060 ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field";
14062 wysihtml5.toolbar.Dialog_foreColorStyle = wysihtml5.toolbar.Dialog.extend({
14065 _serialize: function() {
14067 fields = this.container.querySelectorAll(SELECTOR_FIELDS),
14068 length = fields.length,
14071 for (; i<length; i++) {
14072 data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
14077 _interpolate: function(avoidHiddenFields) {
14081 focusedElement = document.querySelector(":focus"),
14082 fields = this.container.querySelectorAll(SELECTOR_FIELDS),
14083 length = fields.length,
14085 firstElement = (this.elementToChange) ? ((wysihtml5.lang.object(this.elementToChange).isArray()) ? this.elementToChange[0] : this.elementToChange) : null,
14086 colorStr = (firstElement) ? firstElement.getAttribute('style') : null,
14087 color = (colorStr) ? wysihtml5.quirks.styleParser.parseColor(colorStr, "color") : null;
14089 for (; i<length; i++) {
14091 // Never change elements where the user is currently typing in
14092 if (field === focusedElement) {
14095 // Don't update hidden fields3
14096 if (avoidHiddenFields && field.type === "hidden") {
14099 if (field.getAttribute(ATTRIBUTE_FIELDS) === "color") {
14101 if (color[3] && color[3] != 1) {
14102 field.value = "rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + color[3] + ");";
14104 field.value = "rgb(" + color[0] + "," + color[1] + "," + color[2] + ");";
14107 field.value = "rgb(0,0,0);";
14115 ;(function(wysihtml5) {
14116 var dom = wysihtml5.dom,
14117 SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]",
14118 ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field";
14120 wysihtml5.toolbar.Dialog_fontSizeStyle = wysihtml5.toolbar.Dialog.extend({
14123 _serialize: function() {
14124 return {"size" : this.container.querySelector('[data-wysihtml5-dialog-field="size"]').value};
14127 _interpolate: function(avoidHiddenFields) {
14128 var focusedElement = document.querySelector(":focus"),
14129 field = this.container.querySelector("[data-wysihtml5-dialog-field='size']"),
14130 firstElement = (this.elementToChange) ? ((wysihtml5.lang.object(this.elementToChange).isArray()) ? this.elementToChange[0] : this.elementToChange) : null,
14131 styleStr = (firstElement) ? firstElement.getAttribute('style') : null,
14132 size = (styleStr) ? wysihtml5.quirks.styleParser.parseFontSize(styleStr) : null;
14134 if (field && field !== focusedElement && size && !(/^\s*$/).test(size)) {
14135 field.value = size;
14145 Copyright (C) 2011 by Yehuda Katz
14147 Permission is hereby granted, free of charge, to any person obtaining a copy
14148 of this software and associated documentation files (the "Software"), to deal
14149 in the Software without restriction, including without limitation the rights
14150 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14151 copies of the Software, and to permit persons to whom the Software is
14152 furnished to do so, subject to the following conditions:
14154 The above copyright notice and this permission notice shall be included in
14155 all copies or substantial portions of the Software.
14157 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14158 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14159 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14160 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
14161 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
14162 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
14167 var Handlebars=function(){var a=function(){"use strict";function a(a){this.string=a}var b;return a.prototype.toString=function(){return""+this.string},b=a}(),b=function(a){"use strict";function b(a){return h[a]||"&"}function c(a,b){for(var c in b)Object.prototype.hasOwnProperty.call(b,c)&&(a[c]=b[c])}function d(a){return a instanceof g?a.toString():a||0===a?(a=""+a,j.test(a)?a.replace(i,b):a):""}function e(a){return a||0===a?m(a)&&0===a.length?!0:!1:!0}var f={},g=a,h={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},i=/[&<>"'`]/g,j=/[&<>"'`]/;f.extend=c;var k=Object.prototype.toString;f.toString=k;var l=function(a){return"function"==typeof a};l(/x/)&&(l=function(a){return"function"==typeof a&&"[object Function]"===k.call(a)});var l;f.isFunction=l;var m=Array.isArray||function(a){return a&&"object"==typeof a?"[object Array]"===k.call(a):!1};return f.isArray=m,f.escapeExpression=d,f.isEmpty=e,f}(a),c=function(){"use strict";function a(a,b){var d;b&&b.firstLine&&(d=b.firstLine,a+=" - "+d+":"+b.firstColumn);for(var e=Error.prototype.constructor.call(this,a),f=0;f<c.length;f++)this[c[f]]=e[c[f]];d&&(this.lineNumber=d,this.column=b.firstColumn)}var b,c=["description","fileName","lineNumber","message","name","number","stack"];return a.prototype=new Error,b=a}(),d=function(a,b){"use strict";function c(a,b){this.helpers=a||{},this.partials=b||{},d(this)}function d(a){a.registerHelper("helperMissing",function(a){if(2===arguments.length)return void 0;throw new h("Missing helper: '"+a+"'")}),a.registerHelper("blockHelperMissing",function(b,c){var d=c.inverse||function(){},e=c.fn;return m(b)&&(b=b.call(this)),b===!0?e(this):b===!1||null==b?d(this):l(b)?b.length>0?a.helpers.each(b,c):d(this):e(b)}),a.registerHelper("each",function(a,b){var c,d=b.fn,e=b.inverse,f=0,g="";if(m(a)&&(a=a.call(this)),b.data&&(c=q(b.data)),a&&"object"==typeof a)if(l(a))for(var h=a.length;h>f;f++)c&&(c.index=f,c.first=0===f,c.last=f===a.length-1),g+=d(a[f],{data:c});else for(var i in a)a.hasOwnProperty(i)&&(c&&(c.key=i,c.index=f,c.first=0===f),g+=d(a[i],{data:c}),f++);return 0===f&&(g=e(this)),g}),a.registerHelper("if",function(a,b){return m(a)&&(a=a.call(this)),!b.hash.includeZero&&!a||g.isEmpty(a)?b.inverse(this):b.fn(this)}),a.registerHelper("unless",function(b,c){return a.helpers["if"].call(this,b,{fn:c.inverse,inverse:c.fn,hash:c.hash})}),a.registerHelper("with",function(a,b){return m(a)&&(a=a.call(this)),g.isEmpty(a)?void 0:b.fn(a)}),a.registerHelper("log",function(b,c){var d=c.data&&null!=c.data.level?parseInt(c.data.level,10):1;a.log(d,b)})}function e(a,b){p.log(a,b)}var f={},g=a,h=b,i="1.3.0";f.VERSION=i;var j=4;f.COMPILER_REVISION=j;var k={1:"<= 1.0.rc.2",2:"== 1.0.0-rc.3",3:"== 1.0.0-rc.4",4:">= 1.0.0"};f.REVISION_CHANGES=k;var l=g.isArray,m=g.isFunction,n=g.toString,o="[object Object]";f.HandlebarsEnvironment=c,c.prototype={constructor:c,logger:p,log:e,registerHelper:function(a,b,c){if(n.call(a)===o){if(c||b)throw new h("Arg not supported with multiple helpers");g.extend(this.helpers,a)}else c&&(b.not=c),this.helpers[a]=b},registerPartial:function(a,b){n.call(a)===o?g.extend(this.partials,a):this.partials[a]=b}};var p={methodMap:{0:"debug",1:"info",2:"warn",3:"error"},DEBUG:0,INFO:1,WARN:2,ERROR:3,level:3,log:function(a,b){if(p.level<=a){var c=p.methodMap[a];"undefined"!=typeof console&&console[c]&&console[c].call(console,b)}}};f.logger=p,f.log=e;var q=function(a){var b={};return g.extend(b,a),b};return f.createFrame=q,f}(b,c),e=function(a,b,c){"use strict";function d(a){var b=a&&a[0]||1,c=m;if(b!==c){if(c>b){var d=n[c],e=n[b];throw new l("Template was precompiled with an older version of Handlebars than the current runtime. Please update your precompiler to a newer version ("+d+") or downgrade your runtime to an older version ("+e+").")}throw new l("Template was precompiled with a newer version of Handlebars than the current runtime. Please update your runtime to a newer version ("+a[1]+").")}}function e(a,b){if(!b)throw new l("No environment passed to template");var c=function(a,c,d,e,f,g){var h=b.VM.invokePartial.apply(this,arguments);if(null!=h)return h;if(b.compile){var i={helpers:e,partials:f,data:g};return f[c]=b.compile(a,{data:void 0!==g},b),f[c](d,i)}throw new l("The partial "+c+" could not be compiled when running in runtime-only mode")},d={escapeExpression:k.escapeExpression,invokePartial:c,programs:[],program:function(a,b,c){var d=this.programs[a];return c?d=g(a,b,c):d||(d=this.programs[a]=g(a,b)),d},merge:function(a,b){var c=a||b;return a&&b&&a!==b&&(c={},k.extend(c,b),k.extend(c,a)),c},programWithDepth:b.VM.programWithDepth,noop:b.VM.noop,compilerInfo:null};return function(c,e){e=e||{};var f,g,h=e.partial?e:b;e.partial||(f=e.helpers,g=e.partials);var i=a.call(d,h,c,f,g,e.data);return e.partial||b.VM.checkRevision(d.compilerInfo),i}}function f(a,b,c){var d=Array.prototype.slice.call(arguments,3),e=function(a,e){return e=e||{},b.apply(this,[a,e.data||c].concat(d))};return e.program=a,e.depth=d.length,e}function g(a,b,c){var d=function(a,d){return d=d||{},b(a,d.data||c)};return d.program=a,d.depth=0,d}function h(a,b,c,d,e,f){var g={partial:!0,helpers:d,partials:e,data:f};if(void 0===a)throw new l("The partial "+b+" could not be found");return a instanceof Function?a(c,g):void 0}function i(){return""}var j={},k=a,l=b,m=c.COMPILER_REVISION,n=c.REVISION_CHANGES;return j.checkRevision=d,j.template=e,j.programWithDepth=f,j.program=g,j.invokePartial=h,j.noop=i,j}(b,c,d),f=function(a,b,c,d,e){"use strict";var f,g=a,h=b,i=c,j=d,k=e,l=function(){var a=new g.HandlebarsEnvironment;return j.extend(a,g),a.SafeString=h,a.Exception=i,a.Utils=j,a.VM=k,a.template=function(b){return k.template(b,a)},a},m=l();return m.create=l,f=m}(d,a,c,b,e);return f}();this["wysihtml5"] = this["wysihtml5"] || {};
14168 this["wysihtml5"]["tpl"] = this["wysihtml5"]["tpl"] || {};
14170 this["wysihtml5"]["tpl"]["blockquote"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14171 this.compilerInfo = [4,'>= 1.0.0'];
14172 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14173 var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14175 function program1(depth0,data) {
14177 var buffer = "", stack1;
14179 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14183 function program3(depth0,data) {
14186 return " \n <span class=\"fa fa-quote-left\"></span>\n ";
14189 function program5(depth0,data) {
14192 return "\n <span class=\"glyphicon glyphicon-quote\"></span>\n ";
14195 buffer += "<li>\n <a class=\"btn ";
14196 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14197 if(stack1 || stack1 === 0) { buffer += stack1; }
14198 buffer += " btn-default\" data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"blockquote\" data-wysihtml5-display-format-name=\"false\" tabindex=\"-1\">\n ";
14199 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(5, program5, data),fn:self.program(3, program3, data),data:data});
14200 if(stack1 || stack1 === 0) { buffer += stack1; }
14201 buffer += "\n </a>\n</li>\n";
14205 this["wysihtml5"]["tpl"]["color"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14206 this.compilerInfo = [4,'>= 1.0.0'];
14207 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14208 var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14210 function program1(depth0,data) {
14212 var buffer = "", stack1;
14214 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14218 buffer += "<li class=\"dropdown\">\n <a class=\"btn btn-default dropdown-toggle ";
14219 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14220 if(stack1 || stack1 === 0) { buffer += stack1; }
14221 buffer += "\" data-toggle=\"dropdown\" tabindex=\"-1\">\n <span class=\"current-color\">"
14222 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.black)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14223 + "</span>\n <b class=\"caret\"></b>\n </a>\n <ul class=\"dropdown-menu\">\n <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\">"
14224 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.black)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14225 + "</a></li>\n <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\">"
14226 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.silver)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14227 + "</a></li>\n <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\">"
14228 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.gray)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14229 + "</a></li>\n <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\">"
14230 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.maroon)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14231 + "</a></li>\n <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\">"
14232 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.red)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14233 + "</a></li>\n <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\">"
14234 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.purple)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14235 + "</a></li>\n <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\">"
14236 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.green)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14237 + "</a></li>\n <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\">"
14238 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.olive)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14239 + "</a></li>\n <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\">"
14240 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.navy)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14241 + "</a></li>\n <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\">"
14242 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.blue)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14243 + "</a></li>\n <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\">"
14244 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.colours)),stack1 == null || stack1 === false ? stack1 : stack1.orange)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14245 + "</a></li>\n </ul>\n</li>\n";
14249 this["wysihtml5"]["tpl"]["emphasis"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14250 this.compilerInfo = [4,'>= 1.0.0'];
14251 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14252 var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14254 function program1(depth0,data) {
14256 var buffer = "", stack1;
14258 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14262 function program3(depth0,data) {
14264 var buffer = "", stack1;
14265 buffer += "\n <a class=\"btn ";
14266 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14267 if(stack1 || stack1 === 0) { buffer += stack1; }
14268 buffer += " btn-default\" data-wysihtml5-command=\"small\" title=\"CTRL+S\" tabindex=\"-1\">"
14269 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.emphasis)),stack1 == null || stack1 === false ? stack1 : stack1.small)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14274 buffer += "<li>\n <div class=\"btn-group\">\n <a class=\"btn ";
14275 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14276 if(stack1 || stack1 === 0) { buffer += stack1; }
14277 buffer += " btn-default\" data-wysihtml5-command=\"bold\" title=\"CTRL+B\" tabindex=\"-1\">"
14278 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.emphasis)),stack1 == null || stack1 === false ? stack1 : stack1.bold)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14279 + "</a>\n <a class=\"btn ";
14280 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14281 if(stack1 || stack1 === 0) { buffer += stack1; }
14282 buffer += " btn-default\" data-wysihtml5-command=\"italic\" title=\"CTRL+I\" tabindex=\"-1\">"
14283 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.emphasis)),stack1 == null || stack1 === false ? stack1 : stack1.italic)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14284 + "</a>\n <a class=\"btn ";
14285 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14286 if(stack1 || stack1 === 0) { buffer += stack1; }
14287 buffer += " btn-default\" data-wysihtml5-command=\"underline\" title=\"CTRL+U\" tabindex=\"-1\">"
14288 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.emphasis)),stack1 == null || stack1 === false ? stack1 : stack1.underline)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14290 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.emphasis)),stack1 == null || stack1 === false ? stack1 : stack1.small), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});
14291 if(stack1 || stack1 === 0) { buffer += stack1; }
14292 buffer += "\n </div>\n</li>\n";
14296 this["wysihtml5"]["tpl"]["font-styles"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14297 this.compilerInfo = [4,'>= 1.0.0'];
14298 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14299 var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14301 function program1(depth0,data) {
14303 var buffer = "", stack1;
14305 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14309 function program3(depth0,data) {
14312 return "\n <span class=\"fa fa-font\"></span>\n ";
14315 function program5(depth0,data) {
14318 return "\n <span class=\"glyphicon glyphicon-font\"></span>\n ";
14321 buffer += "<li class=\"dropdown\">\n <a class=\"btn btn-default dropdown-toggle ";
14322 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14323 if(stack1 || stack1 === 0) { buffer += stack1; }
14324 buffer += "\" data-toggle=\"dropdown\">\n ";
14325 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(5, program5, data),fn:self.program(3, program3, data),data:data});
14326 if(stack1 || stack1 === 0) { buffer += stack1; }
14327 buffer += "\n <span class=\"current-font\">"
14328 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.normal)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14329 + "</span>\n <b class=\"caret\"></b>\n </a>\n <ul class=\"dropdown-menu\">\n <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"p\" tabindex=\"-1\">"
14330 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.normal)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14331 + "</a></li>\n <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h1\" tabindex=\"-1\">"
14332 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h1)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14333 + "</a></li>\n <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h2\" tabindex=\"-1\">"
14334 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h2)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14335 + "</a></li>\n <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h3\" tabindex=\"-1\">"
14336 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h3)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14337 + "</a></li>\n <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h4\" tabindex=\"-1\">"
14338 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h4)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14339 + "</a></li>\n <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h5\" tabindex=\"-1\">"
14340 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h5)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14341 + "</a></li>\n <li><a data-wysihtml5-command=\"formatBlock\" data-wysihtml5-command-value=\"h6\" tabindex=\"-1\">"
14342 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.font_styles)),stack1 == null || stack1 === false ? stack1 : stack1.h6)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14343 + "</a></li>\n </ul>\n</li>\n";
14347 this["wysihtml5"]["tpl"]["html"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14348 this.compilerInfo = [4,'>= 1.0.0'];
14349 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14350 var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14352 function program1(depth0,data) {
14354 var buffer = "", stack1;
14356 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14360 function program3(depth0,data) {
14363 return "\n <span class=\"fa fa-pencil\"></span>\n ";
14366 function program5(depth0,data) {
14369 return "\n <span class=\"glyphicon glyphicon-pencil\"></span>\n ";
14372 buffer += "<li>\n <div class=\"btn-group\">\n <a class=\"btn ";
14373 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14374 if(stack1 || stack1 === 0) { buffer += stack1; }
14375 buffer += " btn-default\" data-wysihtml5-action=\"change_view\" title=\""
14376 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.html)),stack1 == null || stack1 === false ? stack1 : stack1.edit)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14377 + "\" tabindex=\"-1\">\n ";
14378 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(5, program5, data),fn:self.program(3, program3, data),data:data});
14379 if(stack1 || stack1 === 0) { buffer += stack1; }
14380 buffer += "\n </a>\n </div>\n</li>\n";
14384 this["wysihtml5"]["tpl"]["image"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14385 this.compilerInfo = [4,'>= 1.0.0'];
14386 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14387 var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14389 function program1(depth0,data) {
14395 function program3(depth0,data) {
14397 var buffer = "", stack1;
14399 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14403 function program5(depth0,data) {
14406 return "\n <span class=\"fa fa-file-image-o\"></span>\n ";
14409 function program7(depth0,data) {
14412 return "\n <span class=\"glyphicon glyphicon-picture\"></span>\n ";
14415 buffer += "<li>\n <div class=\"bootstrap-wysihtml5-insert-image-modal modal fade\" data-wysihtml5-dialog=\"insertImage\">\n <div class=\"modal-dialog ";
14416 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.smallmodals), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14417 if(stack1 || stack1 === 0) { buffer += stack1; }
14418 buffer += "\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <a class=\"close\" data-dismiss=\"modal\">×</a>\n <h3>"
14419 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.image)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14420 + "</h3>\n </div>\n <div class=\"modal-body\">\n <div class=\"form-group\">\n <input value=\"http://\" class=\"bootstrap-wysihtml5-insert-image-url form-control\" data-wysihtml5-dialog-field=\"src\">\n </div> \n </div>\n <div class=\"modal-footer\">\n <a class=\"btn btn-default\" data-dismiss=\"modal\" data-wysihtml5-dialog-action=\"cancel\" href=\"#\">"
14421 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.image)),stack1 == null || stack1 === false ? stack1 : stack1.cancel)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14422 + "</a>\n <a class=\"btn btn-primary\" data-dismiss=\"modal\" data-wysihtml5-dialog-action=\"save\" href=\"#\">"
14423 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.image)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14424 + "</a>\n </div>\n </div>\n </div>\n </div>\n <a class=\"btn ";
14425 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});
14426 if(stack1 || stack1 === 0) { buffer += stack1; }
14427 buffer += " btn-default\" data-wysihtml5-command=\"insertImage\" title=\""
14428 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.image)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14429 + "\" tabindex=\"-1\">\n ";
14430 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(7, program7, data),fn:self.program(5, program5, data),data:data});
14431 if(stack1 || stack1 === 0) { buffer += stack1; }
14432 buffer += "\n </a>\n</li>\n";
14436 this["wysihtml5"]["tpl"]["link"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14437 this.compilerInfo = [4,'>= 1.0.0'];
14438 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14439 var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14441 function program1(depth0,data) {
14447 function program3(depth0,data) {
14449 var buffer = "", stack1;
14451 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14455 function program5(depth0,data) {
14458 return "\n <span class=\"fa fa-share-square-o\"></span>\n ";
14461 function program7(depth0,data) {
14464 return "\n <span class=\"glyphicon glyphicon-share\"></span>\n ";
14467 buffer += "<li>\n <div class=\"bootstrap-wysihtml5-insert-link-modal modal fade\" data-wysihtml5-dialog=\"createLink\">\n <div class=\"modal-dialog ";
14468 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.smallmodals), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14469 if(stack1 || stack1 === 0) { buffer += stack1; }
14470 buffer += "\">\n <div class=\"modal-content\">\n <div class=\"modal-header\">\n <a class=\"close\" data-dismiss=\"modal\">×</a>\n <h3>"
14471 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.link)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14472 + "</h3>\n </div>\n <div class=\"modal-body\">\n <div class=\"form-group\">\n <input value=\"http://\" class=\"bootstrap-wysihtml5-insert-link-url form-control\" data-wysihtml5-dialog-field=\"href\">\n </div> \n <div class=\"checkbox\">\n <label> \n <input type=\"checkbox\" class=\"bootstrap-wysihtml5-insert-link-target\" checked>"
14473 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.link)),stack1 == null || stack1 === false ? stack1 : stack1.target)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14474 + "\n </label>\n </div>\n </div>\n <div class=\"modal-footer\">\n <a class=\"btn btn-default\" data-dismiss=\"modal\" data-wysihtml5-dialog-action=\"cancel\" href=\"#\">"
14475 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.link)),stack1 == null || stack1 === false ? stack1 : stack1.cancel)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14476 + "</a>\n <a href=\"#\" class=\"btn btn-primary\" data-dismiss=\"modal\" data-wysihtml5-dialog-action=\"save\">"
14477 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.link)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14478 + "</a>\n </div>\n </div>\n </div>\n </div>\n <a class=\"btn ";
14479 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(3, program3, data),data:data});
14480 if(stack1 || stack1 === 0) { buffer += stack1; }
14481 buffer += " btn-default\" data-wysihtml5-command=\"createLink\" title=\""
14482 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.link)),stack1 == null || stack1 === false ? stack1 : stack1.insert)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14483 + "\" tabindex=\"-1\">\n ";
14484 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(7, program7, data),fn:self.program(5, program5, data),data:data});
14485 if(stack1 || stack1 === 0) { buffer += stack1; }
14486 buffer += "\n </a>\n</li>\n";
14490 this["wysihtml5"]["tpl"]["lists"] = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
14491 this.compilerInfo = [4,'>= 1.0.0'];
14492 helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
14493 var buffer = "", stack1, functionType="function", escapeExpression=this.escapeExpression, self=this;
14495 function program1(depth0,data) {
14497 var buffer = "", stack1;
14499 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1));
14503 function program3(depth0,data) {
14506 return "\n <span class=\"fa fa-list-ul\"></span>\n ";
14509 function program5(depth0,data) {
14512 return "\n <span class=\"glyphicon glyphicon-list\"></span>\n ";
14515 function program7(depth0,data) {
14518 return "\n <span class=\"fa fa-list-ol\"></span>\n ";
14521 function program9(depth0,data) {
14524 return "\n <span class=\"glyphicon glyphicon-th-list\"></span>\n ";
14527 function program11(depth0,data) {
14530 return "\n <span class=\"fa fa-outdent\"></span>\n ";
14533 function program13(depth0,data) {
14536 return "\n <span class=\"glyphicon glyphicon-indent-right\"></span>\n ";
14539 function program15(depth0,data) {
14542 return "\n <span class=\"fa fa-indent\"></span>\n ";
14545 function program17(depth0,data) {
14548 return "\n <span class=\"glyphicon glyphicon-indent-left\"></span>\n ";
14551 buffer += "<li>\n <div class=\"btn-group\">\n <a class=\"btn ";
14552 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14553 if(stack1 || stack1 === 0) { buffer += stack1; }
14554 buffer += " btn-default\" data-wysihtml5-command=\"insertUnorderedList\" title=\""
14555 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.lists)),stack1 == null || stack1 === false ? stack1 : stack1.unordered)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14556 + "\" tabindex=\"-1\">\n ";
14557 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(5, program5, data),fn:self.program(3, program3, data),data:data});
14558 if(stack1 || stack1 === 0) { buffer += stack1; }
14559 buffer += "\n </a>\n <a class=\"btn ";
14560 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14561 if(stack1 || stack1 === 0) { buffer += stack1; }
14562 buffer += " btn-default\" data-wysihtml5-command=\"insertOrderedList\" title=\""
14563 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.lists)),stack1 == null || stack1 === false ? stack1 : stack1.ordered)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14564 + "\" tabindex=\"-1\">\n ";
14565 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(9, program9, data),fn:self.program(7, program7, data),data:data});
14566 if(stack1 || stack1 === 0) { buffer += stack1; }
14567 buffer += "\n </a>\n <a class=\"btn ";
14568 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14569 if(stack1 || stack1 === 0) { buffer += stack1; }
14570 buffer += " btn-default\" data-wysihtml5-command=\"Outdent\" title=\""
14571 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.lists)),stack1 == null || stack1 === false ? stack1 : stack1.outdent)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14572 + "\" tabindex=\"-1\">\n ";
14573 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(13, program13, data),fn:self.program(11, program11, data),data:data});
14574 if(stack1 || stack1 === 0) { buffer += stack1; }
14575 buffer += "\n </a>\n <a class=\"btn ";
14576 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.size), {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
14577 if(stack1 || stack1 === 0) { buffer += stack1; }
14578 buffer += " btn-default\" data-wysihtml5-command=\"Indent\" title=\""
14579 + escapeExpression(((stack1 = ((stack1 = ((stack1 = (depth0 && depth0.locale)),stack1 == null || stack1 === false ? stack1 : stack1.lists)),stack1 == null || stack1 === false ? stack1 : stack1.indent)),typeof stack1 === functionType ? stack1.apply(depth0) : stack1))
14580 + "\" tabindex=\"-1\">\n ";
14581 stack1 = helpers['if'].call(depth0, ((stack1 = ((stack1 = (depth0 && depth0.options)),stack1 == null || stack1 === false ? stack1 : stack1.toolbar)),stack1 == null || stack1 === false ? stack1 : stack1.fa), {hash:{},inverse:self.program(17, program17, data),fn:self.program(15, program15, data),data:data});
14582 if(stack1 || stack1 === 0) { buffer += stack1; }
14583 buffer += "\n </a>\n </div>\n</li>\n";
14585 });(function (factory) {
14587 if (typeof define === 'function' && define.amd) {
14588 // AMD. Register as an anonymous module.
14589 define('bootstrap.wysihtml5', ['jquery', 'wysihtml5', 'bootstrap', 'bootstrap.wysihtml5.templates', 'bootstrap.wysihtml5.commands'], factory);
14592 factory(jQuery, wysihtml5); // jshint ignore:line
14594 }(function ($, wysihtml5) {
14596 var bsWysihtml5 = function($, wysihtml5) {
14598 var templates = function(key, locale, options) {
14599 if(wysihtml5.tpl[key]) {
14600 return wysihtml5.tpl[key]({locale: locale, options: options});
14604 var Wysihtml5 = function(el, options) {
14606 var toolbarOpts = $.extend(true, {}, defaultOptions, options);
14607 for(var t in toolbarOpts.customTemplates) {
14608 if (toolbarOpts.customTemplates.hasOwnProperty(t)) {
14609 wysihtml5.tpl[t] = toolbarOpts.customTemplates[t];
14612 this.toolbar = this.createToolbar(el, toolbarOpts);
14613 this.editor = this.createEditor(toolbarOpts);
14616 Wysihtml5.prototype = {
14618 constructor: Wysihtml5,
14620 createEditor: function(options) {
14621 options = options || {};
14623 // Add the toolbar to a clone of the options object so multiple instances
14624 // of the WYISYWG don't break because 'toolbar' is already defined
14625 options = $.extend(true, {}, options);
14626 options.toolbar = this.toolbar[0];
14628 this.initializeEditor(this.el[0], options);
14632 initializeEditor: function(el, options) {
14633 var editor = new wysihtml5.Editor(this.el[0], options);
14635 editor.on('beforeload', this.syncBootstrapDialogEvents);
14636 editor.on('beforeload', this.loadParserRules);
14638 // #30 - body is in IE 10 not created by default, which leads to nullpointer
14639 // 2014/02/13 - adapted to wysihtml5-0.4, does not work in IE
14640 if(editor.composer.editableArea.contentDocument) {
14641 this.addMoreShortcuts(editor,
14642 editor.composer.editableArea.contentDocument.body || editor.composer.editableArea.contentDocument,
14643 options.shortcuts);
14645 this.addMoreShortcuts(editor, editor.composer.editableArea, options.shortcuts);
14648 if(options && options.events) {
14649 for(var eventName in options.events) {
14650 if (options.events.hasOwnProperty(eventName)) {
14651 editor.on(eventName, options.events[eventName]);
14659 loadParserRules: function() {
14660 if($.type(this.config.parserRules) === 'string') {
14663 url: this.config.parserRules,
14665 error: function (jqXHR, textStatus, errorThrown) {
14666 console.log(errorThrown);
14668 success: function (parserRules) {
14669 this.config.parserRules = parserRules;
14670 console.log('parserrules loaded');
14675 if(this.config.pasteParserRulesets && $.type(this.config.pasteParserRulesets) === 'string') {
14678 url: this.config.pasteParserRulesets,
14680 error: function (jqXHR, textStatus, errorThrown) {
14681 console.log(errorThrown);
14683 success: function (pasteParserRulesets) {
14684 this.config.pasteParserRulesets = pasteParserRulesets;
14690 //sync wysihtml5 events for dialogs with bootstrap events
14691 syncBootstrapDialogEvents: function() {
14693 $.map(this.toolbar.commandMapping, function(value) {
14695 }).filter(function(commandObj) {
14696 return commandObj.dialog;
14697 }).map(function(commandObj) {
14698 return commandObj.dialog;
14699 }).forEach(function(dialog) {
14700 dialog.on('show', function() {
14701 $(this.container).modal('show');
14703 dialog.on('hide', function() {
14704 $(this.container).modal('hide');
14705 setTimeout(editor.composer.focus, 0);
14707 $(dialog.container).on('shown.bs.modal', function () {
14708 $(this).find('input, select, textarea').first().focus();
14711 this.on('change_view', function() {
14712 $(this.toolbar.container.children).find('a.btn').not('[data-wysihtml5-action="change_view"]').toggleClass('disabled');
14716 createToolbar: function(el, options) {
14718 var toolbar = $('<ul/>', {
14719 'class' : 'wysihtml5-toolbar',
14720 'style': 'display:none'
14722 var culture = options.locale || defaultOptions.locale || 'en';
14723 if(!locale.hasOwnProperty(culture)) {
14724 console.debug('Locale \'' + culture + '\' not found. Available locales are: ' + Object.keys(locale) + '. Falling back to \'en\'.');
14727 var localeObject = $.extend(true, {}, locale.en, locale[culture]);
14728 for(var key in options.toolbar) {
14729 if(options.toolbar[key]) {
14730 toolbar.append(templates(key, localeObject, options));
14734 toolbar.find('a[data-wysihtml5-command="formatBlock"]').click(function(e) {
14735 var target = e.delegateTarget || e.target || e.srcElement,
14737 showformat = el.data('wysihtml5-display-format-name'),
14738 formatname = el.data('wysihtml5-format-name') || el.html();
14739 if(showformat === undefined || showformat === 'true') {
14740 self.toolbar.find('.current-font').text(formatname);
14744 toolbar.find('a[data-wysihtml5-command="foreColor"]').click(function(e) {
14745 var target = e.target || e.srcElement;
14746 var el = $(target);
14747 self.toolbar.find('.current-color').text(el.html());
14750 this.el.before(toolbar);
14755 addMoreShortcuts: function(editor, el, shortcuts) {
14756 /* some additional shortcuts */
14757 wysihtml5.dom.observe(el, 'keydown', function(event) {
14758 var keyCode = event.keyCode,
14759 command = shortcuts[keyCode];
14760 if ((event.ctrlKey || event.metaKey || event.altKey) && command && wysihtml5.commands[command]) {
14761 var commandObj = editor.toolbar.commandMapping[command + ':null'];
14762 if (commandObj && commandObj.dialog && !commandObj.state) {
14763 commandObj.dialog.show();
14765 wysihtml5.commands[command].exec(editor.composer, command);
14767 event.preventDefault();
14773 // these define our public api
14775 resetDefaults: function() {
14776 $.fn.wysihtml5.defaultOptions = $.extend(true, {}, $.fn.wysihtml5.defaultOptionsCache);
14778 bypassDefaults: function(options) {
14779 return this.each(function () {
14780 var $this = $(this);
14781 $this.data('wysihtml5', new Wysihtml5($this, options));
14784 shallowExtend: function (options) {
14785 var settings = $.extend({}, $.fn.wysihtml5.defaultOptions, options || {}, $(this).data());
14787 return methods.bypassDefaults.apply(that, [settings]);
14789 deepExtend: function(options) {
14790 var settings = $.extend(true, {}, $.fn.wysihtml5.defaultOptions, options || {});
14792 return methods.bypassDefaults.apply(that, [settings]);
14794 init: function(options) {
14796 return methods.shallowExtend.apply(that, [options]);
14800 $.fn.wysihtml5 = function ( method ) {
14801 if ( methods[method] ) {
14802 return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
14803 } else if ( typeof method === 'object' || ! method ) {
14804 return methods.init.apply( this, arguments );
14806 $.error( 'Method ' + method + ' does not exist on jQuery.wysihtml5' );
14810 $.fn.wysihtml5.Constructor = Wysihtml5;
14812 var defaultOptions = $.fn.wysihtml5.defaultOptions = {
14814 'font-styles': true,
14819 'blockquote': true,
14824 'smallmodals': false
14826 useLineBreaks: false,
14829 'wysiwyg-color-silver' : 1,
14830 'wysiwyg-color-gray' : 1,
14831 'wysiwyg-color-white' : 1,
14832 'wysiwyg-color-maroon' : 1,
14833 'wysiwyg-color-red' : 1,
14834 'wysiwyg-color-purple' : 1,
14835 'wysiwyg-color-fuchsia' : 1,
14836 'wysiwyg-color-green' : 1,
14837 'wysiwyg-color-lime' : 1,
14838 'wysiwyg-color-olive' : 1,
14839 'wysiwyg-color-yellow' : 1,
14840 'wysiwyg-color-navy' : 1,
14841 'wysiwyg-color-blue' : 1,
14842 'wysiwyg-color-teal' : 1,
14843 'wysiwyg-color-aqua' : 1,
14844 'wysiwyg-color-orange' : 1
14865 'check_attributes': {
14866 'width': 'numbers',
14869 'height': 'numbers'
14873 'check_attributes': {
14876 'set_attributes': {
14877 'target': '_blank',
14891 '75': 'createLink'// K
14895 if (typeof $.fn.wysihtml5.defaultOptionsCache === 'undefined') {
14896 $.fn.wysihtml5.defaultOptionsCache = $.extend(true, {}, $.fn.wysihtml5.defaultOptions);
14899 var locale = $.fn.wysihtml5.locale = {};
14901 bsWysihtml5($, wysihtml5);
14903 (function(wysihtml5) {
14904 wysihtml5.commands.small = {
14905 exec: function(composer, command) {
14906 return wysihtml5.commands.formatInline.exec(composer, command, "small");
14909 state: function(composer, command) {
14910 return wysihtml5.commands.formatInline.state(composer, command, "small");
14916 * English translation for bootstrap-wysihtml5
14918 (function (factory) {
14919 if (typeof define === 'function' && define.amd) {
14920 // AMD. Register as an anonymous module.
14921 define('bootstrap.wysihtml5.en-US', ['jquery', 'bootstrap.wysihtml5'], factory);
14927 $.fn.wysihtml5.locale.en = $.fn.wysihtml5.locale['en-US'] = {
14929 normal: 'Normal text',
14940 underline: 'Underline',
14944 unordered: 'Unordered list',
14945 ordered: 'Ordered list',
14946 outdent: 'Outdent',
14950 insert: 'Insert link',
14952 target: 'Open link in new window'
14955 insert: 'Insert image',