]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/static/AdminLTE-2.3.7/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.all.js
update sources to v12.1.0
[ceph.git] / ceph / src / pybind / mgr / dashboard / static / AdminLTE-2.3.7 / plugins / bootstrap-wysihtml5 / bootstrap3-wysihtml5.all.js
1 // TODO: in future try to replace most inline compability checks with polyfills for code readability
2
3 // element.textContent polyfill.
4 // Unsupporting browsers: IE8
5
6 if (Object.defineProperty && Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(Element.prototype, "textContent") && !Object.getOwnPropertyDescriptor(Element.prototype, "textContent").get) {
7 (function() {
8 var innerText = Object.getOwnPropertyDescriptor(Element.prototype, "innerText");
9 Object.defineProperty(Element.prototype, "textContent",
10 {
11 get: function() {
12 return innerText.get.call(this);
13 },
14 set: function(s) {
15 return innerText.set.call(this, s);
16 }
17 }
18 );
19 })();
20 }
21
22 // isArray polyfill for ie8
23 if(!Array.isArray) {
24 Array.isArray = function(arg) {
25 return Object.prototype.toString.call(arg) === '[object Array]';
26 };
27 };/**
28 * @license wysihtml5x v0.4.15
29 * https://github.com/Edicy/wysihtml5
30 *
31 * Author: Christopher Blum (https://github.com/tiff)
32 * Secondary author of extended features: Oliver Pulges (https://github.com/pulges)
33 *
34 * Copyright (C) 2012 XING AG
35 * Licensed under the MIT license (MIT)
36 *
37 */
38 var wysihtml5 = {
39 version: "0.4.15",
40
41 // namespaces
42 commands: {},
43 dom: {},
44 quirks: {},
45 toolbar: {},
46 lang: {},
47 selection: {},
48 views: {},
49
50 INVISIBLE_SPACE: "\uFEFF",
51
52 EMPTY_FUNCTION: function() {},
53
54 ELEMENT_NODE: 1,
55 TEXT_NODE: 3,
56
57 BACKSPACE_KEY: 8,
58 ENTER_KEY: 13,
59 ESCAPE_KEY: 27,
60 SPACE_KEY: 32,
61 DELETE_KEY: 46
62 };
63 ;/**
64 * Rangy, a cross-browser JavaScript range and selection library
65 * http://code.google.com/p/rangy/
66 *
67 * Copyright 2014, Tim Down
68 * Licensed under the MIT license.
69 * Version: 1.3alpha.20140804
70 * Build date: 4 August 2014
71 */
72
73 (function(factory, global) {
74 if (typeof define == "function" && define.amd) {
75 // AMD. Register as an anonymous module.
76 define(factory);
77 /*
78 TODO: look into this properly.
79
80 } else if (typeof exports == "object") {
81 // Node/CommonJS style for Browserify
82 module.exports = factory;
83 */
84 } else {
85 // No AMD or CommonJS support so we place Rangy in a global variable
86 global.rangy = factory();
87 }
88 })(function() {
89
90 var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
91
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"];
96
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"];
101
102 var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
103
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"];
107
108 /*----------------------------------------------------------------------------------------------------------------*/
109
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) {
113 var t = typeof o[p];
114 return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
115 }
116
117 function isHostObject(o, p) {
118 return !!(typeof o[p] == OBJECT && o[p]);
119 }
120
121 function isHostProperty(o, p) {
122 return typeof o[p] != UNDEFINED;
123 }
124
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;
129 while (i--) {
130 if (!testFunc(o, props[i])) {
131 return false;
132 }
133 }
134 return true;
135 };
136 }
137
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);
142
143 function isTextRange(range) {
144 return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
145 }
146
147 function getBody(doc) {
148 return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
149 }
150
151 var modules = {};
152
153 var api = {
154 version: "1.3alpha.20140804",
155 initialized: false,
156 supported: true,
157
158 util: {
159 isHostMethod: isHostMethod,
160 isHostObject: isHostObject,
161 isHostProperty: isHostProperty,
162 areHostMethods: areHostMethods,
163 areHostObjects: areHostObjects,
164 areHostProperties: areHostProperties,
165 isTextRange: isTextRange,
166 getBody: getBody
167 },
168
169 features: {},
170
171 modules: modules,
172 config: {
173 alertOnFail: true,
174 alertOnWarn: false,
175 preferTextRange: false,
176 autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
177 }
178 };
179
180 function consoleLog(msg) {
181 if (isHostObject(window, "console") && isHostMethod(window.console, "log")) {
182 window.console.log(msg);
183 }
184 }
185
186 function alertOrLog(msg, shouldAlert) {
187 if (shouldAlert) {
188 window.alert(msg);
189 } else {
190 consoleLog(msg);
191 }
192 }
193
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);
198 }
199
200 api.fail = fail;
201
202 function warn(msg) {
203 alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
204 }
205
206 api.warn = warn;
207
208 // Add utility extend() method
209 if ({}.hasOwnProperty) {
210 api.util.extend = function(obj, props, deep) {
211 var o, p;
212 for (var i in props) {
213 if (props.hasOwnProperty(i)) {
214 o = obj[i];
215 p = props[i];
216 if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
217 api.util.extend(o, p, true);
218 }
219 obj[i] = p;
220 }
221 }
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;
225 }
226 return obj;
227 };
228 } else {
229 fail("hasOwnProperty not supported");
230 }
231
232 // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
233 (function() {
234 var el = document.createElement("div");
235 el.appendChild(document.createElement("span"));
236 var slice = [].slice;
237 var toArray;
238 try {
239 if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
240 toArray = function(arrayLike) {
241 return slice.call(arrayLike, 0);
242 };
243 }
244 } catch (e) {}
245
246 if (!toArray) {
247 toArray = function(arrayLike) {
248 var arr = [];
249 for (var i = 0, len = arrayLike.length; i < len; ++i) {
250 arr[i] = arrayLike[i];
251 }
252 return arr;
253 };
254 }
255
256 api.util.toArray = toArray;
257 })();
258
259
260 // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
261 // normalization of event properties
262 var addListener;
263 if (isHostMethod(document, "addEventListener")) {
264 addListener = function(obj, eventType, listener) {
265 obj.addEventListener(eventType, listener, false);
266 };
267 } else if (isHostMethod(document, "attachEvent")) {
268 addListener = function(obj, eventType, listener) {
269 obj.attachEvent("on" + eventType, listener);
270 };
271 } else {
272 fail("Document does not have required addEventListener or attachEvent method");
273 }
274
275 api.util.addListener = addListener;
276
277 var initListeners = [];
278
279 function getErrorDesc(ex) {
280 return ex.message || ex.description || String(ex);
281 }
282
283 // Initialization
284 function init() {
285 if (api.initialized) {
286 return;
287 }
288 var testRange;
289 var implementsDomRange = false, implementsTextRange = false;
290
291 // First, perform basic feature tests
292
293 if (isHostMethod(document, "createRange")) {
294 testRange = document.createRange();
295 if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
296 implementsDomRange = true;
297 }
298 }
299
300 var body = getBody(document);
301 if (!body || body.nodeName.toLowerCase() != "body") {
302 fail("No body element found");
303 return;
304 }
305
306 if (body && isHostMethod(body, "createTextRange")) {
307 testRange = body.createTextRange();
308 if (isTextRange(testRange)) {
309 implementsTextRange = true;
310 }
311 }
312
313 if (!implementsDomRange && !implementsTextRange) {
314 fail("Neither Range nor TextRange are available");
315 return;
316 }
317
318 api.initialized = true;
319 api.features = {
320 implementsDomRange: implementsDomRange,
321 implementsTextRange: implementsTextRange
322 };
323
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);
329 }
330 }
331
332 // Call init listeners
333 for (var i = 0, len = initListeners.length; i < len; ++i) {
334 try {
335 initListeners[i](api);
336 } catch (ex) {
337 errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
338 consoleLog(errorMessage);
339 }
340 }
341 }
342
343 // Allow external scripts to initialize this library in case it's loaded after the document has loaded
344 api.init = init;
345
346 // Execute listener immediately if already initialized
347 api.addInitListener = function(listener) {
348 if (api.initialized) {
349 listener(api);
350 } else {
351 initListeners.push(listener);
352 }
353 };
354
355 var shimListeners = [];
356
357 api.addShimListener = function(listener) {
358 shimListeners.push(listener);
359 };
360
361 function shim(win) {
362 win = win || window;
363 init();
364
365 // Notify listeners
366 for (var i = 0, len = shimListeners.length; i < len; ++i) {
367 shimListeners[i](win);
368 }
369 }
370
371 api.shim = api.createMissingNativeApi = shim;
372
373 function Module(name, dependencies, initializer) {
374 this.name = name;
375 this.dependencies = dependencies;
376 this.initialized = false;
377 this.supported = false;
378 this.initializer = initializer;
379 }
380
381 Module.prototype = {
382 init: function() {
383 var requiredModuleNames = this.dependencies || [];
384 for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
385 moduleName = requiredModuleNames[i];
386
387 requiredModule = modules[moduleName];
388 if (!requiredModule || !(requiredModule instanceof Module)) {
389 throw new Error("required module '" + moduleName + "' not found");
390 }
391
392 requiredModule.init();
393
394 if (!requiredModule.supported) {
395 throw new Error("required module '" + moduleName + "' not supported");
396 }
397 }
398
399 // Now run initializer
400 this.initializer(this);
401 },
402
403 fail: function(reason) {
404 this.initialized = true;
405 this.supported = false;
406 throw new Error("Module '" + this.name + "' failed to load: " + reason);
407 },
408
409 warn: function(msg) {
410 api.warn("Module " + this.name + ": " + msg);
411 },
412
413 deprecationNotice: function(deprecated, replacement) {
414 api.warn("DEPRECATED: " + deprecated + " in module " + this.name + "is deprecated. Please use " +
415 replacement + " instead");
416 },
417
418 createError: function(msg) {
419 return new Error("Error in Rangy " + this.name + " module: " + msg);
420 }
421 };
422
423 function createModule(isCore, name, dependencies, initFunc) {
424 var newModule = new Module(name, dependencies, function(module) {
425 if (!module.initialized) {
426 module.initialized = true;
427 try {
428 initFunc(api, module);
429 module.supported = true;
430 } catch (ex) {
431 var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
432 consoleLog(errorMessage);
433 }
434 }
435 });
436 modules[name] = newModule;
437 }
438
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];
444 dependencies = [];
445 } else {
446 initFunc = arguments[2];
447 dependencies = arguments[1];
448 }
449
450 var module = createModule(false, name, dependencies, initFunc);
451
452 // Initialize the module immediately if the core is already initialized
453 if (api.initialized) {
454 module.init();
455 }
456 };
457
458 api.createCoreModule = function(name, dependencies, initFunc) {
459 createModule(true, name, dependencies, initFunc);
460 };
461
462 /*----------------------------------------------------------------------------------------------------------------*/
463
464 // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
465
466 function RangePrototype() {}
467 api.RangePrototype = RangePrototype;
468 api.rangePrototype = new RangePrototype();
469
470 function SelectionPrototype() {}
471 api.selectionPrototype = new SelectionPrototype();
472
473 /*----------------------------------------------------------------------------------------------------------------*/
474
475 // Wait for document to load before running tests
476
477 var docReady = false;
478
479 var loadHandler = function(e) {
480 if (!docReady) {
481 docReady = true;
482 if (!api.initialized && api.config.autoInitialize) {
483 init();
484 }
485 }
486 };
487
488 // Test whether we have window and document objects that we will need
489 if (typeof window == UNDEFINED) {
490 fail("No window found");
491 return;
492 }
493 if (typeof document == UNDEFINED) {
494 fail("No document found");
495 return;
496 }
497
498 if (isHostMethod(document, "addEventListener")) {
499 document.addEventListener("DOMContentLoaded", loadHandler, false);
500 }
501
502 // Add a fallback in case the DOMContentLoaded event isn't supported
503 addListener(window, "load", loadHandler);
504
505 /*----------------------------------------------------------------------------------------------------------------*/
506
507 // DOM utility methods used by Rangy
508 api.createCoreModule("DomUtil", [], function(api, module) {
509 var UNDEF = "undefined";
510 var util = api.util;
511
512 // Perform feature tests
513 if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
514 module.fail("document missing a Node creation method");
515 }
516
517 if (!util.isHostMethod(document, "getElementsByTagName")) {
518 module.fail("document missing getElementsByTagName method");
519 }
520
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");
525 }
526
527 // innerHTML is required for Range's createContextualFragment method
528 if (!util.isHostProperty(el, "innerHTML")) {
529 module.fail("Element is missing innerHTML property");
530 }
531
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");
537 }
538
539 /*----------------------------------------------------------------------------------------------------------------*/
540
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 ?
545 function(arr, val) {
546 return arr.indexOf(val) > -1;
547 }:*/
548
549 function(arr, val) {
550 var i = arr.length;
551 while (i--) {
552 if (arr[i] === val) {
553 return true;
554 }
555 }
556 return false;
557 };
558
559 // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
560 function isHtmlNamespace(node) {
561 var ns;
562 return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
563 }
564
565 function parentElement(node) {
566 var parent = node.parentNode;
567 return (parent.nodeType == 1) ? parent : null;
568 }
569
570 function getNodeIndex(node) {
571 var i = 0;
572 while( (node = node.previousSibling) ) {
573 ++i;
574 }
575 return i;
576 }
577
578 function getNodeLength(node) {
579 switch (node.nodeType) {
580 case 7:
581 case 10:
582 return 0;
583 case 3:
584 case 8:
585 return node.length;
586 default:
587 return node.childNodes.length;
588 }
589 }
590
591 function getCommonAncestor(node1, node2) {
592 var ancestors = [], n;
593 for (n = node1; n; n = n.parentNode) {
594 ancestors.push(n);
595 }
596
597 for (n = node2; n; n = n.parentNode) {
598 if (arrayContains(ancestors, n)) {
599 return n;
600 }
601 }
602
603 return null;
604 }
605
606 function isAncestorOf(ancestor, descendant, selfIsAncestor) {
607 var n = selfIsAncestor ? descendant : descendant.parentNode;
608 while (n) {
609 if (n === ancestor) {
610 return true;
611 } else {
612 n = n.parentNode;
613 }
614 }
615 return false;
616 }
617
618 function isOrIsAncestorOf(ancestor, descendant) {
619 return isAncestorOf(ancestor, descendant, true);
620 }
621
622 function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
623 var p, n = selfIsAncestor ? node : node.parentNode;
624 while (n) {
625 p = n.parentNode;
626 if (p === ancestor) {
627 return n;
628 }
629 n = p;
630 }
631 return null;
632 }
633
634 function isCharacterDataNode(node) {
635 var t = node.nodeType;
636 return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
637 }
638
639 function isTextOrCommentNode(node) {
640 if (!node) {
641 return false;
642 }
643 var t = node.nodeType;
644 return t == 3 || t == 8 ; // Text or Comment
645 }
646
647 function insertAfter(node, precedingNode) {
648 var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
649 if (nextNode) {
650 parent.insertBefore(node, nextNode);
651 } else {
652 parent.appendChild(node);
653 }
654 return node;
655 }
656
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);
663
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;
671 }
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)) {
674 ++position.offset;
675 }
676 }
677 }
678 return newNode;
679 }
680
681 function getDocument(node) {
682 if (node.nodeType == 9) {
683 return node;
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);
690 } else {
691 throw module.createError("getDocument: no document found for node");
692 }
693 }
694
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;
701 } else {
702 throw module.createError("Cannot get a window object for node");
703 }
704 }
705
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;
711 } else {
712 throw module.createError("getIframeDocument: No Document object found for iframe element");
713 }
714 }
715
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;
721 } else {
722 throw module.createError("getIframeWindow: No Window object found for iframe element");
723 }
724 }
725
726 // This looks bad. Is it worth it?
727 function isWindow(obj) {
728 return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
729 }
730
731 function getContentDocument(obj, module, methodName) {
732 var doc;
733
734 if (!obj) {
735 doc = document;
736 }
737
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);
742 }
743
744 // Test if the doc parameter appears to be a Window object
745 else if (isWindow(obj)) {
746 doc = obj.document;
747 }
748
749 if (!doc) {
750 throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
751 }
752
753 return doc;
754 }
755
756 function getRootContainer(node) {
757 var parent;
758 while ( (parent = node.parentNode) ) {
759 node = parent;
760 }
761 return node;
762 }
763
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;
776 } else {
777 root = getCommonAncestor(nodeA, nodeB);
778 if (!root) {
779 throw new Error("comparePoints error: nodes have no common ancestor");
780 }
781
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);
785
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!");
789 } else {
790 n = root.firstChild;
791 while (n) {
792 if (n === childA) {
793 return -1;
794 } else if (n === childB) {
795 return 1;
796 }
797 n = n.nextSibling;
798 }
799 }
800 }
801 }
802
803 /*----------------------------------------------------------------------------------------------------------------*/
804
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;
807
808 function isBrokenNode(node) {
809 var n;
810 try {
811 n = node.parentNode;
812 return false;
813 } catch (e) {
814 return true;
815 }
816 }
817
818 (function() {
819 var el = document.createElement("b");
820 el.innerHTML = "1";
821 var textNode = el.firstChild;
822 el.innerHTML = "<br>";
823 crashyTextNodes = isBrokenNode(textNode);
824
825 api.features.crashyTextNodes = crashyTextNodes;
826 })();
827
828 /*----------------------------------------------------------------------------------------------------------------*/
829
830 function inspectNode(node) {
831 if (!node) {
832 return "[No node]";
833 }
834 if (crashyTextNodes && isBrokenNode(node)) {
835 return "[Broken node]";
836 }
837 if (isCharacterDataNode(node)) {
838 return '"' + node.data + '"';
839 }
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) + "]";
843 }
844 return node.nodeName;
845 }
846
847 function fragmentFromNodeChildren(node) {
848 var fragment = getDocument(node).createDocumentFragment(), child;
849 while ( (child = node.firstChild) ) {
850 fragment.appendChild(child);
851 }
852 return fragment;
853 }
854
855 var getComputedStyleProperty;
856 if (typeof window.getComputedStyle != UNDEF) {
857 getComputedStyleProperty = function(el, propName) {
858 return getWindow(el).getComputedStyle(el, null)[propName];
859 };
860 } else if (typeof document.documentElement.currentStyle != UNDEF) {
861 getComputedStyleProperty = function(el, propName) {
862 return el.currentStyle[propName];
863 };
864 } else {
865 module.fail("No means of obtaining computed style properties found");
866 }
867
868 function NodeIterator(root) {
869 this.root = root;
870 this._next = root;
871 }
872
873 NodeIterator.prototype = {
874 _current: null,
875
876 hasNext: function() {
877 return !!this._next;
878 },
879
880 next: function() {
881 var n = this._current = this._next;
882 var child, next;
883 if (this._current) {
884 child = n.firstChild;
885 if (child) {
886 this._next = child;
887 } else {
888 next = null;
889 while ((n !== this.root) && !(next = n.nextSibling)) {
890 n = n.parentNode;
891 }
892 this._next = next;
893 }
894 }
895 return this._current;
896 },
897
898 detach: function() {
899 this._current = this._next = this.root = null;
900 }
901 };
902
903 function createIterator(root) {
904 return new NodeIterator(root);
905 }
906
907 function DomPosition(node, offset) {
908 this.node = node;
909 this.offset = offset;
910 }
911
912 DomPosition.prototype = {
913 equals: function(pos) {
914 return !!pos && this.node === pos.node && this.offset == pos.offset;
915 },
916
917 inspect: function() {
918 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
919 },
920
921 toString: function() {
922 return this.inspect();
923 }
924 };
925
926 function DOMException(codeName) {
927 this.code = this[codeName];
928 this.codeName = codeName;
929 this.message = "DOMException: " + this.codeName;
930 }
931
932 DOMException.prototype = {
933 INDEX_SIZE_ERR: 1,
934 HIERARCHY_REQUEST_ERR: 3,
935 WRONG_DOCUMENT_ERR: 4,
936 NO_MODIFICATION_ALLOWED_ERR: 7,
937 NOT_FOUND_ERR: 8,
938 NOT_SUPPORTED_ERR: 9,
939 INVALID_STATE_ERR: 11,
940 INVALID_NODE_TYPE_ERR: 24
941 };
942
943 DOMException.prototype.toString = function() {
944 return this.message;
945 };
946
947 api.dom = {
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,
966 isWindow: isWindow,
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
976 };
977
978 api.DOMException = DOMException;
979 });
980
981 /*----------------------------------------------------------------------------------------------------------------*/
982
983 // Pure JavaScript implementation of DOM Range
984 api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
985 var dom = api.dom;
986 var util = api.util;
987 var DomPosition = dom.DomPosition;
988 var DOMException = api.DOMException;
989
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;
1001
1002 /*----------------------------------------------------------------------------------------------------------------*/
1003
1004 // Utility functions
1005
1006 function isNonTextPartiallySelected(node, range) {
1007 return (node.nodeType != 3) &&
1008 (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
1009 }
1010
1011 function getRangeDocument(range) {
1012 return range.document || getDocument(range.startContainer);
1013 }
1014
1015 function getBoundaryBeforeNode(node) {
1016 return new DomPosition(node.parentNode, getNodeIndex(node));
1017 }
1018
1019 function getBoundaryAfterNode(node) {
1020 return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
1021 }
1022
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);
1028 } else {
1029 n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
1030 }
1031 } else if (o >= n.childNodes.length) {
1032 n.appendChild(node);
1033 } else {
1034 n.insertBefore(node, n.childNodes[o]);
1035 }
1036 return firstNodeInserted;
1037 }
1038
1039 function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
1040 assertRangeValid(rangeA);
1041 assertRangeValid(rangeB);
1042
1043 if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
1044 throw new DOMException("WRONG_DOCUMENT_ERR");
1045 }
1046
1047 var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
1048 endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
1049
1050 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1051 }
1052
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();
1062 }
1063
1064 if (node.nodeType == 10) { // DocumentType
1065 throw new DOMException("HIERARCHY_REQUEST_ERR");
1066 }
1067 frag.appendChild(node);
1068 }
1069 return frag;
1070 }
1071
1072 function iterateSubtree(rangeIterator, func, iteratorState) {
1073 var it, n;
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;
1079 return;
1080 } else {
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) {
1087 return;
1088 }
1089 }
1090 } else {
1091 // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1092 // descendants
1093 it = dom.createIterator(node);
1094 while ( (n = it.next()) ) {
1095 if (func(n) === false) {
1096 iteratorState.stop = true;
1097 return;
1098 }
1099 }
1100 }
1101 }
1102 }
1103
1104 function deleteSubtree(iterator) {
1105 var subIterator;
1106 while (iterator.next()) {
1107 if (iterator.isPartiallySelectedSubtree()) {
1108 subIterator = iterator.getSubtreeIterator();
1109 deleteSubtree(subIterator);
1110 subIterator.detach();
1111 } else {
1112 iterator.remove();
1113 }
1114 }
1115 }
1116
1117 function extractSubtree(iterator) {
1118 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1119
1120 if (iterator.isPartiallySelectedSubtree()) {
1121 node = node.cloneNode(false);
1122 subIterator = iterator.getSubtreeIterator();
1123 node.appendChild(extractSubtree(subIterator));
1124 subIterator.detach();
1125 } else {
1126 iterator.remove();
1127 }
1128 if (node.nodeType == 10) { // DocumentType
1129 throw new DOMException("HIERARCHY_REQUEST_ERR");
1130 }
1131 frag.appendChild(node);
1132 }
1133 return frag;
1134 }
1135
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("|") + ")$");
1141 }
1142
1143 var nodes = [];
1144 iterateSubtree(new RangeIterator(range, false), function(node) {
1145 if (filterNodeTypes && !regex.test(node.nodeType)) {
1146 return;
1147 }
1148 if (filterExists && !filter(node)) {
1149 return;
1150 }
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) {
1155 return;
1156 }
1157
1158 var ec = range.endContainer;
1159 if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
1160 return;
1161 }
1162
1163 nodes.push(node);
1164 });
1165 return nodes;
1166 }
1167
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 + ")]";
1172 }
1173
1174 /*----------------------------------------------------------------------------------------------------------------*/
1175
1176 // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
1177
1178 function RangeIterator(range, clonePartiallySelectedTextNodes) {
1179 this.range = range;
1180 this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
1181
1182
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;
1189
1190 if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
1191 this.isSingleCharacterDataNode = true;
1192 this._first = this._last = this._next = this.sc;
1193 } else {
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);
1198 }
1199 }
1200 }
1201
1202 RangeIterator.prototype = {
1203 _current: null,
1204 _next: null,
1205 _first: null,
1206 _last: null,
1207 isSingleCharacterDataNode: false,
1208
1209 reset: function() {
1210 this._current = null;
1211 this._next = this._first;
1212 },
1213
1214 hasNext: function() {
1215 return !!this._next;
1216 },
1217
1218 next: function() {
1219 // Move to next node
1220 var current = this._current = this._next;
1221 if (current) {
1222 this._next = (current !== this._last) ? current.nextSibling : null;
1223
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);
1228 }
1229 if (this._current === this.sc) {
1230 (current = current.cloneNode(true)).deleteData(0, this.so);
1231 }
1232 }
1233 }
1234
1235 return current;
1236 },
1237
1238 remove: function() {
1239 var current = this._current, start, end;
1240
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;
1244 if (start != end) {
1245 current.deleteData(start, end - start);
1246 }
1247 } else {
1248 if (current.parentNode) {
1249 current.parentNode.removeChild(current);
1250 } else {
1251 }
1252 }
1253 },
1254
1255 // Checks if the current node is partially selected
1256 isPartiallySelectedSubtree: function() {
1257 var current = this._current;
1258 return isNonTextPartiallySelected(current, this.range);
1259 },
1260
1261 getSubtreeIterator: function() {
1262 var subRange;
1263 if (this.isSingleCharacterDataNode) {
1264 subRange = this.range.cloneRange();
1265 subRange.collapse(false);
1266 } else {
1267 subRange = new Range(getRangeDocument(this.range));
1268 var current = this._current;
1269 var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
1270
1271 if (isOrIsAncestorOf(current, this.sc)) {
1272 startContainer = this.sc;
1273 startOffset = this.so;
1274 }
1275 if (isOrIsAncestorOf(current, this.ec)) {
1276 endContainer = this.ec;
1277 endOffset = this.eo;
1278 }
1279
1280 updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
1281 }
1282 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
1283 },
1284
1285 detach: function() {
1286 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
1287 }
1288 };
1289
1290 /*----------------------------------------------------------------------------------------------------------------*/
1291
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];
1297
1298 function createAncestorFinder(nodeTypes) {
1299 return function(node, selfIsAncestor) {
1300 var t, n = selfIsAncestor ? node : node.parentNode;
1301 while (n) {
1302 t = n.nodeType;
1303 if (arrayContains(nodeTypes, t)) {
1304 return n;
1305 }
1306 n = n.parentNode;
1307 }
1308 return null;
1309 };
1310 }
1311
1312 var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1313 var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1314 var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1315
1316 function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1317 if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1318 throw new DOMException("INVALID_NODE_TYPE_ERR");
1319 }
1320 }
1321
1322 function assertValidNodeType(node, invalidTypes) {
1323 if (!arrayContains(invalidTypes, node.nodeType)) {
1324 throw new DOMException("INVALID_NODE_TYPE_ERR");
1325 }
1326 }
1327
1328 function assertValidOffset(node, offset) {
1329 if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1330 throw new DOMException("INDEX_SIZE_ERR");
1331 }
1332 }
1333
1334 function assertSameDocumentOrFragment(node1, node2) {
1335 if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1336 throw new DOMException("WRONG_DOCUMENT_ERR");
1337 }
1338 }
1339
1340 function assertNodeNotReadOnly(node) {
1341 if (getReadonlyAncestor(node, true)) {
1342 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1343 }
1344 }
1345
1346 function assertNode(node, codeName) {
1347 if (!node) {
1348 throw new DOMException(codeName);
1349 }
1350 }
1351
1352 function isOrphan(node) {
1353 return (crashyTextNodes && dom.isBrokenNode(node)) ||
1354 !arrayContains(rootContainerNodeTypes, node.nodeType) && !getDocumentOrFragmentContainer(node, true);
1355 }
1356
1357 function isValidOffset(node, offset) {
1358 return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1359 }
1360
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));
1367 }
1368
1369 function assertRangeValid(range) {
1370 if (!isRangeValid(range)) {
1371 throw new Error("Range error: Range is no longer valid after DOM mutation (" + range.inspect() + ")");
1372 }
1373 }
1374
1375 /*----------------------------------------------------------------------------------------------------------------*/
1376
1377 // Test the browser's innerHTML support to decide how to implement createContextualFragment
1378 var styleEl = document.createElement("style");
1379 var htmlParsingConforms = false;
1380 try {
1381 styleEl.innerHTML = "<b>x</b>";
1382 htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1383 } catch (e) {
1384 // IE 6 and 7 throw
1385 }
1386
1387 api.features.htmlParsingConforms = htmlParsingConforms;
1388
1389 var createContextualFragment = htmlParsingConforms ?
1390
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);
1399
1400 // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1401 // exception and abort these steps."
1402 if (!node) {
1403 throw new DOMException("INVALID_STATE_ERR");
1404 }
1405
1406 // "Let element be as follows, depending on node's interface:"
1407 // Document, Document Fragment: null
1408 var el = null;
1409
1410 // "Element: node"
1411 if (node.nodeType == 1) {
1412 el = node;
1413
1414 // "Text, Comment: node's parentElement"
1415 } else if (isCharacterDataNode(node)) {
1416 el = dom.parentElement(node);
1417 }
1418
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
1421 // namespace"
1422 if (el === null || (
1423 el.nodeName == "HTML" &&
1424 dom.isHtmlNamespace(getDocument(el).documentElement) &&
1425 dom.isHtmlNamespace(el)
1426 )) {
1427
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");
1431 } else {
1432 el = el.cloneNode(false);
1433 }
1434
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;
1440
1441 // "If this raises an exception, then abort these steps. Otherwise, let new
1442 // children be the nodes returned."
1443
1444 // "Let fragment be a new DocumentFragment."
1445 // "Append all new children to fragment."
1446 // "Return fragment."
1447 return dom.fragmentFromNodeChildren(el);
1448 } :
1449
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;
1456
1457 return dom.fragmentFromNodeChildren(el);
1458 };
1459
1460 function splitRangeBoundaries(range, positionsToPreserve) {
1461 assertRangeValid(range);
1462
1463 var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
1464 var startEndSame = (sc === ec);
1465
1466 if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1467 splitDataNode(ec, eo, positionsToPreserve);
1468 }
1469
1470 if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1471 sc = splitDataNode(sc, so, positionsToPreserve);
1472 if (startEndSame) {
1473 eo -= so;
1474 ec = sc;
1475 } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
1476 eo++;
1477 }
1478 so = 0;
1479 }
1480 range.setStartAndEnd(sc, so, ec, eo);
1481 }
1482
1483 function rangeToHtml(range) {
1484 assertRangeValid(range);
1485 var container = range.commonAncestorContainer.parentNode.cloneNode(false);
1486 container.appendChild( range.cloneContents() );
1487 return container.innerHTML;
1488 }
1489
1490 /*----------------------------------------------------------------------------------------------------------------*/
1491
1492 var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1493 "commonAncestorContainer"];
1494
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;
1497
1498 util.extend(api.rangePrototype, {
1499 compareBoundaryPoints: function(how, range) {
1500 assertRangeValid(this);
1501 assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1502
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);
1511 },
1512
1513 insertNode: function(node) {
1514 assertRangeValid(this);
1515 assertValidNodeType(node, insertableNodeTypes);
1516 assertNodeNotReadOnly(this.startContainer);
1517
1518 if (isOrIsAncestorOf(node, this.startContainer)) {
1519 throw new DOMException("HIERARCHY_REQUEST_ERR");
1520 }
1521
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
1524 // to add the node
1525
1526 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1527 this.setStartBefore(firstNodeInserted);
1528 },
1529
1530 cloneContents: function() {
1531 assertRangeValid(this);
1532
1533 var clone, frag;
1534 if (this.collapsed) {
1535 return getRangeDocument(this).createDocumentFragment();
1536 } else {
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);
1542 return frag;
1543 } else {
1544 var iterator = new RangeIterator(this, true);
1545 clone = cloneSubtree(iterator);
1546 iterator.detach();
1547 }
1548 return clone;
1549 }
1550 },
1551
1552 canSurroundContents: function() {
1553 assertRangeValid(this);
1554 assertNodeNotReadOnly(this.startContainer);
1555 assertNodeNotReadOnly(this.endContainer);
1556
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)));
1562 iterator.detach();
1563 return !boundariesInvalid;
1564 },
1565
1566 surroundContents: function(node) {
1567 assertValidNodeType(node, surroundNodeTypes);
1568
1569 if (!this.canSurroundContents()) {
1570 throw new DOMException("INVALID_STATE_ERR");
1571 }
1572
1573 // Extract the contents
1574 var content = this.extractContents();
1575
1576 // Clear the children of the node
1577 if (node.hasChildNodes()) {
1578 while (node.lastChild) {
1579 node.removeChild(node.lastChild);
1580 }
1581 }
1582
1583 // Insert the new node and add the extracted contents
1584 insertNodeAtPosition(node, this.startContainer, this.startOffset);
1585 node.appendChild(content);
1586
1587 this.selectNode(node);
1588 },
1589
1590 cloneRange: function() {
1591 assertRangeValid(this);
1592 var range = new Range(getRangeDocument(this));
1593 var i = rangeProperties.length, prop;
1594 while (i--) {
1595 prop = rangeProperties[i];
1596 range[prop] = this[prop];
1597 }
1598 return range;
1599 },
1600
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) : "";
1606 } else {
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);
1612 }
1613 });
1614 iterator.detach();
1615 return textParts.join("");
1616 }
1617 },
1618
1619 // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
1620 // been removed from Mozilla.
1621
1622 compareNode: function(node) {
1623 assertRangeValid(this);
1624
1625 var parent = node.parentNode;
1626 var nodeIndex = getNodeIndex(node);
1627
1628 if (!parent) {
1629 throw new DOMException("NOT_FOUND_ERR");
1630 }
1631
1632 var startComparison = this.comparePoint(parent, nodeIndex),
1633 endComparison = this.comparePoint(parent, nodeIndex + 1);
1634
1635 if (startComparison < 0) { // Node starts before
1636 return (endComparison > 0) ? n_b_a : n_b;
1637 } else {
1638 return (endComparison > 0) ? n_a : n_i;
1639 }
1640 },
1641
1642 comparePoint: function(node, offset) {
1643 assertRangeValid(this);
1644 assertNode(node, "HIERARCHY_REQUEST_ERR");
1645 assertSameDocumentOrFragment(node, this.startContainer);
1646
1647 if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
1648 return -1;
1649 } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
1650 return 1;
1651 }
1652 return 0;
1653 },
1654
1655 createContextualFragment: createContextualFragment,
1656
1657 toHtml: function() {
1658 return rangeToHtml(this);
1659 },
1660
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)) {
1667 return false;
1668 }
1669
1670 var parent = node.parentNode, offset = getNodeIndex(node);
1671 assertNode(parent, "NOT_FOUND_ERR");
1672
1673 var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
1674 endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
1675
1676 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1677 },
1678
1679 isPointInRange: function(node, offset) {
1680 assertRangeValid(this);
1681 assertNode(node, "HIERARCHY_REQUEST_ERR");
1682 assertSameDocumentOrFragment(node, this.startContainer);
1683
1684 return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
1685 (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
1686 },
1687
1688 // The methods below are non-standard and invented by me.
1689
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);
1693 },
1694
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);
1698 },
1699
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);
1704
1705 var intersectionRange = this.cloneRange();
1706 if (startComparison == -1) {
1707 intersectionRange.setStart(range.startContainer, range.startOffset);
1708 }
1709 if (endComparison == 1) {
1710 intersectionRange.setEnd(range.endContainer, range.endOffset);
1711 }
1712 return intersectionRange;
1713 }
1714 return null;
1715 },
1716
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);
1722 }
1723 if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
1724 unionRange.setEnd(range.endContainer, range.endOffset);
1725 }
1726 return unionRange;
1727 } else {
1728 throw new DOMException("Ranges do not intersect");
1729 }
1730 },
1731
1732 containsNode: function(node, allowPartial) {
1733 if (allowPartial) {
1734 return this.intersectsNode(node, false);
1735 } else {
1736 return this.compareNode(node) == n_i;
1737 }
1738 },
1739
1740 containsNodeContents: function(node) {
1741 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
1742 },
1743
1744 containsRange: function(range) {
1745 var intersection = this.intersection(range);
1746 return intersection !== null && range.equals(intersection);
1747 },
1748
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);
1758 } else {
1759 return this.containsNodeContents(node);
1760 }
1761 },
1762
1763 getNodes: function(nodeTypes, filter) {
1764 assertRangeValid(this);
1765 return getNodesInRange(this, nodeTypes, filter);
1766 },
1767
1768 getDocument: function() {
1769 return getRangeDocument(this);
1770 },
1771
1772 collapseBefore: function(node) {
1773 this.setEndBefore(node);
1774 this.collapse(false);
1775 },
1776
1777 collapseAfter: function(node) {
1778 this.setStartAfter(node);
1779 this.collapse(true);
1780 },
1781
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;
1789 if (range) {
1790 preSelectionRange.setEnd(range.startContainer, range.startOffset);
1791 start = preSelectionRange.toString().length;
1792 end = start + range.toString().length;
1793 }
1794
1795 return {
1796 start: start,
1797 end: end,
1798 containerNode: containerNode
1799 };
1800 },
1801
1802 moveToBookmark: function(bookmark) {
1803 var containerNode = bookmark.containerNode;
1804 var charIndex = 0;
1805 this.setStart(containerNode, 0);
1806 this.collapse(true);
1807 var nodeStack = [containerNode], node, foundStart = false, stop = false;
1808 var nextCharIndex, i, childNodes;
1809
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);
1815 foundStart = true;
1816 }
1817 if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
1818 this.setEnd(node, bookmark.end - charIndex);
1819 stop = true;
1820 }
1821 charIndex = nextCharIndex;
1822 } else {
1823 childNodes = node.childNodes;
1824 i = childNodes.length;
1825 while (i--) {
1826 nodeStack.push(childNodes[i]);
1827 }
1828 }
1829 }
1830 },
1831
1832 getName: function() {
1833 return "DomRange";
1834 },
1835
1836 equals: function(range) {
1837 return Range.rangesEqual(this, range);
1838 },
1839
1840 isValid: function() {
1841 return isRangeValid(this);
1842 },
1843
1844 inspect: function() {
1845 return inspect(this);
1846 },
1847
1848 detach: function() {
1849 // In DOM4, detach() is now a no-op.
1850 }
1851 });
1852
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;
1858
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;
1863 }
1864
1865 function copyComparisonConstants(constructor) {
1866 copyComparisonConstantsToObject(constructor);
1867 copyComparisonConstantsToObject(constructor.prototype);
1868 }
1869
1870 function createRangeContentRemover(remover, boundaryUpdater) {
1871 return function() {
1872 assertRangeValid(this);
1873
1874 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
1875
1876 var iterator = new RangeIterator(this, true);
1877
1878 // Work out where to position the range after content removal
1879 var node, boundary;
1880 if (sc !== root) {
1881 node = getClosestAncestorIn(sc, root, true);
1882 boundary = getBoundaryAfterNode(node);
1883 sc = boundary.node;
1884 so = boundary.offset;
1885 }
1886
1887 // Check none of the range is read-only
1888 iterateSubtree(iterator, assertNodeNotReadOnly);
1889
1890 iterator.reset();
1891
1892 // Remove the content
1893 var returnValue = remover(iterator);
1894 iterator.detach();
1895
1896 // Move to the new position
1897 boundaryUpdater(this, sc, so, sc, so);
1898
1899 return returnValue;
1900 };
1901 }
1902
1903 function createPrototypeRange(constructor, boundaryUpdater) {
1904 function createBeforeAfterNodeSetter(isBefore, isStart) {
1905 return function(node) {
1906 assertValidNodeType(node, beforeAfterNodeTypes);
1907 assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
1908
1909 var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
1910 (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
1911 };
1912 }
1913
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) {
1920 ec = node;
1921 eo = offset;
1922 }
1923 boundaryUpdater(range, node, offset, ec, eo);
1924 }
1925 }
1926
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) {
1933 sc = node;
1934 so = offset;
1935 }
1936 boundaryUpdater(range, sc, so, node, offset);
1937 }
1938 }
1939
1940 // Set up inheritance
1941 var F = function() {};
1942 F.prototype = api.rangePrototype;
1943 constructor.prototype = new F();
1944
1945 util.extend(constructor.prototype, {
1946 setStart: function(node, offset) {
1947 assertNoDocTypeNotationEntityAncestor(node, true);
1948 assertValidOffset(node, offset);
1949
1950 setRangeStart(this, node, offset);
1951 },
1952
1953 setEnd: function(node, offset) {
1954 assertNoDocTypeNotationEntityAncestor(node, true);
1955 assertValidOffset(node, offset);
1956
1957 setRangeEnd(this, node, offset);
1958 },
1959
1960 /**
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
1967 */
1968 setStartAndEnd: function() {
1969 var args = arguments;
1970 var sc = args[0], so = args[1], ec = sc, eo = so;
1971
1972 switch (args.length) {
1973 case 3:
1974 eo = args[2];
1975 break;
1976 case 4:
1977 ec = args[2];
1978 eo = args[3];
1979 break;
1980 }
1981
1982 boundaryUpdater(this, sc, so, ec, eo);
1983 },
1984
1985 setBoundary: function(node, offset, isStart) {
1986 this["set" + (isStart ? "Start" : "End")](node, offset);
1987 },
1988
1989 setStartBefore: createBeforeAfterNodeSetter(true, true),
1990 setStartAfter: createBeforeAfterNodeSetter(false, true),
1991 setEndBefore: createBeforeAfterNodeSetter(true, false),
1992 setEndAfter: createBeforeAfterNodeSetter(false, false),
1993
1994 collapse: function(isStart) {
1995 assertRangeValid(this);
1996 if (isStart) {
1997 boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
1998 } else {
1999 boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
2000 }
2001 },
2002
2003 selectNodeContents: function(node) {
2004 assertNoDocTypeNotationEntityAncestor(node, true);
2005
2006 boundaryUpdater(this, node, 0, node, getNodeLength(node));
2007 },
2008
2009 selectNode: function(node) {
2010 assertNoDocTypeNotationEntityAncestor(node, false);
2011 assertValidNodeType(node, beforeAfterNodeTypes);
2012
2013 var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
2014 boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
2015 },
2016
2017 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
2018
2019 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
2020
2021 canSurroundContents: function() {
2022 assertRangeValid(this);
2023 assertNodeNotReadOnly(this.startContainer);
2024 assertNodeNotReadOnly(this.endContainer);
2025
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)));
2031 iterator.detach();
2032 return !boundariesInvalid;
2033 },
2034
2035 splitBoundaries: function() {
2036 splitRangeBoundaries(this);
2037 },
2038
2039 splitBoundariesPreservingPositions: function(positionsToPreserve) {
2040 splitRangeBoundaries(this, positionsToPreserve);
2041 },
2042
2043 normalizeBoundaries: function() {
2044 assertRangeValid(this);
2045
2046 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
2047
2048 var mergeForward = function(node) {
2049 var sibling = node.nextSibling;
2050 if (sibling && sibling.nodeType == node.nodeType) {
2051 ec = node;
2052 eo = node.length;
2053 node.appendData(sibling.data);
2054 sibling.parentNode.removeChild(sibling);
2055 }
2056 };
2057
2058 var mergeBackward = function(node) {
2059 var sibling = node.previousSibling;
2060 if (sibling && sibling.nodeType == node.nodeType) {
2061 sc = node;
2062 var nodeLength = node.length;
2063 so = sibling.length;
2064 node.insertData(0, sibling.data);
2065 sibling.parentNode.removeChild(sibling);
2066 if (sc == ec) {
2067 eo += so;
2068 ec = sc;
2069 } else if (ec == node.parentNode) {
2070 var nodeIndex = getNodeIndex(node);
2071 if (eo == nodeIndex) {
2072 ec = node;
2073 eo = nodeLength;
2074 } else if (eo > nodeIndex) {
2075 eo--;
2076 }
2077 }
2078 }
2079 };
2080
2081 var normalizeStart = true;
2082
2083 if (isCharacterDataNode(ec)) {
2084 if (ec.length == eo) {
2085 mergeForward(ec);
2086 }
2087 } else {
2088 if (eo > 0) {
2089 var endNode = ec.childNodes[eo - 1];
2090 if (endNode && isCharacterDataNode(endNode)) {
2091 mergeForward(endNode);
2092 }
2093 }
2094 normalizeStart = !this.collapsed;
2095 }
2096
2097 if (normalizeStart) {
2098 if (isCharacterDataNode(sc)) {
2099 if (so == 0) {
2100 mergeBackward(sc);
2101 }
2102 } else {
2103 if (so < sc.childNodes.length) {
2104 var startNode = sc.childNodes[so];
2105 if (startNode && isCharacterDataNode(startNode)) {
2106 mergeBackward(startNode);
2107 }
2108 }
2109 }
2110 } else {
2111 sc = ec;
2112 so = eo;
2113 }
2114
2115 boundaryUpdater(this, sc, so, ec, eo);
2116 },
2117
2118 collapseToPoint: function(node, offset) {
2119 assertNoDocTypeNotationEntityAncestor(node, true);
2120 assertValidOffset(node, offset);
2121 this.setStartAndEnd(node, offset);
2122 }
2123 });
2124
2125 copyComparisonConstants(constructor);
2126 }
2127
2128 /*----------------------------------------------------------------------------------------------------------------*/
2129
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);
2135 }
2136
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);
2143
2144 updateCollapsedAndCommonAncestor(range);
2145 }
2146
2147 function Range(doc) {
2148 this.startContainer = doc;
2149 this.startOffset = 0;
2150 this.endContainer = doc;
2151 this.endOffset = 0;
2152 this.document = doc;
2153 updateCollapsedAndCommonAncestor(this);
2154 }
2155
2156 createPrototypeRange(Range, updateBoundaries);
2157
2158 util.extend(Range, {
2159 rangeProperties: rangeProperties,
2160 RangeIterator: RangeIterator,
2161 copyComparisonConstants: copyComparisonConstants,
2162 createPrototypeRange: createPrototypeRange,
2163 inspect: inspect,
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;
2171 }
2172 });
2173
2174 api.DomRange = Range;
2175 });
2176
2177 /*----------------------------------------------------------------------------------------------------------------*/
2178
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;
2182 var dom = api.dom;
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;
2189
2190
2191 /*----------------------------------------------------------------------------------------------------------------*/
2192
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
2197
2198 (function() {
2199 var rangeProto;
2200 var rangeProperties = DomRange.rangeProperties;
2201
2202 function updateRangeProperties(range) {
2203 var i = rangeProperties.length, prop;
2204 while (i--) {
2205 prop = rangeProperties[i];
2206 range[prop] = range.nativeRange[prop];
2207 }
2208 // Fix for broken collapsed property in IE 9.
2209 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2210 }
2211
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);
2216
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);
2221 }
2222 }
2223
2224 var createBeforeAfterNodeSetter;
2225
2226 WrappedRange = function(range) {
2227 if (!range) {
2228 throw module.createError("WrappedRange: Range must be specified");
2229 }
2230 this.nativeRange = range;
2231 updateRangeProperties(this);
2232 };
2233
2234 DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
2235
2236 rangeProto = WrappedRange.prototype;
2237
2238 rangeProto.selectNode = function(node) {
2239 this.nativeRange.selectNode(node);
2240 updateRangeProperties(this);
2241 };
2242
2243 rangeProto.cloneContents = function() {
2244 return this.nativeRange.cloneContents();
2245 };
2246
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.
2249
2250 rangeProto.surroundContents = function(node) {
2251 this.nativeRange.surroundContents(node);
2252 updateRangeProperties(this);
2253 };
2254
2255 rangeProto.collapse = function(isStart) {
2256 this.nativeRange.collapse(isStart);
2257 updateRangeProperties(this);
2258 };
2259
2260 rangeProto.cloneRange = function() {
2261 return new WrappedRange(this.nativeRange.cloneRange());
2262 };
2263
2264 rangeProto.refresh = function() {
2265 updateRangeProperties(this);
2266 };
2267
2268 rangeProto.toString = function() {
2269 return this.nativeRange.toString();
2270 };
2271
2272 // Create test range and node for feature detection
2273
2274 var testTextNode = document.createTextNode("test");
2275 getBody(document).appendChild(testTextNode);
2276 var range = document.createRange();
2277
2278 /*--------------------------------------------------------------------------------------------------------*/
2279
2280 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2281 // correct for it
2282
2283 range.setStart(testTextNode, 0);
2284 range.setEnd(testTextNode, 0);
2285
2286 try {
2287 range.setStart(testTextNode, 1);
2288
2289 rangeProto.setStart = function(node, offset) {
2290 this.nativeRange.setStart(node, offset);
2291 updateRangeProperties(this);
2292 };
2293
2294 rangeProto.setEnd = function(node, offset) {
2295 this.nativeRange.setEnd(node, offset);
2296 updateRangeProperties(this);
2297 };
2298
2299 createBeforeAfterNodeSetter = function(name) {
2300 return function(node) {
2301 this.nativeRange[name](node);
2302 updateRangeProperties(this);
2303 };
2304 };
2305
2306 } catch(ex) {
2307
2308 rangeProto.setStart = function(node, offset) {
2309 try {
2310 this.nativeRange.setStart(node, offset);
2311 } catch (ex) {
2312 this.nativeRange.setEnd(node, offset);
2313 this.nativeRange.setStart(node, offset);
2314 }
2315 updateRangeProperties(this);
2316 };
2317
2318 rangeProto.setEnd = function(node, offset) {
2319 try {
2320 this.nativeRange.setEnd(node, offset);
2321 } catch (ex) {
2322 this.nativeRange.setStart(node, offset);
2323 this.nativeRange.setEnd(node, offset);
2324 }
2325 updateRangeProperties(this);
2326 };
2327
2328 createBeforeAfterNodeSetter = function(name, oppositeName) {
2329 return function(node) {
2330 try {
2331 this.nativeRange[name](node);
2332 } catch (ex) {
2333 this.nativeRange[oppositeName](node);
2334 this.nativeRange[name](node);
2335 }
2336 updateRangeProperties(this);
2337 };
2338 };
2339 }
2340
2341 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2342 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2343 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2344 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2345
2346 /*--------------------------------------------------------------------------------------------------------*/
2347
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));
2352 };
2353
2354 /*--------------------------------------------------------------------------------------------------------*/
2355
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
2358
2359 range.selectNodeContents(testTextNode);
2360 range.setEnd(testTextNode, 3);
2361
2362 var range2 = document.createRange();
2363 range2.selectNodeContents(testTextNode);
2364 range2.setEnd(testTextNode, 4);
2365 range2.setStart(testTextNode, 2);
2366
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
2370
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;
2377 }
2378 return this.nativeRange.compareBoundaryPoints(type, range);
2379 };
2380 } else {
2381 rangeProto.compareBoundaryPoints = function(type, range) {
2382 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2383 };
2384 }
2385
2386 /*--------------------------------------------------------------------------------------------------------*/
2387
2388 // Test for IE 9 deleteContents() and extractContents() bug and correct it. See issue 107.
2389
2390 var el = document.createElement("div");
2391 el.innerHTML = "123";
2392 var textNode = el.firstChild;
2393 var body = getBody(document);
2394 body.appendChild(el);
2395
2396 range.setStart(textNode, 1);
2397 range.setEnd(textNode, 2);
2398 range.deleteContents();
2399
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);
2406 };
2407
2408 rangeProto.extractContents = function() {
2409 var frag = this.nativeRange.extractContents();
2410 updateRangeProperties(this);
2411 return frag;
2412 };
2413 } else {
2414 }
2415
2416 body.removeChild(el);
2417 body = null;
2418
2419 /*--------------------------------------------------------------------------------------------------------*/
2420
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);
2425 };
2426 }
2427
2428 /*--------------------------------------------------------------------------------------------------------*/
2429
2430 // Clean up
2431 getBody(document).removeChild(testTextNode);
2432
2433 rangeProto.getName = function() {
2434 return "WrappedRange";
2435 };
2436
2437 api.WrappedRange = WrappedRange;
2438
2439 api.createNativeRange = function(doc) {
2440 doc = getContentDocument(doc, module, "createNativeRange");
2441 return doc.createRange();
2442 };
2443 })();
2444 }
2445
2446 if (api.features.implementsTextRange) {
2447 /*
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):
2450
2451 <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
2452
2453 var range = document.selection.createRange();
2454 alert(range.parentElement().id); // Should alert "ul" but alerts "b"
2455
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)
2460 */
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);
2470
2471 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
2472 };
2473
2474 var textRangeIsCollapsed = function(textRange) {
2475 return textRange.compareEndPoints("StartToEnd", textRange) == 0;
2476 };
2477
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();
2486
2487 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
2488 // check for that
2489 if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
2490 containerElement = wholeRangeContainerElement;
2491 }
2492
2493
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));
2498 return {
2499 boundaryPosition: pos,
2500 nodeInfo: {
2501 nodeIndex: pos.offset,
2502 containerElement: pos.node
2503 }
2504 };
2505 }
2506
2507 var workingNode = dom.getDocument(containerElement).createElement("span");
2508
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);
2513 }
2514
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;
2520
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;
2524
2525 while (true) {
2526 if (nodeIndex == childNodeCount) {
2527 containerElement.appendChild(workingNode);
2528 } else {
2529 containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
2530 }
2531 workingRange.moveToElementText(workingNode);
2532 comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
2533 if (comparison == 0 || start == end) {
2534 break;
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.
2538 break;
2539 } else {
2540 start = nodeIndex;
2541 }
2542 } else {
2543 end = (end == start + 1) ? start : nodeIndex;
2544 }
2545 nodeIndex = Math.floor((start + end) / 2);
2546 containerElement.removeChild(workingNode);
2547 }
2548
2549
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;
2553
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);
2559
2560 var offset;
2561
2562 if (/[\r\n]/.test(boundaryNode.data)) {
2563 /*
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
2566 IE. The facts:
2567
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
2571
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.
2580
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).
2586
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.
2592 */
2593 var tempRange = workingRange.duplicate();
2594 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
2595
2596 offset = tempRange.moveStart("character", rangeLength);
2597 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
2598 offset++;
2599 tempRange.moveStart("character", 1);
2600 }
2601 } else {
2602 offset = workingRange.text.length;
2603 }
2604 boundaryPosition = new DomPosition(boundaryNode, offset);
2605 } else {
2606
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);
2615 } else {
2616 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
2617 }
2618 }
2619
2620 // Clean up
2621 workingNode.parentNode.removeChild(workingNode);
2622
2623 return {
2624 boundaryPosition: boundaryPosition,
2625 nodeInfo: {
2626 nodeIndex: nodeIndex,
2627 containerElement: containerElement
2628 }
2629 };
2630 };
2631
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);
2640
2641 if (nodeIsDataNode) {
2642 boundaryNode = boundaryPosition.node;
2643 boundaryParent = boundaryNode.parentNode;
2644 } else {
2645 childNodes = boundaryPosition.node.childNodes;
2646 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
2647 boundaryParent = boundaryPosition.node;
2648 }
2649
2650 // Position the range immediately before the node containing the boundary
2651 workingNode = doc.createElement("span");
2652
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;";
2656
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
2659 if (boundaryNode) {
2660 boundaryParent.insertBefore(workingNode, boundaryNode);
2661 } else {
2662 boundaryParent.appendChild(workingNode);
2663 }
2664
2665 workingRange.moveToElementText(workingNode);
2666 workingRange.collapse(!isStart);
2667
2668 // Clean up
2669 boundaryParent.removeChild(workingNode);
2670
2671 // Move the working range to the text offset, if required
2672 if (nodeIsDataNode) {
2673 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
2674 }
2675
2676 return workingRange;
2677 };
2678
2679 /*------------------------------------------------------------------------------------------------------------*/
2680
2681 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
2682 // prototype
2683
2684 WrappedTextRange = function(textRange) {
2685 this.textRange = textRange;
2686 this.refresh();
2687 };
2688
2689 WrappedTextRange.prototype = new DomRange(document);
2690
2691 WrappedTextRange.prototype.refresh = function() {
2692 var start, end, startBoundary;
2693
2694 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
2695 var rangeContainerElement = getTextRangeContainerElement(this.textRange);
2696
2697 if (textRangeIsCollapsed(this.textRange)) {
2698 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
2699 true).boundaryPosition;
2700 } else {
2701 startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
2702 start = startBoundary.boundaryPosition;
2703
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;
2709 }
2710
2711 this.setStart(start.node, start.offset);
2712 this.setEnd(end.node, end.offset);
2713 };
2714
2715 WrappedTextRange.prototype.getName = function() {
2716 return "WrappedTextRange";
2717 };
2718
2719 DomRange.copyComparisonConstants(WrappedTextRange);
2720
2721 var rangeToTextRange = function(range) {
2722 if (range.collapsed) {
2723 return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
2724 } else {
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);
2730 return textRange;
2731 }
2732 };
2733
2734 WrappedTextRange.rangeToTextRange = rangeToTextRange;
2735
2736 WrappedTextRange.prototype.toTextRange = function() {
2737 return rangeToTextRange(this);
2738 };
2739
2740 api.WrappedTextRange = WrappedTextRange;
2741
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;
2749 }
2750
2751 api.createNativeRange = function(doc) {
2752 doc = getContentDocument(doc, module, "createNativeRange");
2753 return getBody(doc).createTextRange();
2754 };
2755
2756 api.WrappedRange = WrappedTextRange;
2757 }
2758 }
2759
2760 api.createRange = function(doc) {
2761 doc = getContentDocument(doc, module, "createRange");
2762 return new api.WrappedRange(api.createNativeRange(doc));
2763 };
2764
2765 api.createRangyRange = function(doc) {
2766 doc = getContentDocument(doc, module, "createRangyRange");
2767 return new DomRange(doc);
2768 };
2769
2770 api.createIframeRange = function(iframeEl) {
2771 module.deprecationNotice("createIframeRange()", "createRange(iframeEl)");
2772 return api.createRange(iframeEl);
2773 };
2774
2775 api.createIframeRangyRange = function(iframeEl) {
2776 module.deprecationNotice("createIframeRangyRange()", "createRangyRange(iframeEl)");
2777 return api.createRangyRange(iframeEl);
2778 };
2779
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);
2785 };
2786 }
2787 doc = win = null;
2788 });
2789 });
2790
2791 /*----------------------------------------------------------------------------------------------------------------*/
2792
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;
2797
2798 var BOOLEAN = "boolean";
2799 var NUMBER = "number";
2800 var dom = api.dom;
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;
2814
2815
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;
2820 }
2821
2822 function getWindow(win, methodName) {
2823 if (!win) {
2824 return window;
2825 } else if (dom.isWindow(win)) {
2826 return win;
2827 } else if (win instanceof WrappedSelection) {
2828 return win.win;
2829 } else {
2830 var doc = dom.getContentDocument(win, module, methodName);
2831 return dom.getWindow(doc);
2832 }
2833 }
2834
2835 function getWinSelection(winParam) {
2836 return getWindow(winParam, "getWinSelection").getSelection();
2837 }
2838
2839 function getDocSelection(winParam) {
2840 return getWindow(winParam, "getDocSelection").document.selection;
2841 }
2842
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);
2847 }
2848 return backward;
2849 }
2850
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");
2855
2856 features.implementsWinGetSelection = implementsWinGetSelection;
2857 features.implementsDocSelection = implementsDocSelection;
2858
2859 var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
2860
2861 if (useDocumentSelection) {
2862 getNativeSelection = getDocSelection;
2863 api.isSelectionValid = function(winParam) {
2864 var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
2865
2866 // Check whether the selection TextRange is actually contained within the correct document
2867 return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
2868 };
2869 } else if (implementsWinGetSelection) {
2870 getNativeSelection = getWinSelection;
2871 api.isSelectionValid = function() {
2872 return true;
2873 };
2874 } else {
2875 module.fail("Neither document.selection or window.getSelection() detected.");
2876 }
2877
2878 api.getNativeSelection = getNativeSelection;
2879
2880 var testSelection = getNativeSelection();
2881 var testRange = api.createNativeRange(document);
2882 var body = getBody(document);
2883
2884 // Obtaining a range from a selection
2885 var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
2886 ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
2887
2888 features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
2889
2890 // Test for existence of native selection extend() method
2891 var selectionHasExtend = isHostMethod(testSelection, "extend");
2892 features.selectionHasExtend = selectionHasExtend;
2893
2894 // Test if rangeCount exists
2895 var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
2896 features.selectionHasRangeCount = selectionHasRangeCount;
2897
2898 var selectionSupportsMultipleRanges = false;
2899 var collapsedNonEditableSelectionsSupported = true;
2900
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);
2908 } : null;
2909
2910 if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
2911 typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
2912
2913 (function() {
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.
2916
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
2920 // selection.
2921 var sel = window.getSelection();
2922 if (sel) {
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);
2930 }
2931
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") );
2937
2938 // Test whether the native selection will allow a collapsed selection within a non-editable element
2939 var r1 = document.createRange();
2940
2941 r1.setStart(textNode, 1);
2942 r1.collapse(true);
2943 sel.addRange(r1);
2944 collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
2945 sel.removeAllRanges();
2946
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;
2957 } else {
2958 var r2 = r1.cloneRange();
2959 r1.setStart(textNode, 0);
2960 r2.setEnd(textNode, 3);
2961 r2.setStart(textNode, 2);
2962 sel.addRange(r1);
2963 sel.addRange(r2);
2964 selectionSupportsMultipleRanges = (sel.rangeCount == 2);
2965 }
2966 }
2967
2968 // Clean up
2969 body.removeChild(testEl);
2970 sel.removeAllRanges();
2971
2972 for (i = 0; i < originalSelectionRangeCount; ++i) {
2973 if (i == 0 && originalSelectionBackward) {
2974 if (addRangeBackwardToNative) {
2975 addRangeBackwardToNative(sel, originalSelectionRanges[i]);
2976 } else {
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]);
2979 }
2980 } else {
2981 sel.addRange(originalSelectionRanges[i]);
2982 }
2983 }
2984 }
2985 })();
2986 }
2987
2988 features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
2989 features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
2990
2991 // ControlRanges
2992 var implementsControlRange = false, testControlRange;
2993
2994 if (body && isHostMethod(body, "createControlRange")) {
2995 testControlRange = body.createControlRange();
2996 if (util.areHostProperties(testControlRange, ["item", "add"])) {
2997 implementsControlRange = true;
2998 }
2999 }
3000 features.implementsControlRange = implementsControlRange;
3001
3002 // Selection collapsedness
3003 if (selectionHasAnchorAndFocus) {
3004 selectionIsCollapsed = function(sel) {
3005 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
3006 };
3007 } else {
3008 selectionIsCollapsed = function(sel) {
3009 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
3010 };
3011 }
3012
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"];
3019 }
3020
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;
3027 }
3028
3029 function updateEmptySelection(sel) {
3030 sel.anchorNode = sel.focusNode = null;
3031 sel.anchorOffset = sel.focusOffset = 0;
3032 sel.rangeCount = 0;
3033 sel.isCollapsed = true;
3034 sel._ranges.length = 0;
3035 }
3036
3037 function getNativeRange(range) {
3038 var nativeRange;
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;
3047 }
3048 return nativeRange;
3049 }
3050
3051 function rangeContainsSingleElement(rangeNodes) {
3052 if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
3053 return false;
3054 }
3055 for (var i = 1, len = rangeNodes.length; i < len; ++i) {
3056 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
3057 return false;
3058 }
3059 }
3060 return true;
3061 }
3062
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");
3067 }
3068 return nodes[0];
3069 }
3070
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";
3074 }
3075
3076 function updateFromTextRange(sel, range) {
3077 // Create a Range from the selected TextRange
3078 var wrappedRange = new WrappedRange(range);
3079 sel._ranges = [wrappedRange];
3080
3081 updateAnchorAndFocusFromRange(sel, wrappedRange, false);
3082 sel.rangeCount = 1;
3083 sel.isCollapsed = wrappedRange.collapsed;
3084 }
3085
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);
3091 } else {
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);
3098 } else {
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);
3105 }
3106 sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
3107 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
3108 }
3109 }
3110 }
3111
3112 function addRangeToControlSelection(sel, range) {
3113 var controlRange = sel.docSelection.createRange();
3114 var rangeElement = getSingleElementFromRange(range);
3115
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));
3122 }
3123 try {
3124 newControlRange.add(rangeElement);
3125 } catch (ex) {
3126 throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
3127 }
3128 newControlRange.select();
3129
3130 // Update the wrapped selection based on what's now in the native selection
3131 updateControlSelection(sel);
3132 }
3133
3134 var getSelectionRangeAt;
3135
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) {
3141 try {
3142 return sel.getRangeAt(index);
3143 } catch (ex) {
3144 return null;
3145 }
3146 };
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);
3152
3153 // Handle the case when the selection was selected backwards (from the end to the start in the
3154 // document)
3155 if (range.collapsed !== this.isCollapsed) {
3156 range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
3157 }
3158
3159 return range;
3160 };
3161 }
3162
3163 function WrappedSelection(selection, docSelection, win) {
3164 this.nativeSelection = selection;
3165 this.docSelection = docSelection;
3166 this._ranges = [];
3167 this.win = win;
3168 this.refresh();
3169 }
3170
3171 WrappedSelection.prototype = api.selectionPrototype;
3172
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;
3177 }
3178
3179 var cachedRangySelections = [];
3180
3181 function actOnCachedSelection(win, action) {
3182 var i = cachedRangySelections.length, cached, sel;
3183 while (i--) {
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);
3191 return true;
3192 } else {
3193 return sel;
3194 }
3195 }
3196 }
3197 if (action == "deleteAll") {
3198 cachedRangySelections.length = 0;
3199 }
3200 return null;
3201 }
3202
3203 var getSelection = function(win) {
3204 // Check if the parameter is a Rangy Selection object
3205 if (win && win instanceof WrappedSelection) {
3206 win.refresh();
3207 return win;
3208 }
3209
3210 win = getWindow(win, "getNativeSelection");
3211
3212 var sel = actOnCachedSelection(win);
3213 var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
3214 if (sel) {
3215 sel.nativeSelection = nativeSel;
3216 sel.docSelection = docSel;
3217 sel.refresh();
3218 } else {
3219 sel = new WrappedSelection(nativeSel, docSel, win);
3220 cachedRangySelections.push( { win: win, selection: sel } );
3221 }
3222 return sel;
3223 };
3224
3225 api.getSelection = getSelection;
3226
3227 api.getIframeSelection = function(iframeEl) {
3228 module.deprecationNotice("getIframeSelection()", "getSelection(iframeEl)");
3229 return api.getSelection(dom.getIframeWindow(iframeEl));
3230 };
3231
3232 var selProto = WrappedSelection.prototype;
3233
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]);
3240 try {
3241 controlRange.add(el);
3242 } catch (ex) {
3243 throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
3244 }
3245 }
3246 controlRange.select();
3247
3248 // Update the wrapped selection based on what's now in the native selection
3249 updateControlSelection(sel);
3250 }
3251
3252 // Selecting a range
3253 if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
3254 selProto.removeAllRanges = function() {
3255 this.nativeSelection.removeAllRanges();
3256 updateEmptySelection(this);
3257 };
3258
3259 var addRangeBackward = function(sel, range) {
3260 addRangeBackwardToNative(sel.nativeSelection, range);
3261 sel.refresh();
3262 };
3263
3264 if (selectionHasRangeCount) {
3265 selProto.addRange = function(range, direction) {
3266 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3267 addRangeToControlSelection(this, range);
3268 } else {
3269 if (isDirectionBackward(direction) && selectionHasExtend) {
3270 addRangeBackward(this, range);
3271 } else {
3272 var previousRangeCount;
3273 if (selectionSupportsMultipleRanges) {
3274 previousRangeCount = this.rangeCount;
3275 } else {
3276 this.removeAllRanges();
3277 previousRangeCount = 0;
3278 }
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
3281 // issue 80.
3282 this.nativeSelection.addRange(getNativeRange(range).cloneRange());
3283
3284 // Check whether adding the range was successful
3285 this.rangeCount = this.nativeSelection.rangeCount;
3286
3287 if (this.rangeCount == previousRangeCount + 1) {
3288 // The range was added successfully
3289
3290 // Check whether the range that we added to the selection is reflected in the last range extracted from
3291 // the selection
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);
3297 }
3298 }
3299 this._ranges[this.rangeCount - 1] = range;
3300 updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
3301 this.isCollapsed = selectionIsCollapsed(this);
3302 } else {
3303 // The range was not added successfully. The simplest thing is to refresh
3304 this.refresh();
3305 }
3306 }
3307 }
3308 };
3309 } else {
3310 selProto.addRange = function(range, direction) {
3311 if (isDirectionBackward(direction) && selectionHasExtend) {
3312 addRangeBackward(this, range);
3313 } else {
3314 this.nativeSelection.addRange(getNativeRange(range));
3315 this.refresh();
3316 }
3317 };
3318 }
3319
3320 selProto.setRanges = function(ranges) {
3321 if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
3322 createControlSelection(this, ranges);
3323 } else {
3324 this.removeAllRanges();
3325 for (var i = 0, len = ranges.length; i < len; ++i) {
3326 this.addRange(ranges[i]);
3327 }
3328 }
3329 };
3330 } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
3331 implementsControlRange && useDocumentSelection) {
3332
3333 selProto.removeAllRanges = function() {
3334 // Added try/catch as fix for issue #21
3335 try {
3336 this.docSelection.empty();
3337
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
3341 // calling empty()
3342 var doc;
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) );
3349 }
3350 }
3351 if (doc) {
3352 var textRange = getBody(doc).createTextRange();
3353 textRange.select();
3354 this.docSelection.empty();
3355 }
3356 }
3357 } catch(ex) {}
3358 updateEmptySelection(this);
3359 };
3360
3361 selProto.addRange = function(range) {
3362 if (this.docSelection.type == CONTROL) {
3363 addRangeToControlSelection(this, range);
3364 } else {
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);
3370 }
3371 };
3372
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]);
3380 }
3381 };
3382 } else {
3383 module.fail("No means of selecting a Range or TextRange was found");
3384 return false;
3385 }
3386
3387 selProto.getRangeAt = function(index) {
3388 if (index < 0 || index >= this.rangeCount) {
3389 throw new DOMException("INDEX_SIZE_ERR");
3390 } else {
3391 // Clone the range to preserve selection-range independence. See issue 80.
3392 return this._ranges[index].cloneRange();
3393 }
3394 };
3395
3396 var refreshSelection;
3397
3398 if (useDocumentSelection) {
3399 refreshSelection = function(sel) {
3400 var range;
3401 if (api.isSelectionValid(sel.win)) {
3402 range = sel.docSelection.createRange();
3403 } else {
3404 range = getBody(sel.win.document).createTextRange();
3405 range.collapse(true);
3406 }
3407
3408 if (sel.docSelection.type == CONTROL) {
3409 updateControlSelection(sel);
3410 } else if (isTextRange(range)) {
3411 updateFromTextRange(sel, range);
3412 } else {
3413 updateEmptySelection(sel);
3414 }
3415 };
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);
3420 } else {
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));
3425 }
3426 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
3427 sel.isCollapsed = selectionIsCollapsed(sel);
3428 } else {
3429 updateEmptySelection(sel);
3430 }
3431 }
3432 };
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];
3439 sel.rangeCount = 1;
3440 updateAnchorAndFocusFromNativeSelection(sel);
3441 sel.isCollapsed = selectionIsCollapsed(sel);
3442 } else {
3443 updateEmptySelection(sel);
3444 }
3445 };
3446 } else {
3447 module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
3448 return false;
3449 }
3450
3451 selProto.refresh = function(checkForChanges) {
3452 var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
3453 var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
3454
3455 refreshSelection(this);
3456 if (checkForChanges) {
3457 // Check the range count first
3458 var i = oldRanges.length;
3459 if (i != this._ranges.length) {
3460 return true;
3461 }
3462
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) {
3466 return true;
3467 }
3468
3469 // Finally, compare each range in turn
3470 while (i--) {
3471 if (!rangesEqual(oldRanges[i], this._ranges[i])) {
3472 return true;
3473 }
3474 }
3475 return false;
3476 }
3477 };
3478
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]);
3486 }
3487 }
3488 if (!sel.rangeCount) {
3489 updateEmptySelection(sel);
3490 }
3491 };
3492
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);
3498
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));
3508 } else {
3509 removed = true;
3510 }
3511 }
3512 newControlRange.select();
3513
3514 // Update the wrapped selection based on what's now in the native selection
3515 updateControlSelection(this);
3516 } else {
3517 removeRangeManually(this, range);
3518 }
3519 };
3520 } else {
3521 selProto.removeRange = function(range) {
3522 removeRangeManually(this, range);
3523 };
3524 }
3525
3526 // Detecting if a selection is backward
3527 var selectionIsBackward;
3528 if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
3529 selectionIsBackward = winSelectionIsBackward;
3530
3531 selProto.isBackward = function() {
3532 return selectionIsBackward(this);
3533 };
3534 } else {
3535 selectionIsBackward = selProto.isBackward = function() {
3536 return false;
3537 };
3538 }
3539
3540 // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
3541 selProto.isBackwards = selProto.isBackward;
3542
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];
3550 }
3551 return rangeTexts.join("");
3552 };
3553
3554 function assertNodeInSameDocument(sel, node) {
3555 if (sel.win.document != getDocument(node)) {
3556 throw new DOMException("WRONG_DOCUMENT_ERR");
3557 }
3558 }
3559
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;
3567 };
3568
3569 selProto.collapseToStart = function() {
3570 if (this.rangeCount) {
3571 var range = this._ranges[0];
3572 this.collapse(range.startContainer, range.startOffset);
3573 } else {
3574 throw new DOMException("INVALID_STATE_ERR");
3575 }
3576 };
3577
3578 selProto.collapseToEnd = function() {
3579 if (this.rangeCount) {
3580 var range = this._ranges[this.rangeCount - 1];
3581 this.collapse(range.endContainer, range.endOffset);
3582 } else {
3583 throw new DOMException("INVALID_STATE_ERR");
3584 }
3585 };
3586
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);
3594 };
3595
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();
3600 var element;
3601 while (controlRange.length) {
3602 element = controlRange.item(0);
3603 controlRange.remove(element);
3604 element.parentNode.removeChild(element);
3605 }
3606 this.refresh();
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();
3613 }
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]);
3617 }
3618 }
3619 };
3620
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) ) ) {
3625 return returnValue;
3626 }
3627 }
3628 };
3629
3630 selProto.getAllRanges = function() {
3631 var ranges = [];
3632 this.eachRange(function(range) {
3633 ranges.push(range);
3634 });
3635 return ranges;
3636 };
3637
3638 selProto.setSingleRange = function(range, direction) {
3639 this.removeAllRanges();
3640 this.addRange(range, direction);
3641 };
3642
3643 selProto.callMethodOnEachRange = function(methodName, params) {
3644 var results = [];
3645 this.eachRange( function(range) {
3646 results.push( range[methodName].apply(range, params) );
3647 } );
3648 return results;
3649 };
3650
3651 function createStartOrEndSetter(isStart) {
3652 return function(node, offset) {
3653 var range;
3654 if (this.rangeCount) {
3655 range = this.getRangeAt(0);
3656 range["set" + (isStart ? "Start" : "End")](node, offset);
3657 } else {
3658 range = api.createRange(this.win.document);
3659 range.setStartAndEnd(node, offset);
3660 }
3661 this.setSingleRange(range, this.isBackward());
3662 };
3663 }
3664
3665 selProto.setStart = createStartOrEndSetter(true);
3666 selProto.setEnd = createStartOrEndSetter(false);
3667
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);
3671 };
3672
3673 selProto.changeEachRange = function(func) {
3674 var ranges = [];
3675 var backward = this.isBackward();
3676
3677 this.eachRange(function(range) {
3678 func(range);
3679 ranges.push(range);
3680 });
3681
3682 this.removeAllRanges();
3683 if (backward && ranges.length == 1) {
3684 this.addRange(ranges[0], "backward");
3685 } else {
3686 this.setRanges(ranges);
3687 }
3688 };
3689
3690 selProto.containsNode = function(node, allowPartial) {
3691 return this.eachRange( function(range) {
3692 return range.containsNode(node, allowPartial);
3693 }, true ) || false;
3694 };
3695
3696 selProto.getBookmark = function(containerNode) {
3697 return {
3698 backward: this.isBackward(),
3699 rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
3700 };
3701 };
3702
3703 selProto.moveToBookmark = function(bookmark) {
3704 var selRanges = [];
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);
3709 }
3710 if (bookmark.backward) {
3711 this.setSingleRange(selRanges[0], "backward");
3712 } else {
3713 this.setRanges(selRanges);
3714 }
3715 };
3716
3717 selProto.toHtml = function() {
3718 var rangeHtmls = [];
3719 this.eachRange(function(range) {
3720 rangeHtmls.push( DomRange.toHtml(range) );
3721 });
3722 return rangeHtmls.join("");
3723 };
3724
3725 if (features.implementsTextRange) {
3726 selProto.getNativeTextRange = function() {
3727 var sel, textRange;
3728 if ( (sel = this.docSelection) ) {
3729 var range = sel.createRange();
3730 if (isTextRange(range)) {
3731 return range;
3732 } else {
3733 throw module.createError("getNativeTextRange: selection is a control selection");
3734 }
3735 } else if (this.rangeCount > 0) {
3736 return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
3737 } else {
3738 throw module.createError("getNativeTextRange: selection contains no range");
3739 }
3740 };
3741 }
3742
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";
3748
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));
3752 }
3753 }
3754 return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
3755 ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
3756 }
3757
3758 selProto.getName = function() {
3759 return "WrappedSelection";
3760 };
3761
3762 selProto.inspect = function() {
3763 return inspect(this);
3764 };
3765
3766 selProto.detach = function() {
3767 actOnCachedSelection(this.win, "delete");
3768 deleteProperties(this);
3769 };
3770
3771 WrappedSelection.detachAll = function() {
3772 actOnCachedSelection(null, "deleteAll");
3773 };
3774
3775 WrappedSelection.inspect = inspect;
3776 WrappedSelection.isDirectionBackward = isDirectionBackward;
3777
3778 api.Selection = WrappedSelection;
3779
3780 api.selectionPrototype = selProto;
3781
3782 api.addShimListener(function(win) {
3783 if (typeof win.getSelection == "undefined") {
3784 win.getSelection = function() {
3785 return getSelection(win);
3786 };
3787 }
3788 win = null;
3789 });
3790 });
3791
3792
3793 /*----------------------------------------------------------------------------------------------------------------*/
3794
3795 return api;
3796 }, this);;/**
3797 * Selection save and restore module for Rangy.
3798 * Saves and restores user selections using marker invisible elements in the DOM.
3799 *
3800 * Part of Rangy, a cross-browser JavaScript range and selection library
3801 * http://code.google.com/p/rangy/
3802 *
3803 * Depends on Rangy core.
3804 *
3805 * Copyright 2014, Tim Down
3806 * Licensed under the MIT license.
3807 * Version: 1.3alpha.20140804
3808 * Build date: 4 August 2014
3809 */
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);
3814 /*
3815 } else if (typeof exports == "object") {
3816 // Node/CommonJS style for Browserify
3817 module.exports = factory;
3818 */
3819 } else {
3820 // No AMD or CommonJS support so we use the rangy global variable
3821 factory(global.rangy);
3822 }
3823 })(function(rangy) {
3824 rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
3825 var dom = api.dom;
3826
3827 var markerTextChar = "\ufeff";
3828
3829 function gEBI(id, doc) {
3830 return (doc || document).getElementById(id);
3831 }
3832
3833 function insertRangeBoundaryMarker(range, atStart) {
3834 var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
3835 var markerEl;
3836 var doc = dom.getDocument(range.startContainer);
3837
3838 // Clone the Range and collapse to the appropriate boundary point
3839 var boundaryRange = range.cloneRange();
3840 boundaryRange.collapse(atStart);
3841
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));
3849
3850 boundaryRange.insertNode(markerEl);
3851 return markerEl;
3852 }
3853
3854 function setRangeBoundary(doc, range, markerId, atStart) {
3855 var markerEl = gEBI(markerId, doc);
3856 if (markerEl) {
3857 range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
3858 markerEl.parentNode.removeChild(markerEl);
3859 } else {
3860 module.warn("Marker element has been removed. Cannot restore selection.");
3861 }
3862 }
3863
3864 function compareRanges(r1, r2) {
3865 return r2.compareBoundaryPoints(r1.START_TO_START, r1);
3866 }
3867
3868 function saveRange(range, backward) {
3869 var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
3870
3871 if (range.collapsed) {
3872 endEl = insertRangeBoundaryMarker(range, false);
3873 return {
3874 document: doc,
3875 markerId: endEl.id,
3876 collapsed: true
3877 };
3878 } else {
3879 endEl = insertRangeBoundaryMarker(range, false);
3880 startEl = insertRangeBoundaryMarker(range, true);
3881
3882 return {
3883 document: doc,
3884 startMarkerId: startEl.id,
3885 endMarkerId: endEl.id,
3886 collapsed: false,
3887 backward: backward,
3888 toString: function() {
3889 return "original text: '" + text + "', new text: '" + range.toString() + "'";
3890 }
3891 };
3892 }
3893 }
3894
3895 function restoreRange(rangeInfo, normalize) {
3896 var doc = rangeInfo.document;
3897 if (typeof normalize == "undefined") {
3898 normalize = true;
3899 }
3900 var range = api.createRange(doc);
3901 if (rangeInfo.collapsed) {
3902 var markerEl = gEBI(rangeInfo.markerId, doc);
3903 if (markerEl) {
3904 markerEl.style.display = "inline";
3905 var previousNode = markerEl.previousSibling;
3906
3907 // Workaround for issue 17
3908 if (previousNode && previousNode.nodeType == 3) {
3909 markerEl.parentNode.removeChild(markerEl);
3910 range.collapseToPoint(previousNode, previousNode.length);
3911 } else {
3912 range.collapseBefore(markerEl);
3913 markerEl.parentNode.removeChild(markerEl);
3914 }
3915 } else {
3916 module.warn("Marker element has been removed. Cannot restore selection.");
3917 }
3918 } else {
3919 setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
3920 setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
3921 }
3922
3923 if (normalize) {
3924 range.normalizeBoundaries();
3925 }
3926
3927 return range;
3928 }
3929
3930 function saveRanges(ranges, backward) {
3931 var rangeInfos = [], range, doc;
3932
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);
3936
3937 for (var i = 0, len = ranges.length; i < len; ++i) {
3938 rangeInfos[i] = saveRange(ranges[i], backward);
3939 }
3940
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) {
3944 range = ranges[i];
3945 doc = api.DomRange.getRangeDocument(range);
3946 if (range.collapsed) {
3947 range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
3948 } else {
3949 range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
3950 range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
3951 }
3952 }
3953
3954 return rangeInfos;
3955 }
3956
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.");
3960 return null;
3961 }
3962 var sel = api.getSelection(win);
3963 var ranges = sel.getAllRanges();
3964 var backward = (ranges.length == 1 && sel.isBackward());
3965
3966 var rangeInfos = saveRanges(ranges, backward);
3967
3968 // Ensure current selection is unaffected
3969 if (backward) {
3970 sel.setSingleRange(ranges[0], "backward");
3971 } else {
3972 sel.setRanges(ranges);
3973 }
3974
3975 return {
3976 win: win,
3977 rangeInfos: rangeInfos,
3978 restored: false
3979 };
3980 }
3981
3982 function restoreRanges(rangeInfos) {
3983 var ranges = [];
3984
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;
3988
3989 for (var i = rangeCount - 1; i >= 0; i--) {
3990 ranges[i] = restoreRange(rangeInfos[i], true);
3991 }
3992
3993 return ranges;
3994 }
3995
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;
4001
4002 if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
4003 sel.removeAllRanges();
4004 sel.addRange(ranges[0], true);
4005 } else {
4006 sel.setRanges(ranges);
4007 }
4008
4009 savedSelection.restored = true;
4010 }
4011 }
4012
4013 function removeMarkerElement(doc, markerId) {
4014 var markerEl = gEBI(markerId, doc);
4015 if (markerEl) {
4016 markerEl.parentNode.removeChild(markerEl);
4017 }
4018 }
4019
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);
4026 } else {
4027 removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
4028 removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
4029 }
4030 }
4031 }
4032
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
4042 });
4043 });
4044
4045 }, this);;/*
4046 Base.js, version 1.1a
4047 Copyright 2006-2010, Dean Edwards
4048 License: http://www.opensource.org/licenses/mit-license.php
4049 */
4050
4051 var Base = function() {
4052 // dummy
4053 };
4054
4055 Base.extend = function(_instance, _static) { // subclass
4056 var extend = Base.prototype.extend;
4057
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
4064 };
4065 delete Base._prototyping;
4066
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);
4078 }
4079 }
4080 };
4081
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();
4092 };
4093 extend.call(klass, _static);
4094 // class initialisation
4095 if (typeof klass.init == "function") klass.init();
4096 return klass;
4097 };
4098
4099 Base.prototype = {
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();
4109 // override
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;
4115 return returnValue;
4116 };
4117 // point to the underlying method
4118 value.valueOf = function(type) {
4119 return (type == "object") ? value : method;
4120 };
4121 value.toString = Base.toString;
4122 }
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;
4129 }
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]);
4138
4139 }
4140 }
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]);
4144 }
4145 }
4146 return this;
4147 }
4148 };
4149
4150 // initialise
4151 Base = Base.extend({
4152 constructor: function() {
4153 this.extend(arguments[0]);
4154 }
4155 }, {
4156 ancestor: Object,
4157 version: "1.1",
4158
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);
4163 }
4164 }
4165 },
4166
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);
4172 } else {
4173 // add the interface using the extend method
4174 this.prototype.extend(arguments[i]);
4175 }
4176 }
4177 return this;
4178 },
4179
4180 toString: function() {
4181 return String(this.valueOf());
4182 }
4183 });;/**
4184 * Detect browser support for specific features
4185 */
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;
4194
4195 function iosVersion(userAgent) {
4196 return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1];
4197 }
4198
4199 function androidVersion(userAgent) {
4200 return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1];
4201 }
4202
4203 function isIE(version, equation) {
4204 var rv = -1,
4205 re;
4206
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,})");
4211 }
4212
4213 if (re && re.exec(navigator.userAgent) != null) {
4214 rv = parseFloat(RegExp.$1);
4215 }
4216
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; }
4224 }
4225
4226 return {
4227 // Static variable needed, publicly accessible, to be able override it in unit tests
4228 USER_AGENT: userAgent,
4229
4230 /**
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
4235 *
4236 * @return {Boolean}
4237 */
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;
4252 },
4253
4254 isTouchDevice: function() {
4255 return this.supportsEvent("touchmove");
4256 },
4257
4258 isIos: function() {
4259 return (/ipad|iphone|ipod/i).test(this.USER_AGENT);
4260 },
4261
4262 isAndroid: function() {
4263 return this.USER_AGENT.indexOf("Android") !== -1;
4264 },
4265
4266 /**
4267 * Whether the browser supports sandboxed iframes
4268 * Currently only IE 6+ offers such feature <iframe security="restricted">
4269 *
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
4272 *
4273 * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)
4274 */
4275 supportsSandboxedIframes: function() {
4276 return isIE();
4277 },
4278
4279 /**
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
4283 */
4284 throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {
4285 return !("querySelector" in document);
4286 },
4287
4288 /**
4289 * Whether the caret is correctly displayed in contentEditable elements
4290 * Firefox sometimes shows a huge caret in the beginning after focusing
4291 */
4292 displaysCaretInEmptyContentEditableCorrectly: function() {
4293 return isIE();
4294 },
4295
4296 /**
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
4300 */
4301 hasCurrentStyleProperty: function() {
4302 return "currentStyle" in testElement;
4303 },
4304
4305 /**
4306 * Firefox on OSX navigates through history when hitting CMD + Arrow right/left
4307 */
4308 hasHistoryIssue: function() {
4309 return isGecko && navigator.platform.substr(0, 3) === "Mac";
4310 },
4311
4312 /**
4313 * Whether the browser inserts a <br> when pressing enter in a contentEditable element
4314 */
4315 insertsLineBreaksOnReturn: function() {
4316 return isGecko;
4317 },
4318
4319 supportsPlaceholderAttributeOn: function(element) {
4320 return "placeholder" in element;
4321 },
4322
4323 supportsEvent: function(eventName) {
4324 return "on" + eventName in testElement || (function() {
4325 testElement.setAttribute("on" + eventName, "return;");
4326 return typeof(testElement["on" + eventName]) === "function";
4327 })();
4328 },
4329
4330 /**
4331 * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe
4332 */
4333 supportsEventsInIframeCorrectly: function() {
4334 return !isOpera;
4335 },
4336
4337 /**
4338 * Everything below IE9 doesn't know how to treat HTML5 tags
4339 *
4340 * @param {Object} context The document object on which to check HTML5 support
4341 *
4342 * @example
4343 * wysihtml5.browser.supportsHTML5Tags(document);
4344 */
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;
4350 },
4351
4352 /**
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
4356 *
4357 * @param {Object} doc Document object on which to check for a query command
4358 * @param {String} command The query command to check for
4359 * @return {Boolean}
4360 *
4361 * @example
4362 * wysihtml5.browser.supportsCommand(document, "bold");
4363 */
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()
4374 };
4375
4376 // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands
4377 var supported = {
4378 "insertHTML": isGecko
4379 };
4380
4381 return function(doc, command) {
4382 var isBuggy = buggyCommands[command];
4383 if (!isBuggy) {
4384 // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled
4385 try {
4386 return doc.queryCommandSupported(command);
4387 } catch(e1) {}
4388
4389 try {
4390 return doc.queryCommandEnabled(command);
4391 } catch(e2) {
4392 return !!supported[command];
4393 }
4394 }
4395 return false;
4396 };
4397 })(),
4398
4399 /**
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).
4408 */
4409 doesAutoLinkingInContentEditable: function() {
4410 return isIE();
4411 },
4412
4413 /**
4414 * As stated above, IE auto links urls typed into contentEditable elements
4415 * Since IE9 it's possible to prevent this behavior
4416 */
4417 canDisableAutoLinking: function() {
4418 return this.supportsCommand(document, "AutoUrlDetect");
4419 },
4420
4421 /**
4422 * IE leaves an empty paragraph in the contentEditable element after clearing it
4423 * Chrome/Safari sometimes an empty <div>
4424 */
4425 clearsContentEditableCorrectly: function() {
4426 return isGecko || isOpera || isWebKit;
4427 },
4428
4429 /**
4430 * IE gives wrong results for getAttribute
4431 */
4432 supportsGetAttributeCorrectly: function() {
4433 var td = document.createElement("td");
4434 return td.getAttribute("rowspan") != "1";
4435 },
4436
4437 /**
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
4440 */
4441 canSelectImagesInContentEditable: function() {
4442 return isGecko || isIE() || isOpera;
4443 },
4444
4445 /**
4446 * All browsers except Safari and Chrome automatically scroll the range/caret position into view
4447 */
4448 autoScrollsToCaret: function() {
4449 return !isWebKit;
4450 },
4451
4452 /**
4453 * Check whether the browser automatically closes tags that don't need to be opened
4454 */
4455 autoClosesUnclosedTags: function() {
4456 var clonedTestElement = testElement.cloneNode(false),
4457 returnValue,
4458 innerHTML;
4459
4460 clonedTestElement.innerHTML = "<p><div></div>";
4461 innerHTML = clonedTestElement.innerHTML.toLowerCase();
4462 returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";
4463
4464 // Cache result by overwriting current function
4465 this.autoClosesUnclosedTags = function() { return returnValue; };
4466
4467 return returnValue;
4468 },
4469
4470 /**
4471 * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists
4472 */
4473 supportsNativeGetElementsByClassName: function() {
4474 return String(document.getElementsByClassName).indexOf("[native code]") !== -1;
4475 },
4476
4477 /**
4478 * As of now (19.04.2011) only supported by Firefox 4 and Chrome
4479 * See https://developer.mozilla.org/en/DOM/Selection/modify
4480 */
4481 supportsSelectionModify: function() {
4482 return "getSelection" in window && "modify" in window.getSelection();
4483 },
4484
4485 /**
4486 * Opera needs a white space after a <br> in order to position the caret correctly
4487 */
4488 needsSpaceAfterLineBreak: function() {
4489 return isOpera;
4490 },
4491
4492 /**
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/
4495 *
4496 * @example
4497 * var input = document.createElement("input");
4498 * if (wysihtml5.browser.supportsSpeechApiOn(input)) {
4499 * // ...
4500 * }
4501 */
4502 supportsSpeechApiOn: function(input) {
4503 var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [undefined, 0];
4504 return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input);
4505 },
4506
4507 /**
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/
4511 */
4512 crashesWhenDefineProperty: function(property) {
4513 return isIE(9) && (property === "XMLHttpRequest" || property === "XDomainRequest");
4514 },
4515
4516 /**
4517 * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element
4518 */
4519 doesAsyncFocus: function() {
4520 return isIE();
4521 },
4522
4523 /**
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
4525 */
4526 hasProblemsSettingCaretAfterImg: function() {
4527 return isIE();
4528 },
4529
4530 hasUndoInContextMenu: function() {
4531 return isGecko || isChrome || isOpera;
4532 },
4533
4534 /**
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>
4539 */
4540 hasInsertNodeIssue: function() {
4541 return isOpera;
4542 },
4543
4544 /**
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>)
4546 */
4547 hasIframeFocusIssue: function() {
4548 return isIE();
4549 },
4550
4551 /**
4552 * Chrome + Safari create invalid nested markup after paste
4553 *
4554 * <p>
4555 * foo
4556 * <p>bar</p> <!-- BOO! -->
4557 * </p>
4558 */
4559 createsNestedInvalidMarkupAfterPaste: function() {
4560 return isWebKit;
4561 },
4562
4563 supportsMutationEvents: function() {
4564 return ("MutationEvent" in window);
4565 },
4566
4567 /**
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
4571 */
4572 supportsModenPaste: function () {
4573 return !("clipboardData" in window);
4574 }
4575 };
4576 })();
4577 ;wysihtml5.lang.array = function(arr) {
4578 return {
4579 /**
4580 * Check whether a given object exists in an array
4581 *
4582 * @example
4583 * wysihtml5.lang.array([1, 2]).contains(1);
4584 * // => true
4585 *
4586 * Can be used to match array with array. If intersection is found true is returned
4587 */
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) {
4592 return true;
4593 }
4594 }
4595 return false;
4596 } else {
4597 return wysihtml5.lang.array(arr).indexOf(needle) !== -1;
4598 }
4599 },
4600
4601 /**
4602 * Check whether a given object exists in an array and return index
4603 * If no elelemt found returns -1
4604 *
4605 * @example
4606 * wysihtml5.lang.array([1, 2]).indexOf(2);
4607 * // => 1
4608 */
4609 indexOf: function(needle) {
4610 if (arr.indexOf) {
4611 return arr.indexOf(needle);
4612 } else {
4613 for (var i=0, length=arr.length; i<length; i++) {
4614 if (arr[i] === needle) { return i; }
4615 }
4616 return -1;
4617 }
4618 },
4619
4620 /**
4621 * Substract one array from another
4622 *
4623 * @example
4624 * wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]);
4625 * // => [1, 2]
4626 */
4627 without: function(arrayToSubstract) {
4628 arrayToSubstract = wysihtml5.lang.array(arrayToSubstract);
4629 var newArr = [],
4630 i = 0,
4631 length = arr.length;
4632 for (; i<length; i++) {
4633 if (!arrayToSubstract.contains(arr[i])) {
4634 newArr.push(arr[i]);
4635 }
4636 }
4637 return newArr;
4638 },
4639
4640 /**
4641 * Return a clean native array
4642 *
4643 * Following will convert a Live NodeList to a proper Array
4644 * @example
4645 * var childNodes = wysihtml5.lang.array(document.body.childNodes).get();
4646 */
4647 get: function() {
4648 var i = 0,
4649 length = arr.length,
4650 newArray = [];
4651 for (; i<length; i++) {
4652 newArray.push(arr[i]);
4653 }
4654 return newArray;
4655 },
4656
4657 /**
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
4660 *
4661 * @example
4662 * var childNodes = wysihtml5.lang.array([1,2,3,4]).map(function (value, index, array) {
4663 return value * 2;
4664 * });
4665 * // => [2,4,6,8]
4666 */
4667 map: function(callback, thisArg) {
4668 if (Array.prototype.map) {
4669 return arr.map(callback, thisArg);
4670 } else {
4671 var len = arr.length >>> 0,
4672 A = new Array(len),
4673 i = 0;
4674 for (; i < len; i++) {
4675 A[i] = callback.call(thisArg, arr[i], i, arr);
4676 }
4677 return A;
4678 }
4679 },
4680
4681 /* ReturnS new array without duplicate entries
4682 *
4683 * @example
4684 * var uniq = wysihtml5.lang.array([1,2,3,2,1,4]).unique();
4685 * // => [1,2,3,4]
4686 */
4687 unique: function() {
4688 var vals = [],
4689 max = arr.length,
4690 idx = 0;
4691
4692 while (idx < max) {
4693 if (!wysihtml5.lang.array(vals).contains(arr[idx])) {
4694 vals.push(arr[idx]);
4695 }
4696 idx++;
4697 }
4698 return vals;
4699 }
4700
4701 };
4702 };
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);
4709 return this;
4710 },
4711
4712 off: function(eventName, handler) {
4713 this.events = this.events || {};
4714 var i = 0,
4715 handlers,
4716 newHandlers;
4717 if (eventName) {
4718 handlers = this.events[eventName] || [],
4719 newHandlers = [];
4720 for (; i<handlers.length; i++) {
4721 if (handlers[i] !== handler && handler) {
4722 newHandlers.push(handlers[i]);
4723 }
4724 }
4725 this.events[eventName] = newHandlers;
4726 } else {
4727 // Clean up all events
4728 this.events = {};
4729 }
4730 return this;
4731 },
4732
4733 fire: function(eventName, payload) {
4734 this.events = this.events || {};
4735 var handlers = this.events[eventName] || [],
4736 i = 0;
4737 for (; i<handlers.length; i++) {
4738 handlers[i].call(this, payload);
4739 }
4740 return this;
4741 },
4742
4743 // deprecated, use .on()
4744 observe: function() {
4745 return this.on.apply(this, arguments);
4746 },
4747
4748 // deprecated, use .off()
4749 stopObserving: function() {
4750 return this.off.apply(this, arguments);
4751 }
4752 });
4753 ;wysihtml5.lang.object = function(obj) {
4754 return {
4755 /**
4756 * @example
4757 * wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get();
4758 * // => { foo: 1, bar: 2, baz: 3 }
4759 */
4760 merge: function(otherObj) {
4761 for (var i in otherObj) {
4762 obj[i] = otherObj[i];
4763 }
4764 return this;
4765 },
4766
4767 get: function() {
4768 return obj;
4769 },
4770
4771 /**
4772 * @example
4773 * wysihtml5.lang.object({ foo: 1 }).clone();
4774 * // => { foo: 1 }
4775 *
4776 * v0.4.14 adds options for deep clone : wysihtml5.lang.object({ foo: 1 }).clone(true);
4777 */
4778 clone: function(deep) {
4779 var newObj = {},
4780 i;
4781
4782 if (obj === null || !wysihtml5.lang.object(obj).isPlainObject()) {
4783 return obj;
4784 }
4785
4786 for (i in obj) {
4787 if(obj.hasOwnProperty(i)) {
4788 if (deep) {
4789 newObj[i] = wysihtml5.lang.object(obj[i]).clone(deep);
4790 } else {
4791 newObj[i] = obj[i];
4792 }
4793 }
4794 }
4795 return newObj;
4796 },
4797
4798 /**
4799 * @example
4800 * wysihtml5.lang.object([]).isArray();
4801 * // => true
4802 */
4803 isArray: function() {
4804 return Object.prototype.toString.call(obj) === "[object Array]";
4805 },
4806
4807 /**
4808 * @example
4809 * wysihtml5.lang.object(function() {}).isFunction();
4810 * // => true
4811 */
4812 isFunction: function() {
4813 return Object.prototype.toString.call(obj) === '[object Function]';
4814 },
4815
4816 isPlainObject: function () {
4817 return Object.prototype.toString.call(obj) === '[object Object]';
4818 }
4819 };
4820 };
4821 ;(function() {
4822 var WHITE_SPACE_START = /^\s+/,
4823 WHITE_SPACE_END = /\s+$/,
4824 ENTITY_REG_EXP = /[&<>\t"]/g,
4825 ENTITY_MAP = {
4826 '&': '&amp;',
4827 '<': '&lt;',
4828 '>': '&gt;',
4829 '"': "&quot;",
4830 '\t':"&nbsp; "
4831 };
4832 wysihtml5.lang.string = function(str) {
4833 str = String(str);
4834 return {
4835 /**
4836 * @example
4837 * wysihtml5.lang.string(" foo ").trim();
4838 * // => "foo"
4839 */
4840 trim: function() {
4841 return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, "");
4842 },
4843
4844 /**
4845 * @example
4846 * wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" });
4847 * // => "Hello Christopher"
4848 */
4849 interpolate: function(vars) {
4850 for (var i in vars) {
4851 str = this.replace("#{" + i + "}").by(vars[i]);
4852 }
4853 return str;
4854 },
4855
4856 /**
4857 * @example
4858 * wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans");
4859 * // => "Hello Hans"
4860 */
4861 replace: function(search) {
4862 return {
4863 by: function(replace) {
4864 return str.split(search).join(replace);
4865 }
4866 };
4867 },
4868
4869 /**
4870 * @example
4871 * wysihtml5.lang.string("hello<br>").escapeHTML();
4872 * // => "hello&lt;br&gt;"
4873 */
4874 escapeHTML: function(linebreaks, convertSpaces) {
4875 var html = str.replace(ENTITY_REG_EXP, function(c) { return ENTITY_MAP[c]; });
4876 if (linebreaks) {
4877 html = html.replace(/(?:\r\n|\r|\n)/g, '<br />');
4878 }
4879 if (convertSpaces) {
4880 html = html.replace(/ /gi, "&nbsp; ");
4881 }
4882 return html;
4883 }
4884 };
4885 };
4886 })();
4887 ;/**
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/
4890 *
4891 * @param {Element} element Container element in which to search for urls
4892 *
4893 * @example
4894 * <div id="text-container">Please click here: www.google.com</div>
4895 * <script>wysihtml5.dom.autoLink(document.getElementById("text-container"));</script>
4896 */
4897 (function(wysihtml5) {
4898 var /**
4899 * Don't auto-link urls that are contained in the following elements:
4900 */
4901 IGNORE_URLS_IN = wysihtml5.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),
4902 /**
4903 * revision 1:
4904 * /(\S+\.{1}[^\s\,\.\!]+)/g
4905 *
4906 * revision 2:
4907 * /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
4908 *
4909 * put this in the beginning if you don't wan't to match within a word
4910 * (^|[\>\(\{\[\s\>])
4911 */
4912 URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi,
4913 TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,
4914 MAX_DISPLAY_LENGTH = 100,
4915 BRACKETS = { ")": "(", "]": "[", "}": "{" };
4916
4917 function autoLink(element, ignoreInClasses) {
4918 if (_hasParentThatShouldBeIgnored(element, ignoreInClasses)) {
4919 return element;
4920 }
4921
4922 if (element === element.ownerDocument.documentElement) {
4923 element = element.ownerDocument.body;
4924 }
4925
4926 return _parseNode(element, ignoreInClasses);
4927 }
4928
4929 /**
4930 * This is basically a rebuild of
4931 * the rails auto_link_urls text helper
4932 */
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, "");
4938
4939 if (url.split(opening).length > url.split(punctuation).length) {
4940 url = url + punctuation;
4941 punctuation = "";
4942 }
4943 var realUrl = url,
4944 displayUrl = url;
4945 if (url.length > MAX_DISPLAY_LENGTH) {
4946 displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";
4947 }
4948 // Add http prefix if necessary
4949 if (realUrl.substr(0, 4) === "www.") {
4950 realUrl = "http://" + realUrl;
4951 }
4952
4953 return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation;
4954 });
4955 }
4956
4957 /**
4958 * Creates or (if already cached) returns a temp element
4959 * for the given document object
4960 */
4961 function _getTempElement(context) {
4962 var tempElement = context._wysihtml5_tempElement;
4963 if (!tempElement) {
4964 tempElement = context._wysihtml5_tempElement = context.createElement("div");
4965 }
4966 return tempElement;
4967 }
4968
4969 /**
4970 * Replaces the original text nodes with the newly auto-linked dom tree
4971 */
4972 function _wrapMatchesInNode(textNode) {
4973 var parentNode = textNode.parentNode,
4974 nodeValue = wysihtml5.lang.string(textNode.data).escapeHTML(),
4975 tempElement = _getTempElement(parentNode.ownerDocument);
4976
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);
4981
4982 while (tempElement.firstChild) {
4983 // inserts tempElement.firstChild before textNode
4984 parentNode.insertBefore(tempElement.firstChild, textNode);
4985 }
4986 parentNode.removeChild(textNode);
4987 }
4988
4989 function _hasParentThatShouldBeIgnored(node, ignoreInClasses) {
4990 var nodeName;
4991 while (node.parentNode) {
4992 node = node.parentNode;
4993 nodeName = node.nodeName;
4994 if (node.className && wysihtml5.lang.array(node.className.split(' ')).contains(ignoreInClasses)) {
4995 return true;
4996 }
4997 if (IGNORE_URLS_IN.contains(nodeName)) {
4998 return true;
4999 } else if (nodeName === "body") {
5000 return false;
5001 }
5002 }
5003 return false;
5004 }
5005
5006 function _parseNode(element, ignoreInClasses) {
5007 if (IGNORE_URLS_IN.contains(element.nodeName)) {
5008 return;
5009 }
5010
5011 if (element.className && wysihtml5.lang.array(element.className.split(' ')).contains(ignoreInClasses)) {
5012 return;
5013 }
5014
5015 if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) {
5016 _wrapMatchesInNode(element);
5017 return;
5018 }
5019
5020 var childNodes = wysihtml5.lang.array(element.childNodes).get(),
5021 childNodesLength = childNodes.length,
5022 i = 0;
5023
5024 for (; i<childNodesLength; i++) {
5025 _parseNode(childNodes[i], ignoreInClasses);
5026 }
5027
5028 return element;
5029 }
5030
5031 wysihtml5.dom.autoLink = autoLink;
5032
5033 // Reveal url reg exp to the outside
5034 wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP;
5035 })(wysihtml5);
5036 ;(function(wysihtml5) {
5037 var api = wysihtml5.dom;
5038
5039 api.addClass = function(element, className) {
5040 var classList = element.classList;
5041 if (classList) {
5042 return classList.add(className);
5043 }
5044 if (api.hasClass(element, className)) {
5045 return;
5046 }
5047 element.className += " " + className;
5048 };
5049
5050 api.removeClass = function(element, className) {
5051 var classList = element.classList;
5052 if (classList) {
5053 return classList.remove(className);
5054 }
5055
5056 element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " ");
5057 };
5058
5059 api.hasClass = function(element, className) {
5060 var classList = element.classList;
5061 if (classList) {
5062 return classList.contains(className);
5063 }
5064
5065 var elementClassName = element.className;
5066 return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
5067 };
5068 })(wysihtml5);
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;
5075 }
5076 return container !== element && container.contains(element);
5077 };
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);
5082 };
5083 }
5084 })();
5085 ;/**
5086 * Converts an HTML fragment/element into a unordered/ordered list
5087 *
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
5091 *
5092 * @example
5093 * <!-- Assume the following dom: -->
5094 * <span id="pseudo-list">
5095 * eminem<br>
5096 * dr. dre
5097 * <div>50 Cent</div>
5098 * </span>
5099 *
5100 * <script>
5101 * wysihtml5.dom.convertToList(document.getElementById("pseudo-list"), "ul");
5102 * </script>
5103 *
5104 * <!-- Will result in: -->
5105 * <ul>
5106 * <li>eminem</li>
5107 * <li>dr. dre</li>
5108 * <li>50 Cent</li>
5109 * </ul>
5110 */
5111 wysihtml5.dom.convertToList = (function() {
5112 function _createListItem(doc, list) {
5113 var listItem = doc.createElement("li");
5114 list.appendChild(listItem);
5115 return listItem;
5116 }
5117
5118 function _createList(doc, type) {
5119 return doc.createElement(type);
5120 }
5121
5122 function convertToList(element, listType, uneditableClass) {
5123 if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {
5124 // Already a list
5125 return element;
5126 }
5127
5128 var doc = element.ownerDocument,
5129 list = _createList(doc, listType),
5130 lineBreaks = element.querySelectorAll("br"),
5131 lineBreaksLength = lineBreaks.length,
5132 childNodes,
5133 childNodesLength,
5134 childNode,
5135 lineBreak,
5136 parentNode,
5137 isBlockElement,
5138 isLineBreak,
5139 currentListItem,
5140 i;
5141
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);
5148 break;
5149 }
5150 wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode);
5151 }
5152 }
5153
5154 childNodes = wysihtml5.lang.array(element.childNodes).get();
5155 childNodesLength = childNodes.length;
5156
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";
5162
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;
5169 continue;
5170 }
5171
5172 if (isLineBreak) {
5173 // Only create a new list item in the next iteration when the current one has already content
5174 currentListItem = currentListItem.firstChild ? null : currentListItem;
5175 continue;
5176 }
5177
5178 currentListItem.appendChild(childNode);
5179 }
5180
5181 if (childNodes.length === 0) {
5182 _createListItem(doc, list);
5183 }
5184
5185 element.parentNode.replaceChild(list, element);
5186 return list;
5187 }
5188
5189 return convertToList;
5190 })();
5191 ;/**
5192 * Copy a set of attributes from one element to another
5193 *
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)
5198 *
5199 * @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);
5204 *
5205 */
5206 wysihtml5.dom.copyAttributes = function(attributesToCopy) {
5207 return {
5208 from: function(elementToCopyFrom) {
5209 return {
5210 to: function(elementToCopyTo) {
5211 var attribute,
5212 i = 0,
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];
5218 }
5219 }
5220 return { andTo: arguments.callee };
5221 }
5222 };
5223 }
5224 };
5225 };
5226 ;/**
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
5229 * is in the dom
5230 *
5231 * Interesting article on how to copy styles
5232 *
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)
5237 *
5238 * @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);
5243 *
5244 */
5245 (function(dom) {
5246
5247 /**
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
5251 *
5252 * See https://bugzilla.mozilla.org/show_bug.cgi?id=520992
5253 */
5254 var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"];
5255
5256 var shouldIgnoreBoxSizingBorderBox = function(element) {
5257 if (hasBoxSizingBorderBox(element)) {
5258 return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
5259 }
5260 return false;
5261 };
5262
5263 var hasBoxSizingBorderBox = function(element) {
5264 var i = 0,
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];
5269 }
5270 }
5271 };
5272
5273 dom.copyStyles = function(stylesToCopy) {
5274 return {
5275 from: function(element) {
5276 if (shouldIgnoreBoxSizingBorderBox(element)) {
5277 stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);
5278 }
5279
5280 var cssText = "",
5281 length = stylesToCopy.length,
5282 i = 0,
5283 property;
5284 for (; i<length; i++) {
5285 property = stylesToCopy[i];
5286 cssText += property + ":" + dom.getStyle(property).from(element) + ";";
5287 }
5288
5289 return {
5290 to: function(element) {
5291 dom.setStyles(cssText).on(element);
5292 return { andTo: arguments.callee };
5293 }
5294 };
5295 }
5296 };
5297 };
5298 })(wysihtml5.dom);
5299 ;/**
5300 * Event Delegation
5301 *
5302 * @example
5303 * wysihtml5.dom.delegate(document.body, "a", "click", function() {
5304 * // foo
5305 * });
5306 */
5307 (function(wysihtml5) {
5308
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));
5313
5314 while (target && target !== container) {
5315 if (match.contains(target)) {
5316 handler.call(target, event);
5317 break;
5318 }
5319 target = target.parentNode;
5320 }
5321 });
5322 };
5323
5324 })(wysihtml5);
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];
5329
5330 var _isBlankText = function(node) {
5331 return node.nodeType === wysihtml5.TEXT_NODE && (/^\s*$/g).test(node.data);
5332 };
5333
5334 return {
5335
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;
5340
5341 if (!prevNode) {
5342 return null;
5343 }
5344
5345 if (
5346 (!wysihtml5.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check.
5347 (options && options.ignoreBlankTexts && _isBlankText(prevNode)) // Blank text nodes bypassed if set
5348 ) {
5349 return wysihtml5.dom.domNode(prevNode).prev(options);
5350 }
5351
5352 return prevNode;
5353 },
5354
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;
5359
5360 if (!nextNode) {
5361 return null;
5362 }
5363
5364 if (
5365 (!wysihtml5.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check.
5366 (options && options.ignoreBlankTexts && _isBlankText(nextNode)) // blank text nodes bypassed if set
5367 ) {
5368 return wysihtml5.dom.domNode(nextNode).next(options);
5369 }
5370
5371 return nextNode;
5372 }
5373
5374
5375
5376 };
5377 };
5378 })(wysihtml5);;/**
5379 * Returns the given html wrapped in a div element
5380 *
5381 * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
5382 * when inserted via innerHTML
5383 *
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
5386 *
5387 * @example
5388 * wysihtml5.dom.getAsDom("<article>foo</article>");
5389 */
5390 wysihtml5.dom.getAsDom = (function() {
5391
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);
5399 return tempElement;
5400 };
5401
5402 /**
5403 * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
5404 */
5405 var _ensureHTML5Compatibility = function(context) {
5406 if (context._wysihtml5_supportsHTML5Tags) {
5407 return;
5408 }
5409 for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) {
5410 context.createElement(HTML5_ELEMENTS[i]);
5411 }
5412 context._wysihtml5_supportsHTML5Tags = true;
5413 };
5414
5415
5416 /**
5417 * List of html5 tags
5418 * taken from http://simon.html5.org/html5-elements
5419 */
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"
5424 ];
5425
5426 return function(html, context) {
5427 context = context || document;
5428 var tempElement;
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;
5435 } else {
5436 _ensureHTML5Compatibility(context);
5437 tempElement = _innerHTMLShiv(html, context);
5438 }
5439 return tempElement;
5440 };
5441 })();
5442 ;/**
5443 * Walks the dom tree from the given node up until it finds a match
5444 * Designed for optimal performance.
5445 *
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)
5450 * @example
5451 * var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] });
5452 * // ... or ...
5453 * var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" });
5454 * // ... or ...
5455 * var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g });
5456 */
5457 wysihtml5.dom.getParentElement = (function() {
5458
5459 function _isSameNodeName(nodeName, desiredNodeNames) {
5460 if (!desiredNodeNames || !desiredNodeNames.length) {
5461 return true;
5462 }
5463
5464 if (typeof(desiredNodeNames) === "string") {
5465 return nodeName === desiredNodeNames;
5466 } else {
5467 return wysihtml5.lang.array(desiredNodeNames).contains(nodeName);
5468 }
5469 }
5470
5471 function _isElement(node) {
5472 return node.nodeType === wysihtml5.ELEMENT_NODE;
5473 }
5474
5475 function _hasClassName(element, className, classRegExp) {
5476 var classNames = (element.className || "").match(classRegExp) || [];
5477 if (!className) {
5478 return !!classNames.length;
5479 }
5480 return classNames[classNames.length - 1] === className;
5481 }
5482
5483 function _hasStyle(element, cssStyle, styleRegExp) {
5484 var styles = (element.getAttribute('style') || "").match(styleRegExp) || [];
5485 if (!cssStyle) {
5486 return !!styles.length;
5487 }
5488 return styles[styles.length - 1] === cssStyle;
5489 }
5490
5491 return function(node, matchingSet, levels, container) {
5492 var findByStyle = (matchingSet.cssStyle || matchingSet.styleRegExp),
5493 findByClass = (matchingSet.className || matchingSet.classRegExp);
5494
5495 levels = levels || 50; // Go max 50 nodes upwards from current node
5496
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))
5501 ) {
5502 return node;
5503 }
5504 node = node.parentNode;
5505 }
5506 return null;
5507 };
5508 })();
5509 ;/**
5510 * Get element's style for a specific css property
5511 *
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", ...)
5514 *
5515 * @example
5516 * wysihtml5.dom.getStyle("display").from(document.body);
5517 * // => "block"
5518 */
5519 wysihtml5.dom.getStyle = (function() {
5520 var stylePropertyMapping = {
5521 "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"
5522 },
5523 REG_EXP_CAMELIZE = /\-[a-z]/g;
5524
5525 function camelize(str) {
5526 return str.replace(REG_EXP_CAMELIZE, function(match) {
5527 return match.charAt(1).toUpperCase();
5528 });
5529 }
5530
5531 return function(property) {
5532 return {
5533 from: function(element) {
5534 if (element.nodeType !== wysihtml5.ELEMENT_NODE) {
5535 return;
5536 }
5537
5538 var doc = element.ownerDocument,
5539 camelizedProperty = stylePropertyMapping[property] || camelize(property),
5540 style = element.style,
5541 currentStyle = element.currentStyle,
5542 styleValue = style[camelizedProperty];
5543 if (styleValue) {
5544 return styleValue;
5545 }
5546
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
5552 if (currentStyle) {
5553 try {
5554 return currentStyle[camelizedProperty];
5555 } catch(e) {
5556 //ie will occasionally fail for unknown reasons. swallowing exception
5557 }
5558 }
5559
5560 var win = doc.defaultView || doc.parentWindow,
5561 needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
5562 originalOverflow,
5563 returnValue;
5564
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";
5571 }
5572 returnValue = win.getComputedStyle(element, null).getPropertyValue(property);
5573 if (needsOverflowReset) {
5574 style.overflow = originalOverflow || "";
5575 }
5576 return returnValue;
5577 }
5578 }
5579 };
5580 };
5581 })();
5582 ;wysihtml5.dom.getTextNodes = function(node, ingoreEmpty){
5583 var all = [];
5584 for (node=node.firstChild;node;node=node.nextSibling){
5585 if (node.nodeType == 3) {
5586 if (!ingoreEmpty || !(/^\s*$/).test(node.innerText || node.textContent)) {
5587 all.push(node);
5588 }
5589 } else {
5590 all = all.concat(wysihtml5.dom.getTextNodes(node, ingoreEmpty));
5591 }
5592 }
5593 return all;
5594 };;/**
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
5598 *
5599 * @param {Object} doc The document object of the context where to check
5600 * @param {String} tagName Upper cased tag name
5601 * @example
5602 * wysihtml5.dom.hasElementWithTagName(document, "IMG");
5603 */
5604 wysihtml5.dom.hasElementWithTagName = (function() {
5605 var LIVE_CACHE = {},
5606 DOCUMENT_IDENTIFIER = 1;
5607
5608 function _getDocumentIdentifier(doc) {
5609 return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
5610 }
5611
5612 return function(doc, tagName) {
5613 var key = _getDocumentIdentifier(doc) + ":" + tagName,
5614 cacheEntry = LIVE_CACHE[key];
5615 if (!cacheEntry) {
5616 cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
5617 }
5618
5619 return cacheEntry.length > 0;
5620 };
5621 })();
5622 ;/**
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
5626 *
5627 * @param {Object} doc The document object of the context where to check
5628 * @param {String} tagName Upper cased tag name
5629 * @example
5630 * wysihtml5.dom.hasElementWithClassName(document, "foobar");
5631 */
5632 (function(wysihtml5) {
5633 var LIVE_CACHE = {},
5634 DOCUMENT_IDENTIFIER = 1;
5635
5636 function _getDocumentIdentifier(doc) {
5637 return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++);
5638 }
5639
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);
5645 }
5646
5647 var key = _getDocumentIdentifier(doc) + ":" + className,
5648 cacheEntry = LIVE_CACHE[key];
5649 if (!cacheEntry) {
5650 cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);
5651 }
5652
5653 return cacheEntry.length > 0;
5654 };
5655 })(wysihtml5);
5656 ;wysihtml5.dom.insert = function(elementToInsert) {
5657 return {
5658 after: function(element) {
5659 element.parentNode.insertBefore(elementToInsert, element.nextSibling);
5660 },
5661
5662 before: function(element) {
5663 element.parentNode.insertBefore(elementToInsert, element);
5664 },
5665
5666 into: function(element) {
5667 element.appendChild(elementToInsert);
5668 }
5669 };
5670 };
5671 ;wysihtml5.dom.insertCSS = function(rules) {
5672 rules = rules.join("\n");
5673
5674 return {
5675 into: function(doc) {
5676 var styleElement = doc.createElement("style");
5677 styleElement.type = "text/css";
5678
5679 if (styleElement.styleSheet) {
5680 styleElement.styleSheet.cssText = rules;
5681 } else {
5682 styleElement.appendChild(doc.createTextNode(rules));
5683 }
5684
5685 var link = doc.querySelector("head link");
5686 if (link) {
5687 link.parentNode.insertBefore(styleElement, link);
5688 return;
5689 } else {
5690 var head = doc.querySelector("head");
5691 if (head) {
5692 head.appendChild(styleElement);
5693 }
5694 }
5695 }
5696 };
5697 };
5698 ;// TODO: Refactor dom tree traversing here
5699 (function(wysihtml5) {
5700 wysihtml5.dom.lineBreaks = function(node) {
5701
5702 function _isLineBreak(n) {
5703 return n.nodeName === "BR";
5704 }
5705
5706 /**
5707 * Checks whether the elment causes a visual line break
5708 * (<br> or block elements)
5709 */
5710 function _isLineBreakOrBlockElement(element) {
5711 if (_isLineBreak(element)) {
5712 return true;
5713 }
5714
5715 if (wysihtml5.dom.getStyle("display").from(element) === "block") {
5716 return true;
5717 }
5718
5719 return false;
5720 }
5721
5722 return {
5723
5724 /* wysihtml5.dom.lineBreaks(element).add();
5725 *
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>)
5728 */
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});
5733
5734 if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
5735 wysihtml5.dom.insert(doc.createElement("br")).after(node);
5736 }
5737 if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
5738 wysihtml5.dom.insert(doc.createElement("br")).before(node);
5739 }
5740 },
5741
5742 /* wysihtml5.dom.lineBreaks(element).remove();
5743 *
5744 * Removes line breaks before and after the given node
5745 */
5746 remove: function(options) {
5747 var nextSibling = wysihtml5.dom.domNode(node).next({ignoreBlankTexts: true}),
5748 previousSibling = wysihtml5.dom.domNode(node).prev({ignoreBlankTexts: true});
5749
5750 if (nextSibling && _isLineBreak(nextSibling)) {
5751 nextSibling.parentNode.removeChild(nextSibling);
5752 }
5753 if (previousSibling && _isLineBreak(previousSibling)) {
5754 previousSibling.parentNode.removeChild(previousSibling);
5755 }
5756 }
5757 };
5758 };
5759 })(wysihtml5);;/**
5760 * Method to set dom events
5761 *
5762 * @example
5763 * wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
5764 */
5765 wysihtml5.dom.observe = function(element, eventNames, handler) {
5766 eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;
5767
5768 var handlerWrapper,
5769 eventName,
5770 i = 0,
5771 length = eventNames.length;
5772
5773 for (; i<length; i++) {
5774 eventName = eventNames[i];
5775 if (element.addEventListener) {
5776 element.addEventListener(eventName, handler, false);
5777 } else {
5778 handlerWrapper = function(event) {
5779 if (!("target" in event)) {
5780 event.target = event.srcElement;
5781 }
5782 event.preventDefault = event.preventDefault || function() {
5783 this.returnValue = false;
5784 };
5785 event.stopPropagation = event.stopPropagation || function() {
5786 this.cancelBubble = true;
5787 };
5788 handler.call(element, event);
5789 };
5790 element.attachEvent("on" + eventName, handlerWrapper);
5791 }
5792 }
5793
5794 return {
5795 stop: function() {
5796 var eventName,
5797 i = 0,
5798 length = eventNames.length;
5799 for (; i<length; i++) {
5800 eventName = eventNames[i];
5801 if (element.removeEventListener) {
5802 element.removeEventListener(eventName, handler, false);
5803 } else {
5804 element.detachEvent("on" + eventName, handlerWrapper);
5805 }
5806 }
5807 }
5808 };
5809 };
5810 ;/**
5811 * HTML Sanitizer
5812 * Rewrites the HTML based on given rules
5813 *
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
5819 *
5820 * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.
5821 *
5822 * @example
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, {
5825 * tags {
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
5830 * }
5831 * });
5832 * // => <div><div><span>foo bar</span></div></div>
5833 *
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>'
5837 *
5838 * var userHTML = '<div>foobar<br>foobar</div>';
5839 * wysihtml5.dom.parse(userHTML, {
5840 * tags: {
5841 * div: undefined,
5842 * br: true
5843 * }
5844 * });
5845 * // => ''
5846 *
5847 * var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
5848 * wysihtml5.dom.parse(userHTML, {
5849 * classes: {
5850 * red: 1,
5851 * green: 1
5852 * },
5853 * tags: {
5854 * div: {
5855 * rename_tag: "p"
5856 * }
5857 * }
5858 * });
5859 * // => '<p class="red">foo</p><p>bar</p>'
5860 */
5861
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 */
5865
5866 /**
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
5869 * node isn't closed
5870 *
5871 * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
5872 */
5873 var NODE_TYPE_MAPPING = {
5874 "1": _handleElement,
5875 "3": _handleText,
5876 "8": _handleComment
5877 },
5878 // Rename unknown tags to this
5879 DEFAULT_NODE_NAME = "span",
5880 WHITE_SPACE_REG_EXP = /\s+/,
5881 defaultRules = { tags: {}, classes: {} },
5882 currentRules = {};
5883
5884 /**
5885 * Iterates over all childs of the element, recreates them, appends them into a document fragment
5886 * which later replaces the entire body content
5887 */
5888 function parse(elementOrHtml, config) {
5889 wysihtml5.lang.object(currentRules).merge(defaultRules).merge(config.rules).get();
5890
5891 var context = config.context || elementOrHtml.ownerDocument || document,
5892 fragment = context.createDocumentFragment(),
5893 isString = typeof(elementOrHtml) === "string",
5894 clearInternals = false,
5895 element,
5896 newNode,
5897 firstChild;
5898
5899 if (config.clearInternals === true) {
5900 clearInternals = true;
5901 }
5902
5903 if (isString) {
5904 element = wysihtml5.dom.getAsDom(elementOrHtml, context);
5905 } else {
5906 element = elementOrHtml;
5907 }
5908
5909 if (currentRules.selectors) {
5910 _applySelectorRules(element, currentRules.selectors);
5911 }
5912
5913 while (element.firstChild) {
5914 firstChild = element.firstChild;
5915 newNode = _convert(firstChild, config.cleanUp, clearInternals, config.uneditableClass);
5916 if (newNode) {
5917 fragment.appendChild(newNode);
5918 }
5919 if (firstChild !== newNode) {
5920 element.removeChild(firstChild);
5921 }
5922 }
5923
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 ");
5929 }
5930 }
5931
5932 // Clear element contents
5933 element.innerHTML = "";
5934
5935 // Insert new DOM tree
5936 element.appendChild(fragment);
5937
5938 return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element;
5939 }
5940
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],
5946 i = 0,
5947 fragment,
5948 newNode,
5949 newChild;
5950
5951 // Passes directly elemets with uneditable class
5952 if (uneditableClass && oldNodeType === 1 && wysihtml5.dom.hasClass(oldNode, uneditableClass)) {
5953 return oldNode;
5954 }
5955
5956 newNode = method && method(oldNode, clearInternals);
5957
5958 // Remove or unwrap node in case of return value null or false
5959 if (!newNode) {
5960 if (newNode === false) {
5961 // false defines that tag should be removed but contents should remain (unwrap)
5962 fragment = oldNode.ownerDocument.createDocumentFragment();
5963
5964 for (i = oldChildsLength; i--;) {
5965 if (oldChilds[i]) {
5966 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
5967 if (newChild) {
5968 if (oldChilds[i] === newChild) {
5969 i--;
5970 }
5971 fragment.insertBefore(newChild, fragment.firstChild);
5972 }
5973 }
5974 }
5975
5976 if (wysihtml5.dom.getStyle("display").from(oldNode) === "block") {
5977 fragment.appendChild(oldNode.ownerDocument.createElement("br"));
5978 }
5979
5980 // TODO: try to minimize surplus spaces
5981 if (wysihtml5.lang.array([
5982 "div", "pre", "p",
5983 "table", "td", "th",
5984 "ul", "ol", "li",
5985 "dd", "dl",
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(" "));
5992 }
5993 }
5994
5995 if (fragment.normalize) {
5996 fragment.normalize();
5997 }
5998 return fragment;
5999 } else {
6000 // Remove
6001 return null;
6002 }
6003 }
6004
6005 // Converts all childnodes
6006 for (i=0; i<oldChildsLength; i++) {
6007 if (oldChilds[i]) {
6008 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
6009 if (newChild) {
6010 if (oldChilds[i] === newChild) {
6011 i--;
6012 }
6013 newNode.appendChild(newChild);
6014 }
6015 }
6016 }
6017
6018 // Cleanup senseless <span> elements
6019 if (cleanUp &&
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)
6024 ) {
6025 fragment = newNode.ownerDocument.createDocumentFragment();
6026 while (newNode.firstChild) {
6027 fragment.appendChild(newNode.firstChild);
6028 }
6029 if (fragment.normalize) {
6030 fragment.normalize();
6031 }
6032 return fragment;
6033 }
6034
6035 if (newNode.normalize) {
6036 newNode.normalize();
6037 }
6038 return newNode;
6039 }
6040
6041 function _applySelectorRules (element, selectorRules) {
6042 var sel, method, els;
6043
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]];
6050 }
6051 els = element.querySelectorAll(sel);
6052 for (var i = els.length; i--;) {
6053 method(els[i]);
6054 }
6055 }
6056 }
6057 }
6058
6059 function _handleElement(oldNode, clearInternals) {
6060 var rule,
6061 newNode,
6062 tagRules = currentRules.tags,
6063 nodeName = oldNode.nodeName.toLowerCase(),
6064 scopeName = oldNode.scopeName,
6065 renameTag;
6066
6067 /**
6068 * We already parsed that element
6069 * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
6070 */
6071 if (oldNode._wysihtml5) {
6072 return null;
6073 }
6074 oldNode._wysihtml5 = 1;
6075
6076 if (oldNode.className === "wysihtml5-temp") {
6077 return null;
6078 }
6079
6080 /**
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
6085 */
6086 if (scopeName && scopeName != "HTML") {
6087 nodeName = scopeName + ":" + nodeName;
6088 }
6089 /**
6090 * Repair node
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
6093 */
6094 if ("outerHTML" in oldNode) {
6095 if (!wysihtml5.browser.autoClosesUnclosedTags() &&
6096 oldNode.nodeName === "P" &&
6097 oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
6098 nodeName = "div";
6099 }
6100 }
6101
6102 if (nodeName in tagRules) {
6103 rule = tagRules[nodeName];
6104 if (!rule || rule.remove) {
6105 return null;
6106 } else if (rule.unwrap) {
6107 return false;
6108 }
6109 rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;
6110 } else if (oldNode.firstChild) {
6111 rule = { rename_tag: DEFAULT_NODE_NAME };
6112 } else {
6113 // Remove empty unknown elements
6114 return null;
6115 }
6116
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") {
6121 return false;
6122 } else if (rule.remove_action === "rename") {
6123 renameTag = rule.remove_action_rename_to || DEFAULT_NODE_NAME;
6124 } else {
6125 return null;
6126 }
6127 } else {
6128 return null;
6129 }
6130 }
6131
6132 newNode = oldNode.ownerDocument.createElement(renameTag || rule.rename_tag || nodeName);
6133 _handleAttributes(oldNode, newNode, rule, clearInternals);
6134 _handleStyles(oldNode, newNode, rule);
6135
6136 oldNode = null;
6137
6138 if (newNode.normalize) { newNode.normalize(); }
6139 return newNode;
6140 }
6141
6142 function _testTypes(oldNode, rules, types, clearInternals) {
6143 var definition, type;
6144
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")) {
6147 return true;
6148 }
6149
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)) {
6154 return true;
6155 }
6156 }
6157 }
6158 return false;
6159 }
6160
6161 function array_contains(a, obj) {
6162 var i = a.length;
6163 while (i--) {
6164 if (a[i] === obj) {
6165 return true;
6166 }
6167 }
6168 return false;
6169 }
6170
6171 function _testType(oldNode, definition) {
6172
6173 var nodeClasses = oldNode.getAttribute("class"),
6174 nodeStyles = oldNode.getAttribute("style"),
6175 classesLength, s, s_corrected, a, attr, currentClass, styleProp;
6176
6177 // test for methods
6178 if (definition.methods) {
6179 for (var m in definition.methods) {
6180 if (definition.methods.hasOwnProperty(m) && typeCeckMethods[m]) {
6181
6182 if (typeCeckMethods[m](oldNode)) {
6183 return true;
6184 }
6185 }
6186 }
6187 }
6188
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]]) {
6195 return true;
6196 }
6197 }
6198 }
6199
6200 // test for styles, if one found return true
6201 if (nodeStyles && definition.styles) {
6202
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(':');
6208
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()) ) {
6211 return true;
6212 }
6213 }
6214 }
6215 }
6216 }
6217 }
6218
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) {
6226 return true;
6227 }
6228 }
6229 }
6230 }
6231 }
6232 return false;
6233 }
6234
6235 function _handleStyles(oldNode, newNode, rule) {
6236 var s, v;
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))) {
6243 continue;
6244 }
6245 if (s === "float") {
6246 // IE compability
6247 newNode.style[(oldNode.style.styleFloat) ? 'styleFloat': 'cssFloat'] = v;
6248 } else if (oldNode.style[s]) {
6249 newNode.style[s] = v;
6250 }
6251 }
6252 }
6253 }
6254 };
6255
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);
6261 }
6262 }
6263 return returnAttributes;
6264 }
6265
6266 function _checkAttribute(attributeName, attributeValue, methodName, nodeName) {
6267 var method = attributeCheckMethods[methodName],
6268 newAttributeValue;
6269
6270 if (method) {
6271 if (attributeValue || (attributeName === "alt" && nodeName == "IMG")) {
6272 newAttributeValue = method(attributeValue);
6273 if (typeof(newAttributeValue) === "string") {
6274 return newAttributeValue;
6275 }
6276 }
6277 }
6278
6279 return false;
6280 }
6281
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(),
6285 attributes = {},
6286 oldAttributes = wysihtml5.dom.getAttributes(oldNode),
6287 attributeName, newValue, matchingAttributes;
6288
6289 for (attributeName in checkAttributes) {
6290 if ((/\*$/).test(attributeName)) {
6291
6292 matchingAttributes = _getAttributesBeginningWith(attributeName.slice(0,-1), oldAttributes);
6293 for (var i = 0, imax = matchingAttributes.length; i < imax; i++) {
6294
6295 newValue = _checkAttribute(matchingAttributes[i], oldAttributes[matchingAttributes[i]], checkAttributes[attributeName], oldNode.nodeName);
6296 if (newValue !== false) {
6297 attributes[matchingAttributes[i]] = newValue;
6298 }
6299 }
6300 } else {
6301 newValue = _checkAttribute(attributeName, oldAttributes[attributeName], checkAttributes[attributeName], oldNode.nodeName);
6302 if (newValue !== false) {
6303 attributes[attributeName] = newValue;
6304 }
6305 }
6306 }
6307
6308 return attributes;
6309 }
6310
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,
6319 i = 0,
6320 classes = [],
6321 styles = [],
6322 newClasses = [],
6323 oldClasses = [],
6324 classesLength,
6325 newClassesLength,
6326 currentClass,
6327 newClass,
6328 attributeName,
6329 method;
6330
6331 if (setAttributes) {
6332 attributes = wysihtml5.lang.object(setAttributes).clone();
6333 }
6334
6335 // check/convert values of attributes
6336 attributes = wysihtml5.lang.object(attributes).merge(_checkAttributes(oldNode, rule.check_attributes)).get();
6337
6338 if (setClass) {
6339 classes.push(setClass);
6340 }
6341
6342 if (addClass) {
6343 for (attributeName in addClass) {
6344 method = addClassMethods[addClass[attributeName]];
6345 if (!method) {
6346 continue;
6347 }
6348 newClass = method(wysihtml5.dom.getAttribute(oldNode, attributeName));
6349 if (typeof(newClass) === "string") {
6350 classes.push(newClass);
6351 }
6352 }
6353 }
6354
6355 if (addStyle) {
6356 for (attributeName in addStyle) {
6357 method = addStyleMethods[addStyle[attributeName]];
6358 if (!method) {
6359 continue;
6360 }
6361
6362 newStyle = method(wysihtml5.dom.getAttribute(oldNode, attributeName));
6363 if (typeof(newStyle) === "string") {
6364 styles.push(newStyle);
6365 }
6366 }
6367 }
6368
6369
6370 if (typeof(allowedClasses) === "string" && allowedClasses === "any" && oldNode.getAttribute("class")) {
6371 if (currentRules.classes_blacklist) {
6372 oldClasses = oldNode.getAttribute("class");
6373 if (oldClasses) {
6374 classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
6375 }
6376
6377 classesLength = classes.length;
6378 for (; i<classesLength; i++) {
6379 currentClass = classes[i];
6380 if (!currentRules.classes_blacklist[currentClass]) {
6381 newClasses.push(currentClass);
6382 }
6383 }
6384
6385 if (newClasses.length) {
6386 attributes["class"] = wysihtml5.lang.array(newClasses).unique().join(" ");
6387 }
6388
6389 } else {
6390 attributes["class"] = oldNode.getAttribute("class");
6391 }
6392 } else {
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;
6398 }
6399
6400 // add old classes last
6401 oldClasses = oldNode.getAttribute("class");
6402 if (oldClasses) {
6403 classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
6404 }
6405 classesLength = classes.length;
6406 for (; i<classesLength; i++) {
6407 currentClass = classes[i];
6408 if (allowedClasses[currentClass]) {
6409 newClasses.push(currentClass);
6410 }
6411 }
6412
6413 if (newClasses.length) {
6414 attributes["class"] = wysihtml5.lang.array(newClasses).unique().join(" ");
6415 }
6416 }
6417
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"];
6423 }
6424 }
6425
6426 if (styles.length) {
6427 attributes["style"] = wysihtml5.lang.array(styles).unique().join(" ");
6428 }
6429
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
6435 try {
6436 newNode.setAttribute(attributeName, attributes[attributeName]);
6437 } catch(e) {}
6438 }
6439
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);
6445 }
6446 if (typeof(attributes.height) !== "undefined") {
6447 newNode.setAttribute("height", attributes.height);
6448 }
6449 }
6450 }
6451
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, "");
6458 } else {
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);
6462 }
6463 }
6464
6465 function _handleComment(oldNode) {
6466 if (currentRules.comments) {
6467 return oldNode.ownerDocument.createComment(oldNode.nodeValue);
6468 }
6469 }
6470
6471 // ------------ attribute checks ------------ \\
6472 var attributeCheckMethods = {
6473 url: (function() {
6474 var REG_EXP = /^https?:\/\//i;
6475 return function(attributeValue) {
6476 if (!attributeValue || !attributeValue.match(REG_EXP)) {
6477 return null;
6478 }
6479 return attributeValue.replace(REG_EXP, function(match) {
6480 return match.toLowerCase();
6481 });
6482 };
6483 })(),
6484
6485 src: (function() {
6486 var REG_EXP = /^(\/|https?:\/\/)/i;
6487 return function(attributeValue) {
6488 if (!attributeValue || !attributeValue.match(REG_EXP)) {
6489 return null;
6490 }
6491 return attributeValue.replace(REG_EXP, function(match) {
6492 return match.toLowerCase();
6493 });
6494 };
6495 })(),
6496
6497 href: (function() {
6498 var REG_EXP = /^(#|\/|https?:\/\/|mailto:)/i;
6499 return function(attributeValue) {
6500 if (!attributeValue || !attributeValue.match(REG_EXP)) {
6501 return null;
6502 }
6503 return attributeValue.replace(REG_EXP, function(match) {
6504 return match.toLowerCase();
6505 });
6506 };
6507 })(),
6508
6509 alt: (function() {
6510 var REG_EXP = /[^ a-z0-9_\-]/gi;
6511 return function(attributeValue) {
6512 if (!attributeValue) {
6513 return "";
6514 }
6515 return attributeValue.replace(REG_EXP, "");
6516 };
6517 })(),
6518
6519 numbers: (function() {
6520 var REG_EXP = /\D/g;
6521 return function(attributeValue) {
6522 attributeValue = (attributeValue || "").replace(REG_EXP, "");
6523 return attributeValue || null;
6524 };
6525 })(),
6526
6527 any: (function() {
6528 return function(attributeValue) {
6529 return attributeValue;
6530 };
6531 })()
6532 };
6533
6534 // ------------ style converter (converts an html attribute to a style) ------------ \\
6535 var addStyleMethods = {
6536 align_text: (function() {
6537 var mapping = {
6538 left: "text-align: left;",
6539 right: "text-align: right;",
6540 center: "text-align: center;"
6541 };
6542 return function(attributeValue) {
6543 return mapping[String(attributeValue).toLowerCase()];
6544 };
6545 })(),
6546 };
6547
6548 // ------------ class converter (converts an html attribute to a class name) ------------ \\
6549 var addClassMethods = {
6550 align_img: (function() {
6551 var mapping = {
6552 left: "wysiwyg-float-left",
6553 right: "wysiwyg-float-right"
6554 };
6555 return function(attributeValue) {
6556 return mapping[String(attributeValue).toLowerCase()];
6557 };
6558 })(),
6559
6560 align_text: (function() {
6561 var mapping = {
6562 left: "wysiwyg-text-align-left",
6563 right: "wysiwyg-text-align-right",
6564 center: "wysiwyg-text-align-center",
6565 justify: "wysiwyg-text-align-justify"
6566 };
6567 return function(attributeValue) {
6568 return mapping[String(attributeValue).toLowerCase()];
6569 };
6570 })(),
6571
6572 clear_br: (function() {
6573 var mapping = {
6574 left: "wysiwyg-clear-left",
6575 right: "wysiwyg-clear-right",
6576 both: "wysiwyg-clear-both",
6577 all: "wysiwyg-clear-both"
6578 };
6579 return function(attributeValue) {
6580 return mapping[String(attributeValue).toLowerCase()];
6581 };
6582 })(),
6583
6584 size_font: (function() {
6585 var mapping = {
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"
6595 };
6596 return function(attributeValue) {
6597 return mapping[String(attributeValue).charAt(0)];
6598 };
6599 })()
6600 };
6601
6602 // checks if element is possibly visible
6603 var typeCeckMethods = {
6604 has_visible_contet: (function() {
6605 var txt,
6606 isVisible = false,
6607 visibleElements = ['img', 'video', 'picture', 'br', 'script', 'noscript',
6608 'style', 'table', 'iframe', 'object', 'embed', 'audio',
6609 'svg', 'input', 'button', 'select','textarea', 'canvas'];
6610
6611 return function(el) {
6612
6613 // has visible innertext. so is visible
6614 txt = (el.innerText || el.textContent).replace(/\s/g, '');
6615 if (txt && txt.length > 0) {
6616 return true;
6617 }
6618
6619 // matches list of visible dimensioned elements
6620 for (var i = visibleElements.length; i--;) {
6621 if (el.querySelector(visibleElements[i])) {
6622 return true;
6623 }
6624 }
6625
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) {
6628 return true;
6629 }
6630
6631 return false;
6632 };
6633 })()
6634 };
6635
6636 var elementHandlingMethods = {
6637 unwrap: function (element) {
6638 wysihtml5.dom.unwrap(element);
6639 },
6640
6641 remove: function (element) {
6642 element.parentNode.removeChild(element);
6643 }
6644 };
6645
6646 return parse(elementOrHtml_current, config_current);
6647 };
6648 ;/**
6649 * Checks for empty text node childs and removes them
6650 *
6651 * @param {Element} node The element in which to cleanup
6652 * @example
6653 * wysihtml5.dom.removeEmptyTextNodes(element);
6654 */
6655 wysihtml5.dom.removeEmptyTextNodes = function(node) {
6656 var childNode,
6657 childNodes = wysihtml5.lang.array(node.childNodes).get(),
6658 childNodesLength = childNodes.length,
6659 i = 0;
6660 for (; i<childNodesLength; i++) {
6661 childNode = childNodes[i];
6662 if (childNode.nodeType === wysihtml5.TEXT_NODE && childNode.data === "") {
6663 childNode.parentNode.removeChild(childNode);
6664 }
6665 }
6666 };
6667 ;/**
6668 * Renames an element (eg. a <div> to a <p>) and keeps its childs
6669 *
6670 * @param {Element} element The list element which should be renamed
6671 * @param {Element} newNodeName The desired tag name
6672 *
6673 * @example
6674 * <!-- Assume the following dom: -->
6675 * <ul id="list">
6676 * <li>eminem</li>
6677 * <li>dr. dre</li>
6678 * <li>50 Cent</li>
6679 * </ul>
6680 *
6681 * <script>
6682 * wysihtml5.dom.renameElement(document.getElementById("list"), "ol");
6683 * </script>
6684 *
6685 * <!-- Will result in: -->
6686 * <ol>
6687 * <li>eminem</li>
6688 * <li>dr. dre</li>
6689 * <li>50 Cent</li>
6690 * </ol>
6691 */
6692 wysihtml5.dom.renameElement = function(element, newNodeName) {
6693 var newElement = element.ownerDocument.createElement(newNodeName),
6694 firstChild;
6695 while (firstChild = element.firstChild) {
6696 newElement.appendChild(firstChild);
6697 }
6698 wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement);
6699 element.parentNode.replaceChild(newElement, element);
6700 return newElement;
6701 };
6702 ;/**
6703 * Takes an element, removes it and replaces it with it's childs
6704 *
6705 * @param {Object} node The node which to replace with it's child nodes
6706 * @example
6707 * <div id="foo">
6708 * <span>hello</span>
6709 * </div>
6710 * <script>
6711 * // Remove #foo and replace with it's children
6712 * wysihtml5.dom.replaceWithChildNodes(document.getElementById("foo"));
6713 * </script>
6714 */
6715 wysihtml5.dom.replaceWithChildNodes = function(node) {
6716 if (!node.parentNode) {
6717 return;
6718 }
6719
6720 if (!node.firstChild) {
6721 node.parentNode.removeChild(node);
6722 return;
6723 }
6724
6725 var fragment = node.ownerDocument.createDocumentFragment();
6726 while (node.firstChild) {
6727 fragment.appendChild(node.firstChild);
6728 }
6729 node.parentNode.replaceChild(fragment, node);
6730 node = fragment = null;
6731 };
6732 ;/**
6733 * Unwraps an unordered/ordered list
6734 *
6735 * @param {Element} element The list element which should be unwrapped
6736 *
6737 * @example
6738 * <!-- Assume the following dom: -->
6739 * <ul id="list">
6740 * <li>eminem</li>
6741 * <li>dr. dre</li>
6742 * <li>50 Cent</li>
6743 * </ul>
6744 *
6745 * <script>
6746 * wysihtml5.dom.resolveList(document.getElementById("list"));
6747 * </script>
6748 *
6749 * <!-- Will result in: -->
6750 * eminem<br>
6751 * dr. dre<br>
6752 * 50 Cent<br>
6753 */
6754 (function(dom) {
6755 function _isBlockElement(node) {
6756 return dom.getStyle("display").from(node) === "block";
6757 }
6758
6759 function _isLineBreak(node) {
6760 return node.nodeName === "BR";
6761 }
6762
6763 function _appendLineBreak(element) {
6764 var lineBreak = element.ownerDocument.createElement("br");
6765 element.appendChild(lineBreak);
6766 }
6767
6768 function resolveList(list, useLineBreaks) {
6769 if (!list.nodeName.match(/^(MENU|UL|OL)$/)) {
6770 return;
6771 }
6772
6773 var doc = list.ownerDocument,
6774 fragment = doc.createDocumentFragment(),
6775 previousSibling = wysihtml5.dom.domNode(list).prev({ignoreBlankTexts: true}),
6776 firstChild,
6777 lastChild,
6778 isLastChild,
6779 shouldAppendLineBreak,
6780 paragraph,
6781 listItem;
6782
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);
6787 }
6788
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);
6798 }
6799 }
6800
6801 listItem.parentNode.removeChild(listItem);
6802 }
6803 } else {
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);
6808 }
6809 } else {
6810 paragraph = doc.createElement("p");
6811 while (firstChild = listItem.firstChild) {
6812 paragraph.appendChild(firstChild);
6813 }
6814 fragment.appendChild(paragraph);
6815 }
6816 listItem.parentNode.removeChild(listItem);
6817 }
6818 }
6819
6820 list.parentNode.replaceChild(fragment, list);
6821 }
6822
6823 dom.resolveList = resolveList;
6824 })(wysihtml5.dom);
6825 ;/**
6826 * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
6827 *
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, ...)
6831 *
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
6838 *
6839 * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
6840 * @param {Object} [config] Optional parameters
6841 *
6842 * @example
6843 * new wysihtml5.dom.Sandbox(function(sandbox) {
6844 * sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
6845 * });
6846 */
6847 (function(wysihtml5) {
6848 var /**
6849 * Default configuration
6850 */
6851 doc = document,
6852 /**
6853 * Properties to unset/protect on the window object
6854 */
6855 windowProperties = [
6856 "parent", "top", "opener", "frameElement", "frames",
6857 "localStorage", "globalStorage", "sessionStorage", "indexedDB"
6858 ],
6859 /**
6860 * Properties on the window object which are set to an empty function
6861 */
6862 windowProperties2 = [
6863 "open", "close", "openDialog", "showModalDialog",
6864 "alert", "confirm", "prompt",
6865 "openDatabase", "postMessage",
6866 "XMLHttpRequest", "XDomainRequest"
6867 ],
6868 /**
6869 * Properties to unset/protect on the document object
6870 */
6871 documentProperties = [
6872 "referrer",
6873 "write", "open", "close"
6874 ];
6875
6876 wysihtml5.dom.Sandbox = Base.extend(
6877 /** @scope wysihtml5.dom.Sandbox.prototype */ {
6878
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();
6883 },
6884
6885 insertInto: function(element) {
6886 if (typeof(element) === "string") {
6887 element = doc.getElementById(element);
6888 }
6889
6890 element.appendChild(this.editableArea);
6891 },
6892
6893 getIframe: function() {
6894 return this.editableArea;
6895 },
6896
6897 getWindow: function() {
6898 this._readyError();
6899 },
6900
6901 getDocument: function() {
6902 this._readyError();
6903 },
6904
6905 destroy: function() {
6906 var iframe = this.getIframe();
6907 iframe.parentNode.removeChild(iframe);
6908 },
6909
6910 _readyError: function() {
6911 throw new Error("wysihtml5.Sandbox: Sandbox iframe isn't loaded yet");
6912 },
6913
6914 /**
6915 * Creates the sandbox iframe
6916 *
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
6932 */
6933 _createIframe: function() {
6934 var that = this,
6935 iframe = doc.createElement("iframe");
6936 iframe.className = "wysihtml5-sandbox";
6937 wysihtml5.dom.setAttributes({
6938 "security": "restricted",
6939 "allowtransparency": "true",
6940 "frameborder": 0,
6941 "width": 0,
6942 "height": 0,
6943 "marginwidth": 0,
6944 "marginheight": 0
6945 }).on(iframe);
6946
6947 // Setting the src like this prevents ssl warnings in IE6
6948 if (wysihtml5.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
6949 iframe.src = "javascript:'<html></html>'";
6950 }
6951
6952 iframe.onload = function() {
6953 iframe.onreadystatechange = iframe.onload = null;
6954 that._onLoadIframe(iframe);
6955 };
6956
6957 iframe.onreadystatechange = function() {
6958 if (/loaded|complete/.test(iframe.readyState)) {
6959 iframe.onreadystatechange = iframe.onload = null;
6960 that._onLoadIframe(iframe);
6961 }
6962 };
6963
6964 return iframe;
6965 },
6966
6967 /**
6968 * Callback for when the iframe has finished loading
6969 */
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)) {
6973 return;
6974 }
6975
6976 var that = this,
6977 iframeWindow = iframe.contentWindow,
6978 iframeDocument = iframe.contentWindow.document,
6979 charset = doc.characterSet || doc.charset || "utf-8",
6980 sandboxHtml = this._getHtml({
6981 charset: charset,
6982 stylesheets: this.config.stylesheets
6983 });
6984
6985 // Create the basic dom tree including proper DOCTYPE and charset
6986 iframeDocument.open("text/html", "replace");
6987 iframeDocument.write(sandboxHtml);
6988 iframeDocument.close();
6989
6990 this.getWindow = function() { return iframe.contentWindow; };
6991 this.getDocument = function() { return iframe.contentWindow.document; };
6992
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);
6998 };
6999
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, ...)
7007 var i, length;
7008 for (i=0, length=windowProperties.length; i<length; i++) {
7009 this._unset(iframeWindow, windowProperties[i]);
7010 }
7011 for (i=0, length=windowProperties2.length; i<length; i++) {
7012 this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION);
7013 }
7014 for (i=0, length=documentProperties.length; i<length; i++) {
7015 this._unset(iframeDocument, documentProperties[i]);
7016 }
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);
7020 }
7021
7022 this.loaded = true;
7023
7024 // Trigger the callback
7025 setTimeout(function() { that.callback(that); }, 0);
7026 },
7027
7028 _getHtml: function(templateVars) {
7029 var stylesheets = templateVars.stylesheets,
7030 html = "",
7031 i = 0,
7032 length;
7033 stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
7034 if (stylesheets) {
7035 length = stylesheets.length;
7036 for (; i<length; i++) {
7037 html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
7038 }
7039 }
7040 templateVars.stylesheets = html;
7041
7042 return wysihtml5.lang.string(
7043 '<!DOCTYPE html><html><head>'
7044 + '<meta charset="#{charset}">#{stylesheets}</head>'
7045 + '<body></body></html>'
7046 ).interpolate(templateVars);
7047 },
7048
7049 /**
7050 * Method to unset/override existing variables
7051 * @example
7052 * // Make cookie unreadable and unwritable
7053 * this._unset(document, "cookie", "", true);
7054 */
7055 _unset: function(object, property, value, setter) {
7056 try { object[property] = value; } catch(e) {}
7057
7058 try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
7059 if (setter) {
7060 try { object.__defineSetter__(property, function() {}); } catch(e) {}
7061 }
7062
7063 if (!wysihtml5.browser.crashesWhenDefineProperty(property)) {
7064 try {
7065 var config = {
7066 get: function() { return value; }
7067 };
7068 if (setter) {
7069 config.set = function() {};
7070 }
7071 Object.defineProperty(object, property, config);
7072 } catch(e) {}
7073 }
7074 }
7075 });
7076 })(wysihtml5);
7077 ;(function(wysihtml5) {
7078 var doc = document;
7079 wysihtml5.dom.ContentEditableArea = Base.extend({
7080 getContentEditable: function() {
7081 return this.element;
7082 },
7083
7084 getWindow: function() {
7085 return this.element.ownerDocument.defaultView;
7086 },
7087
7088 getDocument: function() {
7089 return this.element.ownerDocument;
7090 },
7091
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);
7097 } else {
7098 this.element = this._createElement();
7099 }
7100 },
7101
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);
7107 return element;
7108 },
7109
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;
7115 },
7116
7117 _loadElement: function(element, contentExists) {
7118 var that = this;
7119 if (!contentExists) {
7120 var sandboxHtml = this._getHtml();
7121 element.innerHTML = sandboxHtml;
7122 }
7123
7124 this.getWindow = function() { return element.ownerDocument.defaultView; };
7125 this.getDocument = function() { return element.ownerDocument; };
7126
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);
7133 }
7134 */
7135 this.loaded = true;
7136 // Trigger the callback
7137 setTimeout(function() { that.callback(that); }, 0);
7138 },
7139
7140 _getHtml: function(templateVars) {
7141 return '';
7142 }
7143
7144 });
7145 })(wysihtml5);
7146 ;(function() {
7147 var mapping = {
7148 "className": "class"
7149 };
7150 wysihtml5.dom.setAttributes = function(attributes) {
7151 return {
7152 on: function(element) {
7153 for (var i in attributes) {
7154 element.setAttribute(mapping[i] || i, attributes[i]);
7155 }
7156 }
7157 };
7158 };
7159 })();
7160 ;wysihtml5.dom.setStyles = function(styles) {
7161 return {
7162 on: function(element) {
7163 var style = element.style;
7164 if (typeof(styles) === "string") {
7165 style.cssText += ";" + styles;
7166 return;
7167 }
7168 for (var i in styles) {
7169 if (i === "float") {
7170 style.cssFloat = styles[i];
7171 style.styleFloat = styles[i];
7172 } else {
7173 style[i] = styles[i];
7174 }
7175 }
7176 }
7177 };
7178 };
7179 ;/**
7180 * Simulate HTML5 placeholder attribute
7181 *
7182 * Needed since
7183 * - div[contentEditable] elements don't support it
7184 * - older browsers (such as IE8 and Firefox 3.6) don't support it at all
7185 *
7186 * @param {Object} parent Instance of main wysihtml5.Editor class
7187 * @param {Element} view Instance of wysihtml5.views.* class
7188 * @param {String} placeholderText
7189 *
7190 * @example
7191 * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
7192 */
7193 (function(dom) {
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()) {
7199 view.clear();
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);
7206 }
7207 }, 0);
7208 }
7209 }
7210 view.placeholderSet = false;
7211 dom.removeClass(view.element, CLASS_NAME);
7212 },
7213 set = function() {
7214 if (view.isEmpty()) {
7215 view.placeholderSet = true;
7216 view.setValue(placeholderText);
7217 dom.addClass(view.element, CLASS_NAME);
7218 }
7219 };
7220
7221 editor
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);
7227
7228 set();
7229 };
7230 })(wysihtml5.dom);
7231 ;(function(dom) {
7232 var documentElement = document.documentElement;
7233 if ("textContent" in documentElement) {
7234 dom.setTextContent = function(element, text) {
7235 element.textContent = text;
7236 };
7237
7238 dom.getTextContent = function(element) {
7239 return element.textContent;
7240 };
7241 } else if ("innerText" in documentElement) {
7242 dom.setTextContent = function(element, text) {
7243 element.innerText = text;
7244 };
7245
7246 dom.getTextContent = function(element) {
7247 return element.innerText;
7248 };
7249 } else {
7250 dom.setTextContent = function(element, text) {
7251 element.nodeValue = text;
7252 };
7253
7254 dom.getTextContent = function(element) {
7255 return element.nodeValue;
7256 };
7257 }
7258 })(wysihtml5.dom);
7259
7260 ;/**
7261 * Get a set of attribute from one element
7262 *
7263 * IE gives wrong results for hasAttribute/getAttribute, for example:
7264 * var td = document.createElement("td");
7265 * td.getAttribute("rowspan"); // => "1" in IE
7266 *
7267 * Therefore we have to check the element's outerHTML for the attribute
7268 */
7269
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)
7279 return node.src;
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;
7285
7286 return hasAttribute ? node.getAttribute(attributeName) : null;
7287 } else{
7288 return node.getAttribute(attributeName);
7289 }
7290 };
7291 ;/**
7292 * Get all attributes of an element
7293 *
7294 * IE gives wrong results for hasAttribute/getAttribute, for example:
7295 * var td = document.createElement("td");
7296 * td.getAttribute("rowspan"); // => "1" in IE
7297 *
7298 * Therefore we have to check the element's outerHTML for the attribute
7299 */
7300
7301 wysihtml5.dom.getAttributes = function(node) {
7302 var HAS_GET_ATTRIBUTE_BUG = !wysihtml5.browser.supportsGetAttributeCorrectly(),
7303 nodeName = node.nodeName,
7304 attributes = [],
7305 attr;
7306
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;
7315 }
7316 } else {
7317 attributes[node.attributes[attr].name] = node.attributes[attr].value;
7318 }
7319 }
7320 }
7321 }
7322 return attributes;
7323 };;/**
7324 * Check whether the given node is a proper loaded image
7325 * FIXME: Returns undefined when unknown (Chrome, Safari)
7326 */
7327
7328 wysihtml5.dom.isLoadedImage = function (node) {
7329 try {
7330 return node.complete && !node.mozMatchesSelector(":-moz-broken");
7331 } catch(e) {
7332 if (node.complete && node.readyState === "complete") {
7333 return true;
7334 }
7335 }
7336 };
7337 ;(function(wysihtml5) {
7338
7339 var api = wysihtml5.dom;
7340
7341 var MapCell = function(cell) {
7342 this.el = cell;
7343 this.isColspan= false;
7344 this.isRowspan= false;
7345 this.firstCol= true;
7346 this.lastCol= true;
7347 this.firstRow= true;
7348 this.lastRow= true;
7349 this.isReal= true;
7350 this.spanCollection= [];
7351 this.modified = false;
7352 };
7353
7354 var TableModifyerByCell = function (cell, table) {
7355 if (cell) {
7356 this.cell = cell;
7357 this.table = api.getParentElement(cell, { nodeName: ["TABLE"] });
7358 } else if (table) {
7359 this.table = table;
7360 this.cell = this.table.querySelectorAll('th, td')[0];
7361 }
7362 };
7363
7364 function queryInList(list, query) {
7365 var ret = [],
7366 q;
7367 for (var e = 0, len = list.length; e < len; e++) {
7368 q = list[e].querySelectorAll(query);
7369 if (q) {
7370 for(var i = q.length; i--; ret.unshift(q[i]));
7371 }
7372 }
7373 return ret;
7374 }
7375
7376 function removeElement(el) {
7377 el.parentNode.removeChild(el);
7378 }
7379
7380 function insertAfter(referenceNode, newNode) {
7381 referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
7382 }
7383
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()) {
7389 return element;
7390 }
7391 }
7392 return null;
7393 }
7394
7395 TableModifyerByCell.prototype = {
7396
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);
7401
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;
7414
7415 spanCollect.push(map[rr][cc]);
7416 }
7417 }
7418 },
7419
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;
7425 }
7426 }
7427 },
7428
7429 setTableMap: function() {
7430 var map = [];
7431 var tableRows = this.getTableRows(),
7432 ridx, row, cells, cidx, cell,
7433 c,
7434 cspan, rspan;
7435
7436 for (ridx = 0; ridx < tableRows.length; ridx++) {
7437 row = tableRows[ridx];
7438 cells = this.getRowCells(row);
7439 c = 0;
7440 if (typeof map[ridx] == "undefined") { map[ridx] = []; }
7441 for (cidx = 0; cidx < cells.length; cidx++) {
7442 cell = cells[cidx];
7443
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++; }
7447
7448 cspan = api.getAttribute(cell, 'colspan');
7449 rspan = api.getAttribute(cell, 'rowspan');
7450
7451 if (cspan || rspan) {
7452 this.addSpannedCellToMap(cell, map, ridx, c, cspan, rspan);
7453 c = c + ((cspan) ? parseInt(cspan, 10) : 1);
7454 } else {
7455 map[ridx][c] = new MapCell(cell);
7456 c++;
7457 }
7458 }
7459 }
7460 this.map = map;
7461 return map;
7462 },
7463
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;
7469
7470 return tableCells;
7471 },
7472
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;
7478
7479 return tableRows;
7480 },
7481
7482 getMapIndex: function(cell) {
7483 var r_length = this.map.length,
7484 c_length = (this.map && this.map[0]) ? this.map[0].length : 0;
7485
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};
7490 }
7491 }
7492 }
7493 return false;
7494 },
7495
7496 getElementAtIndex: function(idx) {
7497 this.setTableMap();
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;
7500 }
7501 return null;
7502 },
7503
7504 getMapElsTo: function(to_cell) {
7505 var els = [];
7506 this.setTableMap();
7507 this.idx_start = this.getMapIndex(this.cell);
7508 this.idx_end = this.getMapIndex(to_cell);
7509
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;
7515 }
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;
7520 }
7521
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);
7526 }
7527 }
7528 }
7529 return els;
7530 },
7531
7532 orderSelectionEnds: function(secondcell) {
7533 this.setTableMap();
7534 this.idx_start = this.getMapIndex(this.cell);
7535 this.idx_end = this.getMapIndex(secondcell);
7536
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;
7542 }
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;
7547 }
7548
7549 return {
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
7552 };
7553 },
7554
7555 createCells: function(tag, nr, attrs) {
7556 var doc = this.table.ownerDocument,
7557 frag = doc.createDocumentFragment(),
7558 cell;
7559 for (var i = 0; i < nr; i++) {
7560 cell = doc.createElement(tag);
7561
7562 if (attrs) {
7563 for (var attr in attrs) {
7564 if (attrs.hasOwnProperty(attr)) {
7565 cell.setAttribute(attr, attrs[attr]);
7566 }
7567 }
7568 }
7569
7570 // add non breaking space
7571 cell.appendChild(document.createTextNode("\u00a0"));
7572
7573 frag.appendChild(cell);
7574 }
7575 return frag;
7576 },
7577
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],
7581 corrIdx = -1;
7582 for (var i = 0, max = col; i < col; i++) {
7583 if (r[i].isReal){
7584 corrIdx++;
7585 }
7586 }
7587 return corrIdx;
7588 },
7589
7590 getLastNewCellOnRow: function(row, rowLimit) {
7591 var cells = this.getRowCells(row),
7592 cell, idx;
7593
7594 for (var cidx = 0, cmax = cells.length; cidx < cmax; cidx++) {
7595 cell = cells[cidx];
7596 idx = this.getMapIndex(cell);
7597 if (idx === false || (typeof rowLimit != "undefined" && idx.row != rowLimit)) {
7598 return cell;
7599 }
7600 }
7601 return null;
7602 },
7603
7604 removeEmptyTable: function() {
7605 var cells = this.table.querySelectorAll('td, th');
7606 if (!cells || cells.length == 0) {
7607 removeElement(this.table);
7608 return true;
7609 } else {
7610 return false;
7611 }
7612 },
7613
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();
7619 if (colspan > 1) {
7620 var newCells = this.createCells(cType, colspan -1);
7621 insertAfter(cell.el, newCells);
7622 }
7623 cell.el.removeAttribute('colspan');
7624 }
7625 },
7626
7627 getRealRowEl: function(force, idx) {
7628 var r = null,
7629 c = null;
7630
7631 idx = idx || this.idx;
7632
7633 for (var cidx = 0, cmax = this.map[idx.row].length; cidx < cmax; cidx++) {
7634 c = this.map[idx.row][cidx];
7635 if (c.isReal) {
7636 r = api.getParentElement(c.el, { nodeName: ["TR"] });
7637 if (r) {
7638 return r;
7639 }
7640 }
7641 }
7642
7643 if (r === null && force) {
7644 r = api.getParentElement(this.map[idx.row][idx.col].el, { nodeName: ["TR"] }) || null;
7645 }
7646
7647 return r;
7648 },
7649
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);
7653
7654 if (r) {
7655 var n_cidx = this.correctColIndexForUnreals(col, row);
7656 if (n_cidx >= 0) {
7657 insertAfter(this.getRowCells(r)[n_cidx], new_cells);
7658 } else {
7659 r.insertBefore(new_cells, r.firstChild);
7660 }
7661 } else {
7662 var rr = this.table.ownerDocument.createElement('tr');
7663 rr.appendChild(new_cells);
7664 insertAfter(api.getParentElement(c.el, { nodeName: ["TR"] }), rr);
7665 }
7666 },
7667
7668 canMerge: function(to) {
7669 this.to = to;
7670 this.setTableMap();
7671 this.idx_start = this.getMapIndex(this.cell);
7672 this.idx_end = this.getMapIndex(this.to);
7673
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;
7679 }
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;
7684 }
7685
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) {
7689 return false;
7690 }
7691 }
7692 }
7693 return true;
7694 },
7695
7696 decreaseCellSpan: function(cell, span) {
7697 var nr = parseInt(api.getAttribute(cell.el, span), 10) - 1;
7698 if (nr >= 1) {
7699 cell.el.setAttribute(span, nr);
7700 } else {
7701 cell.el.removeAttribute(span);
7702 if (span == 'colspan') {
7703 cell.isColspan = false;
7704 }
7705 if (span == 'rowspan') {
7706 cell.isRowspan = false;
7707 }
7708 cell.firstCol = true;
7709 cell.lastCol = true;
7710 cell.firstRow = true;
7711 cell.lastRow = true;
7712 cell.isReal = true;
7713 }
7714 },
7715
7716 removeSurplusLines: function() {
7717 var row, cell, ridx, rmax, cidx, cmax, allRowspan;
7718
7719 this.setTableMap();
7720 if (this.map) {
7721 ridx = 0;
7722 rmax = this.map.length;
7723 for (;ridx < rmax; ridx++) {
7724 row = this.map[ridx];
7725 allRowspan = true;
7726 cidx = 0;
7727 cmax = row.length;
7728 for (; cidx < cmax; cidx++) {
7729 cell = row[cidx];
7730 if (!(api.getAttribute(cell.el, "rowspan") && parseInt(api.getAttribute(cell.el, "rowspan"), 10) > 1 && cell.firstRow !== true)) {
7731 allRowspan = false;
7732 break;
7733 }
7734 }
7735 if (allRowspan) {
7736 cidx = 0;
7737 for (; cidx < cmax; cidx++) {
7738 this.decreaseCellSpan(row[cidx], 'rowspan');
7739 }
7740 }
7741 }
7742
7743 // remove rows without cells
7744 var tableRows = this.getTableRows();
7745 ridx = 0;
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))) {
7750 removeElement(row);
7751 }
7752 }
7753 }
7754 },
7755
7756 fillMissingCells: function() {
7757 var r_max = 0,
7758 c_max = 0,
7759 prevcell = null;
7760
7761 this.setTableMap();
7762 if (this.map) {
7763
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; }
7768 }
7769
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]) {
7773 if (col > 0) {
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);
7778 }
7779 }
7780 }
7781 }
7782 }
7783 }
7784 },
7785
7786 rectify: function() {
7787 if (!this.removeEmptyTable()) {
7788 this.removeSurplusLines();
7789 this.fillMissingCells();
7790 return true;
7791 } else {
7792 return false;
7793 }
7794 },
7795
7796 unmerge: function() {
7797 if (this.rectify()) {
7798 this.setTableMap();
7799 this.idx = this.getMapIndex(this.cell);
7800
7801 if (this.idx) {
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();
7805
7806 if (thisCell.isRowspan) {
7807 var rowspan = parseInt(api.getAttribute(thisCell.el, "rowspan"), 10);
7808 if (rowspan > 1) {
7809 for (var nr = 1, maxr = rowspan - 1; nr <= maxr; nr++){
7810 this.injectRowAt(this.idx.row + nr, this.idx.col, colspan, cType, thisCell);
7811 }
7812 }
7813 thisCell.el.removeAttribute('rowspan');
7814 }
7815 this.splitRowToCells(thisCell);
7816 }
7817 }
7818 },
7819
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;
7826
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++) {
7829
7830 if (row == this.idx_start.row && col == this.idx_start.col) {
7831 if (rowspan > 1) {
7832 this.map[row][col].el.setAttribute('rowspan', rowspan);
7833 }
7834 if (colspan > 1) {
7835 this.map[row][col].el.setAttribute('colspan', colspan);
7836 }
7837 } else {
7838 // transfer content
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;
7841 }
7842 removeElement(this.map[row][col].el);
7843 }
7844 }
7845 }
7846 this.rectify();
7847 } else {
7848 if (window.console) {
7849 console.log('Do not know how to merge allready merged cells.');
7850 }
7851 }
7852 }
7853 },
7854
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};
7861
7862 if (newRowIdx < this.map.length) {
7863
7864 var row = this.getRealRowEl(false, newIdx);
7865 if (row !== null) {
7866 var n_cidx = this.correctColIndexForUnreals(newIdx.col, newIdx.row);
7867 if (n_cidx >= 0) {
7868 insertAfter(this.getRowCells(row)[n_cidx], cell.el);
7869 } else {
7870 var lastCell = this.getLastNewCellOnRow(row, newRowIdx);
7871 if (lastCell !== null) {
7872 insertAfter(lastCell, cell.el);
7873 } else {
7874 row.insertBefore(cell.el, row.firstChild);
7875 }
7876 }
7877 if (parseInt(api.getAttribute(cell.el, 'rowspan'), 10) > 2) {
7878 cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) - 1);
7879 } else {
7880 cell.el.removeAttribute('rowspan');
7881 }
7882 }
7883 }
7884 },
7885
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) {
7890 if (cell.isReal) {
7891 if (cell.isRowspan) {
7892 this.collapseCellToNextRow(cell);
7893 } else {
7894 removeElement(cell.el);
7895 }
7896 } else {
7897 if (parseInt(api.getAttribute(cell.el, 'rowspan'), 10) > 2) {
7898 cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) - 1);
7899 } else {
7900 cell.el.removeAttribute('rowspan');
7901 }
7902 }
7903 },
7904
7905 getRowElementsByCell: function() {
7906 var cells = [];
7907 this.setTableMap();
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);
7914 }
7915 }
7916 }
7917 return cells;
7918 },
7919
7920 getColumnElementsByCell: function() {
7921 var cells = [];
7922 this.setTableMap();
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);
7928 }
7929 }
7930 }
7931 return cells;
7932 },
7933
7934 // Removes the row of selected cell
7935 removeRow: function() {
7936 var oldRow = api.getParentElement(this.cell, { nodeName: ["TR"] });
7937 if (oldRow) {
7938 this.setTableMap();
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]);
7946 }
7947 }
7948 }
7949 removeElement(oldRow);
7950 }
7951 },
7952
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);
7957 } else {
7958 cell.el.removeAttribute('colspan');
7959 }
7960 } else if (cell.isReal) {
7961 removeElement(cell.el);
7962 }
7963 },
7964
7965 removeColumn: function() {
7966 this.setTableMap();
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]);
7973 }
7974 }
7975 }
7976 },
7977
7978 // removes row or column by selected cell element
7979 remove: function(what) {
7980 if (this.rectify()) {
7981 switch (what) {
7982 case 'row':
7983 this.removeRow();
7984 break;
7985 case 'column':
7986 this.removeColumn();
7987 break;
7988 }
7989 this.rectify();
7990 }
7991 },
7992
7993 addRow: function(where) {
7994 var doc = this.table.ownerDocument;
7995
7996 this.setTableMap();
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;
8000 }
8001
8002 if (this.idx !== false) {
8003 var modRow = this.map[this.idx.row],
8004 newRow = doc.createElement('tr');
8005
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);
8010 }
8011 }
8012
8013 switch (where) {
8014 case 'below':
8015 insertAfter(this.getRealRowEl(true), newRow);
8016 break;
8017 case 'above':
8018 var cr = api.getParentElement(this.map[this.idx.row][this.idx.col].el, { nodeName: ["TR"] });
8019 if (cr) {
8020 cr.parentNode.insertBefore(newRow, cr);
8021 }
8022 break;
8023 }
8024 }
8025 },
8026
8027 addRowCell: function(cell, row, where) {
8028 var colSpanAttr = (cell.isColspan) ? {"colspan" : api.getAttribute(cell.el, 'colspan')} : null;
8029 if (cell.isReal) {
8030 if (where != 'above' && cell.isRowspan) {
8031 cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el,'rowspan'), 10) + 1);
8032 } else {
8033 row.appendChild(this.createCells('td', 1, colSpanAttr));
8034 }
8035 } else {
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);
8040 }
8041 }
8042 },
8043
8044 add: function(where) {
8045 if (this.rectify()) {
8046 if (where == 'below' || where == 'above') {
8047 this.addRow(where);
8048 }
8049 if (where == 'before' || where == 'after') {
8050 this.addColumn(where);
8051 }
8052 }
8053 },
8054
8055 addColCell: function (cell, ridx, where) {
8056 var doAdd,
8057 cType = cell.el.tagName.toLowerCase();
8058
8059 // defines add cell vs expand cell conditions
8060 // true means add
8061 switch (where) {
8062 case "before":
8063 doAdd = (!cell.isColspan || cell.firstCol);
8064 break;
8065 case "after":
8066 doAdd = (!cell.isColspan || cell.lastCol || (cell.isColspan && c.el == this.cell));
8067 break;
8068 }
8069
8070 if (doAdd){
8071 // adds a cell before or after current cell element
8072 switch (where) {
8073 case "before":
8074 cell.el.parentNode.insertBefore(this.createCells(cType, 1), cell.el);
8075 break;
8076 case "after":
8077 insertAfter(cell.el, this.createCells(cType, 1));
8078 break;
8079 }
8080
8081 // handles if cell has rowspan
8082 if (cell.isRowspan) {
8083 this.handleCellAddWithRowspan(cell, ridx+1, where);
8084 }
8085
8086 } else {
8087 // expands cell
8088 cell.el.setAttribute('colspan', parseInt(api.getAttribute(cell.el, 'colspan'), 10) + 1);
8089 }
8090 },
8091
8092 addColumn: function(where) {
8093 var row, modCell;
8094
8095 this.setTableMap();
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;
8099 }
8100
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);
8109 }
8110 }
8111 }
8112 }
8113 },
8114
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(),
8119 cidx, temp_r_cells,
8120 doc = this.table.ownerDocument,
8121 nrow;
8122
8123 for (var i = 0; i < addRowsNr; i++) {
8124 cidx = this.correctColIndexForUnreals(this.idx.col, (ridx + i));
8125 crow = nextNode(crow, 'tr');
8126 if (crow) {
8127 if (cidx > 0) {
8128 switch (where) {
8129 case "before":
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));
8133 } else {
8134 temp_r_cells[cidx].parentNode.insertBefore(this.createCells(cType, 1), temp_r_cells[cidx]);
8135 }
8136
8137 break;
8138 case "after":
8139 insertAfter(this.getRowCells(crow)[cidx], this.createCells(cType, 1));
8140 break;
8141 }
8142 } else {
8143 crow.insertBefore(this.createCells(cType, 1), crow.firstChild);
8144 }
8145 } else {
8146 nrow = doc.createElement('tr');
8147 nrow.appendChild(this.createCells(cType, 1));
8148 this.table.appendChild(nrow);
8149 }
8150 }
8151 }
8152 };
8153
8154 api.table = {
8155 getCellsBetween: function(cell1, cell2) {
8156 var c1 = new TableModifyerByCell(cell1);
8157 return c1.getMapElsTo(cell2);
8158 },
8159
8160 addCells: function(cell, where) {
8161 var c = new TableModifyerByCell(cell);
8162 c.add(where);
8163 },
8164
8165 removeCells: function(cell, what) {
8166 var c = new TableModifyerByCell(cell);
8167 c.remove(what);
8168 },
8169
8170 mergeCellsBetween: function(cell1, cell2) {
8171 var c1 = new TableModifyerByCell(cell1);
8172 c1.merge(cell2);
8173 },
8174
8175 unmergeCell: function(cell) {
8176 var c = new TableModifyerByCell(cell);
8177 c.unmerge();
8178 },
8179
8180 orderSelectionEnds: function(cell, cell2) {
8181 var c = new TableModifyerByCell(cell);
8182 return c.orderSelectionEnds(cell2);
8183 },
8184
8185 indexOf: function(cell) {
8186 var c = new TableModifyerByCell(cell);
8187 c.setTableMap();
8188 return c.getMapIndex(cell);
8189 },
8190
8191 findCell: function(table, idx) {
8192 var c = new TableModifyerByCell(null, table);
8193 return c.getElementAtIndex(idx);
8194 },
8195
8196 findRowByCell: function(cell) {
8197 var c = new TableModifyerByCell(cell);
8198 return c.getRowElementsByCell();
8199 },
8200
8201 findColumnByCell: function(cell) {
8202 var c = new TableModifyerByCell(cell);
8203 return c.getColumnElementsByCell();
8204 },
8205
8206 canMerge: function(cell1, cell2) {
8207 var c = new TableModifyerByCell(cell1);
8208 return c.canMerge(cell2);
8209 }
8210 };
8211
8212
8213
8214 })(wysihtml5);
8215 ;// does a selector query on element or array of elements
8216
8217 wysihtml5.dom.query = function(elements, query) {
8218 var ret = [],
8219 q;
8220
8221 if (elements.nodeType) {
8222 elements = [elements];
8223 }
8224
8225 for (var e = 0, len = elements.length; e < len; e++) {
8226 q = elements[e].querySelectorAll(query);
8227 if (q) {
8228 for(var i = q.length; i--; ret.unshift(q[i]));
8229 }
8230 }
8231 return ret;
8232 };
8233 ;wysihtml5.dom.compareDocumentPosition = (function() {
8234 var documentElement = document.documentElement;
8235 if (documentElement.compareDocumentPosition) {
8236 return function(container, element) {
8237 return container.compareDocumentPosition(element);
8238 };
8239 } else {
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;
8243
8244 if( container.nodeType === 9) // Node.DOCUMENT_NODE
8245 thisOwner = container;
8246 else
8247 thisOwner = container.ownerDocument;
8248
8249 if( element.nodeType === 9) // Node.DOCUMENT_NODE
8250 otherOwner = element;
8251 else
8252 otherOwner = element.ownerDocument;
8253
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;
8258
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;
8262
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;
8265
8266 var point = container;
8267 var parents = [ ];
8268 var previous = null;
8269 while( point ) {
8270 if( point == element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
8271 parents.push( point );
8272 point = point.parentNode;
8273 }
8274 point = element;
8275 previous = null;
8276 while( point ) {
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;
8285 }
8286 else {
8287 return 4; //Node.DOCUMENT_POSITION_FOLLOWING;
8288 }
8289 }
8290 previous = point;
8291 point = point.parentNode;
8292 }
8293 return 1; //Node.DOCUMENT_POSITION_DISCONNECTED;
8294 };
8295 }
8296 })();
8297 ;wysihtml5.dom.unwrap = function(node) {
8298 if (node.parentNode) {
8299 while (node.lastChild) {
8300 wysihtml5.dom.insert(node.lastChild).after(node);
8301 }
8302 node.parentNode.removeChild(node);
8303 }
8304 };;/*
8305 * Methods for fetching pasted html before it gets inserted into content
8306 **/
8307
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.
8311 **/
8312 wysihtml5.dom.getPastedHtml = function(event) {
8313 var html;
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);
8319 }
8320 }
8321 return html;
8322 };
8323
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');
8329
8330 doc.body.appendChild(cleanerDiv);
8331
8332 cleanerDiv.style.width = "1px";
8333 cleanerDiv.style.height = "1px";
8334 cleanerDiv.style.overflow = "hidden";
8335
8336 cleanerDiv.setAttribute('contenteditable', 'true');
8337 cleanerDiv.focus();
8338
8339 setTimeout(function () {
8340 composer.selection.setBookmark(selBookmark);
8341 f(cleanerDiv.innerHTML);
8342 cleanerDiv.parentNode.removeChild(cleanerDiv);
8343 }, 0);
8344 };;/**
8345 * Fix most common html formatting misbehaviors of browsers implementation when inserting
8346 * content via copy & paste contentEditable
8347 *
8348 * @author Christopher Blum
8349 */
8350 wysihtml5.quirks.cleanPastedHTML = (function() {
8351
8352 var styleToRegex = function (styleStr) {
8353 var trimmedStr = wysihtml5.lang.string(styleStr).trim(),
8354 escapedStr = trimmedStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
8355
8356 return new RegExp("^((?!^" + escapedStr + "$).)*$", "i");
8357 };
8358
8359 var extendRulesWithStyleExceptions = function (rules, exceptStyles) {
8360 var newRules = wysihtml5.lang.object(rules).clone(true),
8361 tag, style;
8362
8363 for (tag in newRules.tags) {
8364
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]);
8371 }
8372 }
8373 }
8374 }
8375 }
8376 }
8377
8378 return newRules;
8379 };
8380
8381 var pickRuleset = function(ruleset, html) {
8382 var pickedSet, defaultSet;
8383
8384 if (!ruleset) {
8385 return null;
8386 }
8387
8388 for (var i = 0, max = ruleset.length; i < max; i++) {
8389 if (!ruleset[i].condition) {
8390 defaultSet = ruleset[i].set;
8391 }
8392 if (ruleset[i].condition && ruleset[i].condition.test(html)) {
8393 return ruleset[i].set;
8394 }
8395 }
8396
8397 return defaultSet;
8398 };
8399
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)
8404 },
8405 rules = extendRulesWithStyleExceptions(pickRuleset(options.rules, html) || {}, exceptStyles),
8406 newHtml;
8407
8408 newHtml = wysihtml5.dom.parse(html, {
8409 "rules": rules,
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
8415 });
8416
8417 return newHtml;
8418 };
8419
8420 })();;/**
8421 * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
8422 *
8423 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
8424 * @exaple
8425 * wysihtml5.quirks.ensureProperClearing(myContentEditableElement);
8426 */
8427 wysihtml5.quirks.ensureProperClearing = (function() {
8428 var clearIfNecessary = function() {
8429 var element = this;
8430 setTimeout(function() {
8431 var innerHTML = element.innerHTML.toLowerCase();
8432 if (innerHTML == "<p>&nbsp;</p>" ||
8433 innerHTML == "<p>&nbsp;</p><p>&nbsp;</p>") {
8434 element.innerHTML = "";
8435 }
8436 }, 0);
8437 };
8438
8439 return function(composer) {
8440 wysihtml5.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);
8441 };
8442 })();
8443 ;// See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
8444 //
8445 // In Firefox this:
8446 // var d = document.createElement("div");
8447 // d.innerHTML ='<a href="~"></a>';
8448 // d.innerHTML;
8449 // will result in:
8450 // <a href="%7E"></a>
8451 // which is wrong
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) {
8457 return innerHTML;
8458 }
8459
8460 var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),
8461 url,
8462 urlToSearch,
8463 length,
8464 i;
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);
8469 }
8470 return innerHTML;
8471 };
8472 })(wysihtml5);
8473 ;/**
8474 * Force rerendering of a given element
8475 * Needed to fix display misbehaviors of IE
8476 *
8477 * @param {Element} element The element object which needs to be rerendered
8478 * @example
8479 * wysihtml5.quirks.redraw(document.body);
8480 */
8481 (function(wysihtml5) {
8482 var CLASS_NAME = "wysihtml5-quirks-redraw";
8483
8484 wysihtml5.quirks.redraw = function(element) {
8485 wysihtml5.dom.addClass(element, CLASS_NAME);
8486 wysihtml5.dom.removeClass(element, CLASS_NAME);
8487
8488 // Following hack is needed for firefox to make sure that image resize handles are properly removed
8489 try {
8490 var doc = element.ownerDocument;
8491 doc.execCommand("italic", false, null);
8492 doc.execCommand("italic", false, null);
8493 } catch(e) {}
8494 };
8495 })(wysihtml5);
8496 ;wysihtml5.quirks.tableCellsSelection = function(editable, editor) {
8497
8498 var dom = wysihtml5.dom,
8499 select = {
8500 table: null,
8501 start: null,
8502 end: null,
8503 cells: null,
8504 select: selectCells
8505 },
8506 selection_class = "wysiwyg-tmp-selected-cell",
8507 moveHandler = null,
8508 upHandler = null;
8509
8510 function init () {
8511
8512 dom.observe(editable, "mousedown", function(event) {
8513 var target = wysihtml5.dom.getParentElement(event.target, { nodeName: ["TD", "TH"] });
8514 if (target) {
8515 handleSelectionMousedown(target);
8516 }
8517 });
8518
8519 return select;
8520 }
8521
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"] });
8527
8528 if (select.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");
8534 }
8535 }
8536
8537 // remove all selection classes
8538 function removeCellSelections () {
8539 if (editable) {
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);
8544 }
8545 }
8546 }
8547 }
8548
8549 function addSelections (cells) {
8550 for (var i = 0; i < cells.length; i++) {
8551 dom.addClass(cells[i], selection_class);
8552 }
8553 }
8554
8555 function handleMouseMove (event) {
8556 var curTable = null,
8557 cell = dom.getParentElement(event.target, { nodeName: ["TD","TH"] }),
8558 oldEnd;
8559
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;
8565 select.end = cell;
8566 select.cells = dom.table.getCellsBetween(select.start, cell);
8567 if (select.cells.length > 1) {
8568 editor.composer.selection.deselect();
8569 }
8570 addSelections(select.cells);
8571 if (select.end !== oldEnd) {
8572 editor.fire("tableselectchange").fire("tableselectchange:composer");
8573 }
8574 }
8575 }
8576 }
8577
8578 function handleMouseUp (event) {
8579 moveHandler.stop();
8580 upHandler.stop();
8581 editor.fire("tableselect").fire("tableselect:composer");
8582 setTimeout(function() {
8583 bindSideclick();
8584 },0);
8585 }
8586
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;
8594 select.end = null;
8595 editor.fire("tableunselect").fire("tableunselect:composer");
8596 }
8597 });
8598 }
8599
8600 function selectCells (start, end) {
8601 select.start = start;
8602 select.end = end;
8603 select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] });
8604 selectedCells = dom.table.getCellsBetween(select.start, select.end);
8605 addSelections(selectedCells);
8606 bindSideclick();
8607 editor.fire("tableselect").fire("tableselect:composer");
8608 }
8609
8610 return init();
8611
8612 };
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;
8618
8619 var param_REGX = function (p) {
8620 return new RegExp("(^|\\s|;)" + p + "\\s*:\\s*[^;$]+" , "gi");
8621 };
8622
8623 wysihtml5.quirks.styleParser = {
8624
8625 parseColor: function(stylesStr, paramName) {
8626 var paramRegex = param_REGX(paramName),
8627 params = stylesStr.match(paramRegex),
8628 radix = 10,
8629 str, colorMatch;
8630
8631 if (params) {
8632 for (var i = params.length; i--;) {
8633 params[i] = wysihtml5.lang.string(params[i].split(':')[1]).trim();
8634 }
8635 str = params[params.length-1];
8636
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);
8643 radix = 16;
8644 } else if (HEX3_REGEX.test(str)) {
8645 colorMatch = str.match(HEX3_REGEX);
8646 colorMatch.shift();
8647 colorMatch.push(1);
8648 return wysihtml5.lang.array(colorMatch).map(function(d, idx) {
8649 return (idx < 3) ? (parseInt(d, 16) * 16) + parseInt(d, 16): parseFloat(d);
8650 });
8651 }
8652
8653 if (colorMatch) {
8654 colorMatch.shift();
8655 if (!colorMatch[3]) {
8656 colorMatch.push(1);
8657 }
8658 return wysihtml5.lang.array(colorMatch).map(function(d, idx) {
8659 return (idx < 3) ? parseInt(d, radix): parseFloat(d);
8660 });
8661 }
8662 }
8663 return false;
8664 },
8665
8666 unparseColor: function(val, props) {
8667 if (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];
8678 }
8679 }
8680
8681 if (val[3] && val[3] !== 1) {
8682 return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
8683 } else {
8684 return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
8685 }
8686 },
8687
8688 parseFontSize: function(stylesStr) {
8689 var params = stylesStr.match(param_REGX('font-size'));
8690 if (params) {
8691 return wysihtml5.lang.string(params[params.length - 1].split(':')[1]).trim();
8692 }
8693 return false;
8694 }
8695 };
8696
8697 })(wysihtml5);
8698 ;/**
8699 * Selection API
8700 *
8701 * @example
8702 * var selection = new wysihtml5.Selection(editor);
8703 */
8704 (function(wysihtml5) {
8705 var dom = wysihtml5.dom;
8706
8707 function _getCumulativeOffsetTop(element) {
8708 var top = 0;
8709 if (element.parentNode) {
8710 do {
8711 top += element.offsetTop || 0;
8712 element = element.offsetParent;
8713 } while (element);
8714 }
8715 return top;
8716 }
8717
8718 // Provides the depth of ``descendant`` relative to ``ancestor``
8719 function getDepth(ancestor, descendant) {
8720 var ret = 0;
8721 while (descendant !== ancestor) {
8722 ret++;
8723 descendant = descendant.parentNode;
8724 if (!descendant)
8725 throw new Error("not a descendant of ancestor!");
8726 }
8727 return ret;
8728 }
8729
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;
8734
8735 var common = range.commonAncestorContainer,
8736 start_depth = getDepth(common, range.startContainer),
8737 end_depth = getDepth(common, range.endContainer);
8738
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);
8744 }
8745 else {
8746 range.setEndAfter(range.endContainer);
8747 end_depth = getDepth(common, range.endContainer);
8748 }
8749 }
8750 }
8751
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();
8757
8758 this.editor = editor;
8759 this.composer = editor.composer;
8760 this.doc = this.composer.doc;
8761 this.contain = contain;
8762 this.unselectableClass = unselectableClass || false;
8763 },
8764
8765 /**
8766 * Get the current selection as a bookmark to be able to later restore it
8767 *
8768 * @return {Object} An object that represents the current selection
8769 */
8770 getBookmark: function() {
8771 var range = this.getRange();
8772 if (range) expandRangeToSurround(range);
8773 return range && range.cloneRange();
8774 },
8775
8776 /**
8777 * Restore a selection retrieved via wysihtml5.Selection.prototype.getBookmark
8778 *
8779 * @param {Object} bookmark An object that represents the current selection
8780 */
8781 setBookmark: function(bookmark) {
8782 if (!bookmark) {
8783 return;
8784 }
8785
8786 this.setSelection(bookmark);
8787 },
8788
8789 /**
8790 * Set the caret in front of the given node
8791 *
8792 * @param {Object} node The element or text node where to position the caret in front of
8793 * @example
8794 * selection.setBefore(myElement);
8795 */
8796 setBefore: function(node) {
8797 var range = rangy.createRange(this.doc);
8798 range.setStartBefore(node);
8799 range.setEndBefore(node);
8800 return this.setSelection(range);
8801 },
8802
8803 /**
8804 * Set the caret after the given node
8805 *
8806 * @param {Object} node The element or text node where to position the caret in front of
8807 * @example
8808 * selection.setBefore(myElement);
8809 */
8810 setAfter: function(node) {
8811 var range = rangy.createRange(this.doc);
8812
8813 range.setStartAfter(node);
8814 range.setEndAfter(node);
8815 return this.setSelection(range);
8816 },
8817
8818 /**
8819 * Ability to select/mark nodes
8820 *
8821 * @param {Element} node The node/element to select
8822 * @example
8823 * selection.selectNode(document.getElementById("my-image"));
8824 */
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");
8833
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) {}
8837 }
8838
8839 if (canHaveHTML) {
8840 range.selectNodeContents(node);
8841 } else {
8842 range.selectNode(node);
8843 }
8844
8845 if (canHaveHTML && isEmpty && isElement) {
8846 range.collapse(isBlockElement);
8847 } else if (canHaveHTML && isEmpty) {
8848 range.setStartAfter(node);
8849 range.setEndAfter(node);
8850 }
8851
8852 this.setSelection(range);
8853 },
8854
8855 /**
8856 * Get the node which contains the selection
8857 *
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
8860 * @example
8861 * var nodeThatContainsCaret = selection.getSelectedNode();
8862 */
8863 getSelectedNode: function(controlRange) {
8864 var selection,
8865 range;
8866
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);
8871 }
8872 }
8873
8874 selection = this.getSelection(this.doc);
8875 if (selection.focusNode === selection.anchorNode) {
8876 return selection.focusNode;
8877 } else {
8878 range = this.getRange(this.doc);
8879 return range ? range.commonAncestorContainer : this.doc.body;
8880 }
8881 },
8882
8883 fixSelBorders: function() {
8884 var range = this.getRange();
8885 expandRangeToSurround(range);
8886 this.setSelection(range);
8887 },
8888
8889 getSelectedOwnNodes: function(controlRange) {
8890 var selection,
8891 ranges = this.getOwnRanges(),
8892 ownNodes = [];
8893
8894 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
8895 ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body);
8896 }
8897 return ownNodes;
8898 },
8899
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);
8906 });
8907 nodes = nodes.concat(curNodes);
8908 }
8909 return nodes;
8910 },
8911
8912 containsUneditable: function() {
8913 var uneditables = this.getOwnUneditables(),
8914 selection = this.getSelection();
8915
8916 for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
8917 if (selection.containsNode(uneditables[i])) {
8918 return true;
8919 }
8920 }
8921
8922 return false;
8923 },
8924
8925 deleteContents: function() {
8926 var ranges = this.getOwnRanges();
8927 for (var i = ranges.length; i--;) {
8928 ranges[i].deleteContents();
8929 }
8930 this.setSelection(ranges[0]);
8931 },
8932
8933 getPreviousNode: function(node, ignoreEmpty) {
8934 if (!node) {
8935 var selection = this.getSelection();
8936 node = selection.anchorNode;
8937 }
8938
8939 if (node === this.contain) {
8940 return false;
8941 }
8942
8943 var ret = node.previousSibling,
8944 parent;
8945
8946 if (ret === this.contain) {
8947 return false;
8948 }
8949
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);
8964 }
8965 }
8966
8967 return (ret !== this.contain) ? ret : false;
8968 },
8969
8970 getSelectionParentsByTag: function(tagName) {
8971 var nodes = this.getSelectedOwnNodes(),
8972 curEl, parents = [];
8973
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);
8976 if (curEl) {
8977 parents.push(curEl);
8978 }
8979 }
8980 return (parents.length) ? parents : null;
8981 },
8982
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);
8989
8990 lastR.selectNodeContents(sNode);
8991 lastR.setStart(sNode, pos);
8992 return lastR;
8993 }
8994 },
8995
8996 caretIsLastInSelection: function() {
8997 var r = rangy.createRange(this.doc),
8998 s = this.getSelection(),
8999 endc = this.getRangeToNodeEnd().cloneContents(),
9000 endtxt = endc.textContent;
9001
9002 return (/^\s*$/).test(endtxt);
9003 },
9004
9005 caretIsFirstInSelection: function() {
9006 var r = rangy.createRange(this.doc),
9007 s = this.getSelection(),
9008 range = this.getRange(),
9009 startNode = range.startContainer;
9010
9011 if (startNode.nodeType === wysihtml5.TEXT_NODE) {
9012 return this.isCollapsed() && (startNode.nodeType === wysihtml5.TEXT_NODE && (/^\s*$/).test(startNode.data.substr(0,range.startOffset)));
9013 } else {
9014 r.selectNodeContents(this.getRange().commonAncestorContainer);
9015 r.collapse(true);
9016 return (this.isCollapsed() && (r.startContainer === s.anchorNode || r.endContainer === s.anchorNode) && r.startOffset === s.anchorOffset);
9017 }
9018 },
9019
9020 caretIsInTheBeginnig: function(ofNode) {
9021 var selection = this.getSelection(),
9022 node = selection.anchorNode,
9023 offset = selection.anchorOffset;
9024 if (ofNode) {
9025 return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml5.dom.getParentElement(node.parentNode, { nodeName: ofNode }, 1)));
9026 } else {
9027 return (offset === 0 && !this.getPreviousNode(node, true));
9028 }
9029 },
9030
9031 caretIsBeforeUneditable: function() {
9032 var selection = this.getSelection(),
9033 node = selection.anchorNode,
9034 offset = selection.anchorOffset;
9035
9036 if (offset === 0) {
9037 var prevNode = this.getPreviousNode(node, true);
9038 if (prevNode) {
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];
9043 }
9044 }
9045 }
9046 }
9047 return false;
9048 },
9049
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);
9054
9055 if (!sel) {
9056 method();
9057 } else {
9058 try {
9059 method();
9060 } catch(e) {
9061 setTimeout(function() { throw e; }, 0);
9062 }
9063 }
9064 rangy.restoreSelection(sel);
9065 },
9066
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),
9075 caretPlaceholder,
9076 newCaretPlaceholder,
9077 nextSibling, prevSibling,
9078 node, node2, range2,
9079 newRange;
9080
9081 // Nothing selected, execute and say goodbye
9082 if (!range) {
9083 method(body, body);
9084 return;
9085 }
9086
9087 if (!range.collapsed) {
9088 range2 = range.cloneRange();
9089 node2 = range2.createContextualFragment(placeholderHtml);
9090 range2.collapse(false);
9091 range2.insertNode(node2);
9092 range2.detach();
9093 }
9094
9095 node = range.createContextualFragment(placeholderHtml);
9096 range.insertNode(node);
9097
9098 if (node2) {
9099 caretPlaceholder = this.contain.querySelectorAll("." + className);
9100 range.setStartBefore(caretPlaceholder[0]);
9101 range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]);
9102 }
9103 this.setSelection(range);
9104
9105 // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
9106 try {
9107 method(range.startContainer, range.endContainer);
9108 } catch(e) {
9109 setTimeout(function() { throw e; }, 0);
9110 }
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;
9117 }
9118 if (prevSibling && nextSibling) {
9119 newRange.setStartBefore(nextSibling);
9120 newRange.setEndAfter(prevSibling);
9121 } else {
9122 newCaretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE);
9123 dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]);
9124 newRange.setStartBefore(newCaretPlaceholder);
9125 newRange.setEndAfter(newCaretPlaceholder);
9126 }
9127 this.setSelection(newRange);
9128 for (var i = caretPlaceholder.length; i--;) {
9129 caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]);
9130 }
9131
9132 } else {
9133 // fallback for when all hell breaks loose
9134 this.contain.focus();
9135 }
9136
9137 if (restoreScrollPosition) {
9138 body.scrollTop = oldScrollTop;
9139 body.scrollLeft = oldScrollLeft;
9140 }
9141
9142 // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
9143 try {
9144 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
9145 } catch(e2) {}
9146 },
9147
9148 set: function(node, offset) {
9149 var newRange = rangy.createRange(this.doc);
9150 newRange.setStart(node, offset || 0);
9151 this.setSelection(newRange);
9152 },
9153
9154 /**
9155 * Insert html at the caret position and move the cursor after the inserted html
9156 *
9157 * @param {String} html HTML string to insert
9158 * @example
9159 * selection.insertHTML("<p>foobar</p>");
9160 */
9161 insertHTML: function(html) {
9162 var range = rangy.createRange(this.doc),
9163 node = this.doc.createElement('DIV'),
9164 fragment = this.doc.createDocumentFragment(),
9165 lastChild;
9166
9167 node.innerHTML = html;
9168 lastChild = node.lastChild;
9169
9170 while (node.firstChild) {
9171 fragment.appendChild(node.firstChild);
9172 }
9173 this.insertNode(fragment);
9174
9175 if (lastChild) {
9176 this.setAfter(lastChild);
9177 }
9178 },
9179
9180 /**
9181 * Insert a node at the caret position and move the cursor behind it
9182 *
9183 * @param {Object} node HTML string to insert
9184 * @example
9185 * selection.insertNode(document.createTextNode("foobar"));
9186 */
9187 insertNode: function(node) {
9188 var range = this.getRange();
9189 if (range) {
9190 range.insertNode(node);
9191 }
9192 },
9193
9194 /**
9195 * Wraps current selection with the given node
9196 *
9197 * @param {Object} node The node to surround the selected elements with
9198 */
9199 surround: function(nodeOptions) {
9200 var ranges = this.getOwnRanges(),
9201 node, nodes = [];
9202 if (ranges.length == 0) {
9203 return nodes;
9204 }
9205
9206 for (var i = ranges.length; i--;) {
9207 node = this.doc.createElement(nodeOptions.nodeName);
9208 nodes.push(node);
9209 if (nodeOptions.className) {
9210 node.className = nodeOptions.className;
9211 }
9212 if (nodeOptions.cssStyle) {
9213 node.setAttribute('style', nodeOptions.cssStyle);
9214 }
9215 try {
9216 // This only works when the range boundaries are not overlapping other elements
9217 ranges[i].surroundContents(node);
9218 this.selectNode(node);
9219 } catch(e) {
9220 // fallback
9221 node.appendChild(ranges[i].extractContents());
9222 ranges[i].insertNode(node);
9223 }
9224 }
9225 return nodes;
9226 },
9227
9228 deblockAndSurround: function(nodeOptions) {
9229 var tempElement = this.doc.createElement('div'),
9230 range = rangy.createRange(this.doc),
9231 tempDivElements,
9232 tempElements,
9233 firstChild;
9234
9235 tempElement.className = nodeOptions.className;
9236
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]);
9241
9242 range.setStartBefore(tempDivElements[0]);
9243 range.setEndAfter(tempDivElements[tempDivElements.length - 1]);
9244 tempElements = range.extractContents();
9245
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);
9251 }
9252 if (firstChild.nodeName !== "BR") { tempElement.appendChild(this.doc.createElement('br')); }
9253 tempElements.removeChild(firstChild);
9254 } else {
9255 tempElement.appendChild(firstChild);
9256 }
9257 }
9258 } else {
9259 tempElement = null;
9260 }
9261
9262 return tempElement;
9263 },
9264
9265 /**
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
9268 *
9269 * @example
9270 * selection.scrollIntoView();
9271 */
9272 scrollIntoView: function() {
9273 var doc = this.doc,
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;
9280 return element;
9281 })(),
9282 offsetTop;
9283
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;
9290 }
9291 }
9292 },
9293
9294 /**
9295 * Select line where the caret is in
9296 */
9297 selectLine: function() {
9298 if (wysihtml5.browser.supportsSelectionModify()) {
9299 this._selectLine_W3C();
9300 } else if (this.doc.selection) {
9301 this._selectLine_MSIE();
9302 }
9303 },
9304
9305 /**
9306 * See https://developer.mozilla.org/en/DOM/Selection/modify
9307 */
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");
9313 },
9314
9315 _selectLine_MSIE: function() {
9316 var range = this.doc.selection.createRange(),
9317 rangeTop = range.boundingTop,
9318 scrollWidth = this.doc.body.scrollWidth,
9319 rangeBottom,
9320 rangeEnd,
9321 measureNode,
9322 i,
9323 j;
9324
9325 if (!range.moveToPoint) {
9326 return;
9327 }
9328
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);
9336 }
9337
9338 rangeTop += 1;
9339
9340 for (i=-10; i<scrollWidth; i+=2) {
9341 try {
9342 range.moveToPoint(i, rangeTop);
9343 break;
9344 } catch(e1) {}
9345 }
9346
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--) {
9352 try {
9353 rangeEnd.moveToPoint(j, rangeBottom);
9354 break;
9355 } catch(e2) {}
9356 }
9357
9358 range.setEndPoint("EndToEnd", rangeEnd);
9359 range.select();
9360 },
9361
9362 getText: function() {
9363 var selection = this.getSelection();
9364 return selection ? selection.toString() : "";
9365 },
9366
9367 getNodes: function(nodeType, filter) {
9368 var range = this.getRange();
9369 if (range) {
9370 return range.getNodes([nodeType], filter);
9371 } else {
9372 return [];
9373 }
9374 },
9375
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);
9382 }
9383 if (containment === 0) {
9384 range.setEndAfter(this.contain.lastChild);
9385 }
9386 if (containment === 3) {
9387 range.setStartBefore(this.contain.firstChild);
9388 range.setEndAfter(this.contain.lastChild);
9389 }
9390 } else if (this._detectInlineRangeProblems(range)) {
9391 var previousElementSibling = range.endContainer.previousElementSibling;
9392 if (previousElementSibling) {
9393 range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling));
9394 }
9395 }
9396 }
9397 },
9398
9399 _endOffsetForNode: function(node) {
9400 var range = document.createRange();
9401 range.selectNodeContents(node);
9402 return range.endOffset;
9403 },
9404
9405 _detectInlineRangeProblems: function(range) {
9406 var position = dom.compareDocumentPosition(range.startContainer, range.endContainer);
9407 return (
9408 range.endOffset == 0 &&
9409 position & 4 //Node.DOCUMENT_POSITION_FOLLOWING
9410 );
9411 },
9412
9413 getRange: function(dontFix) {
9414 var selection = this.getSelection(),
9415 range = selection && selection.rangeCount && selection.getRangeAt(0);
9416
9417 if (dontFix !== true) {
9418 this.fixRangeOverflow(range);
9419 }
9420
9421 return range;
9422 },
9423
9424 getOwnUneditables: function() {
9425 var allUneditables = dom.query(this.contain, '.' + this.unselectableClass),
9426 deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass);
9427
9428 return wysihtml5.lang.array(allUneditables).without(deepUneditables);
9429 },
9430
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() {
9435 var ranges = [],
9436 r = this.getRange(),
9437 tmpRanges;
9438
9439 if (r) { ranges.push(r); }
9440
9441 if (this.unselectableClass && this.contain && r) {
9442 var uneditables = this.getOwnUneditables(),
9443 tmpRange;
9444 if (uneditables.length > 0) {
9445 for (var i = 0, imax = uneditables.length; i < imax; i++) {
9446 tmpRanges = [];
9447 for (var j = 0, jmax = ranges.length; j < jmax; j++) {
9448 if (ranges[j]) {
9449 switch (ranges[j].compareNode(uneditables[i])) {
9450 case 2:
9451 // all selection inside uneditable. remove
9452 break;
9453 case 3:
9454 //section begins before and ends after uneditable. spilt
9455 tmpRange = ranges[j].cloneRange();
9456 tmpRange.setEndBefore(uneditables[i]);
9457 tmpRanges.push(tmpRange);
9458
9459 tmpRange = ranges[j].cloneRange();
9460 tmpRange.setStartAfter(uneditables[i]);
9461 tmpRanges.push(tmpRange);
9462 break;
9463 default:
9464 // in all other cases uneditable does not touch selection. dont modify
9465 tmpRanges.push(ranges[j]);
9466 }
9467 }
9468 ranges = tmpRanges;
9469 }
9470 }
9471 }
9472 }
9473 return ranges;
9474 },
9475
9476 getSelection: function() {
9477 return rangy.getSelection(this.doc.defaultView || this.doc.parentWindow);
9478 },
9479
9480 setSelection: function(range) {
9481 var win = this.doc.defaultView || this.doc.parentWindow,
9482 selection = rangy.getSelection(win);
9483 return selection.setSingleRange(range);
9484 },
9485
9486 createRange: function() {
9487 return rangy.createRange(this.doc);
9488 },
9489
9490 isCollapsed: function() {
9491 return this.getSelection().isCollapsed;
9492 },
9493
9494 getHtml: function() {
9495 return this.getSelection().toHtml();
9496 },
9497
9498 isEndToEndInNode: function(nodeNames) {
9499 var range = this.getRange(),
9500 parentElement = range.commonAncestorContainer,
9501 startNode = range.startContainer,
9502 endNode = range.endContainer;
9503
9504
9505 if (parentElement.nodeType === wysihtml5.TEXT_NODE) {
9506 parentElement = parentElement.parentNode;
9507 }
9508
9509 if (startNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) {
9510 return false;
9511 }
9512
9513 if (endNode.nodeType === wysihtml5.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) {
9514 return false;
9515 }
9516
9517 while (startNode && startNode !== parentElement) {
9518 if (startNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, startNode)) {
9519 return false;
9520 }
9521 if (wysihtml5.dom.domNode(startNode).prev({ignoreBlankTexts: true})) {
9522 return false;
9523 }
9524 startNode = startNode.parentNode;
9525 }
9526
9527 while (endNode && endNode !== parentElement) {
9528 if (endNode.nodeType !== wysihtml5.TEXT_NODE && !wysihtml5.dom.contains(parentElement, endNode)) {
9529 return false;
9530 }
9531 if (wysihtml5.dom.domNode(endNode).next({ignoreBlankTexts: true})) {
9532 return false;
9533 }
9534 endNode = endNode.parentNode;
9535 }
9536
9537 return (wysihtml5.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false;
9538 },
9539
9540 deselect: function() {
9541 var sel = this.getSelection();
9542 sel && sel.removeAllRanges();
9543 }
9544 });
9545
9546 })(wysihtml5);
9547 ;/**
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/
9550 *
9551 * changed in order to be able ...
9552 * - to use custom tags
9553 * - to detect and replace similar css classes via reg exp
9554 */
9555 (function(wysihtml5, rangy) {
9556 var defaultTagName = "span";
9557
9558 var REG_EXP_WHITE_SPACE = /\s+/g;
9559
9560 function hasClass(el, cssClass, regExp) {
9561 if (!el.className) {
9562 return false;
9563 }
9564
9565 var matchingClassNames = el.className.match(regExp) || [];
9566 return matchingClassNames[matchingClassNames.length - 1] === cssClass;
9567 }
9568
9569 function hasStyleAttr(el, regExp) {
9570 if (!el.getAttribute || !el.getAttribute('style')) {
9571 return false;
9572 }
9573 var matchingStyles = el.getAttribute('style').match(regExp);
9574 return (el.getAttribute('style').match(regExp)) ? true : false;
9575 }
9576
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'));
9582 } else {
9583 el.setAttribute('style', cssStyle);
9584 }
9585 } else {
9586 el.setAttribute('style', cssStyle);
9587 }
9588 }
9589
9590 function addClass(el, cssClass, regExp) {
9591 if (el.className) {
9592 removeClass(el, regExp);
9593 el.className += " " + cssClass;
9594 } else {
9595 el.className = cssClass;
9596 }
9597 }
9598
9599 function removeClass(el, regExp) {
9600 if (el.className) {
9601 el.className = el.className.replace(regExp, "");
9602 }
9603 }
9604
9605 function removeStyle(el, regExp) {
9606 var s,
9607 s2 = [];
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])) {
9612 s2.push(s[i]);
9613 }
9614 }
9615 if (s2.length) {
9616 el.setAttribute('style', s2.join(';'));
9617 } else {
9618 el.removeAttribute('style');
9619 }
9620 }
9621 }
9622
9623 function getMatchingStyleRegexp(el, style) {
9624 var regexes = [],
9625 sSplit = style.split(';'),
9626 elStyle = el.getAttribute('style');
9627
9628 if (elStyle) {
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"));
9631
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"));
9635 }
9636 }
9637 for (var j = 0, jmax = regexes.length; j < jmax; j++) {
9638 if (elStyle.match(regexes[j])) {
9639 return regexes[j];
9640 }
9641 }
9642 }
9643
9644 return false;
9645 }
9646
9647 function isMatchingAllready(node, tags, style, className) {
9648 if (style) {
9649 return getMatchingStyleRegexp(node, style);
9650 } else if (className) {
9651 return wysihtml5.dom.hasClass(node, className);
9652 } else {
9653 return rangy.dom.arrayContains(tags, node.tagName.toLowerCase());
9654 }
9655 }
9656
9657 function areMatchingAllready(nodes, tags, style, className) {
9658 for (var i = nodes.length; i--;) {
9659 if (!isMatchingAllready(nodes[i], tags, style, className)) {
9660 return false;
9661 }
9662 }
9663 return nodes.length ? true : false;
9664 }
9665
9666 function removeOrChangeStyle(el, style, regExp) {
9667
9668 var exactRegex = getMatchingStyleRegexp(el, style);
9669 if (exactRegex) {
9670 // adding same style value on property again removes style
9671 removeStyle(el, exactRegex);
9672 return "remove";
9673 } else {
9674 // adding new style value changes value
9675 addStyle(el, style, regExp);
9676 return "change";
9677 }
9678 }
9679
9680 function hasSameClasses(el1, el2) {
9681 return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " ");
9682 }
9683
9684 function replaceWithOwnChildren(el) {
9685 var parent = el.parentNode;
9686 while (el.firstChild) {
9687 parent.insertBefore(el.firstChild, el);
9688 }
9689 parent.removeChild(el);
9690 }
9691
9692 function elementsHaveSameNonClassAttributes(el1, el2) {
9693 if (el1.attributes.length != el2.attributes.length) {
9694 return false;
9695 }
9696 for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
9697 attr1 = el1.attributes[i];
9698 name = attr1.name;
9699 if (name != "class") {
9700 attr2 = el2.attributes.getNamedItem(name);
9701 if (attr1.specified != attr2.specified) {
9702 return false;
9703 }
9704 if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) {
9705 return false;
9706 }
9707 }
9708 }
9709 return true;
9710 }
9711
9712 function isSplitPoint(node, offset) {
9713 if (rangy.dom.isCharacterDataNode(node)) {
9714 if (offset == 0) {
9715 return !!node.previousSibling;
9716 } else if (offset == node.length) {
9717 return !!node.nextSibling;
9718 } else {
9719 return true;
9720 }
9721 }
9722
9723 return offset > 0 && offset < node.childNodes.length;
9724 }
9725
9726 function splitNodeAt(node, descendantNode, descendantOffset, container) {
9727 var newNode;
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;
9735 } else {
9736 newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset);
9737 }
9738 }
9739 if (!newNode) {
9740 if (!container || descendantNode !== container) {
9741
9742 newNode = descendantNode.cloneNode(false);
9743 if (newNode.id) {
9744 newNode.removeAttribute("id");
9745 }
9746 var child;
9747 while ((child = descendantNode.childNodes[descendantOffset])) {
9748 newNode.appendChild(child);
9749 }
9750 rangy.dom.insertAfter(newNode, descendantNode);
9751
9752 }
9753 }
9754 return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode), container);
9755 }
9756
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];
9761 }
9762
9763 Merge.prototype = {
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;
9770 if (i) {
9771 parent.removeChild(textNode);
9772 if (!parent.hasChildNodes()) {
9773 parent.parentNode.removeChild(parent);
9774 }
9775 }
9776 }
9777 this.firstTextNode.data = text = textBits.join("");
9778 return text;
9779 },
9780
9781 getLength: function() {
9782 var i = this.textNodes.length, len = 0;
9783 while (i--) {
9784 len += this.textNodes[i].length;
9785 }
9786 return len;
9787 },
9788
9789 toString: function() {
9790 var textBits = [];
9791 for (var i = 0, len = this.textNodes.length; i < len; ++i) {
9792 textBits[i] = "'" + this.textNodes[i].data + "'";
9793 }
9794 return "[Merge(" + textBits.join(",") + ")]";
9795 }
9796 };
9797
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;
9807 }
9808
9809 HTMLApplier.prototype = {
9810 getAncestorWithClass: function(node) {
9811 var cssClassMatch;
9812 while (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) {
9815 return node;
9816 }
9817 node = node.parentNode;
9818 }
9819 return false;
9820 },
9821
9822 // returns parents of node with given style attribute
9823 getAncestorWithStyle: function(node) {
9824 var cssStyleMatch;
9825 while (node) {
9826 cssStyleMatch = this.cssStyle ? hasStyleAttr(node, this.similarStyleRegExp) : false;
9827
9828 if (node.nodeType == wysihtml5.ELEMENT_NODE && node.getAttribute("contenteditable") != "false" && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssStyleMatch) {
9829 return node;
9830 }
9831 node = node.parentNode;
9832 }
9833 return false;
9834 },
9835
9836 getMatchingAncestor: function(node) {
9837 var ancestor = this.getAncestorWithClass(node),
9838 matchType = false;
9839
9840 if (!ancestor) {
9841 ancestor = this.getAncestorWithStyle(node);
9842 if (ancestor) {
9843 matchType = "style";
9844 }
9845 } else {
9846 if (this.cssStyle) {
9847 matchType = "class";
9848 }
9849 }
9850
9851 return {
9852 "element": ancestor,
9853 "type": matchType
9854 };
9855 },
9856
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];
9860
9861 var merges = [], currentMerge;
9862
9863 var rangeStartNode = firstNode, rangeEndNode = lastNode;
9864 var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
9865
9866 var textNode, precedingTextNode;
9867
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);
9873 }
9874 if (precedingTextNode) {
9875 if (!currentMerge) {
9876 currentMerge = new Merge(precedingTextNode);
9877 merges.push(currentMerge);
9878 }
9879 currentMerge.textNodes.push(textNode);
9880 if (textNode === firstNode) {
9881 rangeStartNode = currentMerge.firstTextNode;
9882 rangeStartOffset = rangeStartNode.length;
9883 }
9884 if (textNode === lastNode) {
9885 rangeEndNode = currentMerge.firstTextNode;
9886 rangeEndOffset = currentMerge.getLength();
9887 }
9888 } else {
9889 currentMerge = null;
9890 }
9891 }
9892 // Test whether the first node after the range needs merging
9893 if(lastNode && lastNode.parentNode) {
9894 var nextTextNode = this.getAdjacentMergeableTextNode(lastNode.parentNode, true);
9895 if (nextTextNode) {
9896 if (!currentMerge) {
9897 currentMerge = new Merge(lastNode);
9898 merges.push(currentMerge);
9899 }
9900 currentMerge.textNodes.push(nextTextNode);
9901 }
9902 }
9903 // Do the merges
9904 if (merges.length) {
9905 for (i = 0, len = merges.length; i < len; ++i) {
9906 merges[i].doMerge();
9907 }
9908 // Set the range boundaries
9909 range.setStart(rangeStartNode, rangeStartOffset);
9910 range.setEnd(rangeEndNode, rangeEndOffset);
9911 }
9912 },
9913
9914 getAdjacentMergeableTextNode: function(node, forward) {
9915 var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE);
9916 var el = isTextNode ? node.parentNode : node;
9917 var adjacentNode;
9918 var propName = forward ? "nextSibling" : "previousSibling";
9919 if (isTextNode) {
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;
9924 }
9925 } else {
9926 // Compare element with its sibling
9927 adjacentNode = el[propName];
9928 if (adjacentNode && this.areElementsMergeable(node, adjacentNode)) {
9929 return adjacentNode[forward ? "firstChild" : "lastChild"];
9930 }
9931 }
9932 return null;
9933 },
9934
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);
9940 },
9941
9942 createContainer: function(doc) {
9943 var el = doc.createElement(this.tagNames[0]);
9944 if (this.cssClass) {
9945 el.className = this.cssClass;
9946 }
9947 if (this.cssStyle) {
9948 el.setAttribute('style', this.cssStyle);
9949 }
9950 return el;
9951 },
9952
9953 applyToTextNode: function(textNode) {
9954 var parent = textNode.parentNode;
9955 if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
9956
9957 if (this.cssClass) {
9958 addClass(parent, this.cssClass, this.similarClassRegExp);
9959 }
9960 if (this.cssStyle) {
9961 addStyle(parent, this.cssStyle, this.similarStyleRegExp);
9962 }
9963 } else {
9964 var el = this.createContainer(rangy.dom.getDocument(textNode));
9965 textNode.parentNode.insertBefore(el, textNode);
9966 el.appendChild(textNode);
9967 }
9968 },
9969
9970 isRemovable: function(el) {
9971 return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) &&
9972 wysihtml5.lang.string(el.className).trim() === "" &&
9973 (
9974 !el.getAttribute('style') ||
9975 wysihtml5.lang.string(el.getAttribute('style')).trim() === ""
9976 );
9977 },
9978
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);
9987
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);
9991 }
9992 if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) {
9993 ancestor = splitNodeAt(ancestor, range.startContainer, range.startOffset, this.container);
9994 }
9995 }
9996
9997 if (!styleMode && this.similarClassRegExp) {
9998 removeClass(ancestor, this.similarClassRegExp);
9999 }
10000
10001 if (styleMode && this.similarStyleRegExp) {
10002 styleChanged = (removeOrChangeStyle(ancestor, this.cssStyle, this.similarStyleRegExp) === "change");
10003 }
10004 if (this.isRemovable(ancestor) && !styleChanged) {
10005 replaceWithOwnChildren(ancestor);
10006 }
10007 },
10008
10009 applyToRange: function(range) {
10010 var textNodes;
10011 for (var ri = range.length; ri--;) {
10012 textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10013
10014 if (!textNodes.length) {
10015 try {
10016 var node = this.createContainer(range[ri].endContainer.ownerDocument);
10017 range[ri].surroundContents(node);
10018 this.selectNode(range[ri], node);
10019 return;
10020 } catch(e) {}
10021 }
10022
10023 range[ri].splitBoundaries();
10024 textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10025 if (textNodes.length) {
10026 var textNode;
10027
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);
10032 }
10033 }
10034
10035 range[ri].setStart(textNodes[0], 0);
10036 textNode = textNodes[textNodes.length - 1];
10037 range[ri].setEnd(textNode, textNode.length);
10038
10039 if (this.normalize) {
10040 this.postApply(textNodes, range[ri]);
10041 }
10042 }
10043
10044 }
10045 },
10046
10047 undoToRange: function(range) {
10048 var textNodes, textNode, ancestorWithClass, ancestorWithStyle, ancestor;
10049 for (var ri = range.length; ri--;) {
10050
10051 textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10052 if (textNodes.length) {
10053 range[ri].splitBoundaries();
10054 textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10055 } else {
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];
10061 }
10062
10063 for (var i = 0, len = textNodes.length; i < len; ++i) {
10064 if (range[ri].isValid()) {
10065 textNode = textNodes[i];
10066
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);
10072 }
10073 }
10074 }
10075
10076 if (len == 1) {
10077 this.selectNode(range[ri], textNodes[0]);
10078 } else {
10079 range[ri].setStart(textNodes[0], 0);
10080 textNode = textNodes[textNodes.length - 1];
10081 range[ri].setEnd(textNode, textNode.length);
10082
10083 if (this.normalize) {
10084 this.postApply(textNodes, range[ri]);
10085 }
10086 }
10087
10088 }
10089 },
10090
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);
10096
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) {}
10100 }
10101 range.selectNodeContents(node);
10102 if (isEmpty && isElement) {
10103 range.collapse(false);
10104 } else if (isEmpty) {
10105 range.setStartAfter(node);
10106 range.setEndAfter(node);
10107 }
10108 },
10109
10110 getTextSelectedByRange: function(textNode, range) {
10111 var textRange = range.cloneRange();
10112 textRange.selectNodeContents(textNode);
10113
10114 var intersectionRange = textRange.intersection(range);
10115 var text = intersectionRange ? intersectionRange.toString() : "";
10116 textRange.detach();
10117
10118 return text;
10119 },
10120
10121 isAppliedToRange: function(range) {
10122 var ancestors = [],
10123 appliedType = "full",
10124 ancestor, styleAncestor, textNodes;
10125
10126 for (var ri = range.length; ri--;) {
10127
10128 textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]);
10129 if (!textNodes.length) {
10130 ancestor = this.getMatchingAncestor(range[ri].startContainer).element;
10131
10132 return (ancestor) ? {
10133 "elements": [ancestor],
10134 "coverage": appliedType
10135 } : false;
10136 }
10137
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);
10143
10144 if (wysihtml5.dom.getTextNodes(ancestor, true).length === 1) {
10145 appliedType = "full";
10146 } else if (appliedType === "full") {
10147 appliedType = "inline";
10148 }
10149 } else if (!ancestor) {
10150 appliedType = "partial";
10151 }
10152 }
10153
10154 }
10155
10156 return (ancestors.length) ? {
10157 "elements": ancestors,
10158 "coverage": appliedType
10159 } : false;
10160 },
10161
10162 toggleRange: function(range) {
10163 var isApplied = this.isAppliedToRange(range),
10164 parentsExactMatch;
10165
10166 if (isApplied) {
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);
10174 }
10175 } else {
10176 // partial
10177 if (!areMatchingAllready(isApplied.elements, this.tagNames, this.cssStyle, this.cssClass)) {
10178 this.undoToRange(range);
10179 }
10180 this.applyToRange(range);
10181 }
10182 } else {
10183 this.applyToRange(range);
10184 }
10185 }
10186 };
10187
10188 wysihtml5.selection.HTMLApplier = HTMLApplier;
10189
10190 })(wysihtml5, rangy);
10191 ;/**
10192 * Rich Text Query/Formatting Commands
10193 *
10194 * @example
10195 * var commands = new wysihtml5.Commands(editor);
10196 */
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;
10203 },
10204
10205 /**
10206 * Check whether the browser supports the given command
10207 *
10208 * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
10209 * @example
10210 * commands.supports("createLink");
10211 */
10212 support: function(command) {
10213 return wysihtml5.browser.supportsCommand(this.doc, command);
10214 },
10215
10216 /**
10217 * Check whether the browser supports the given command
10218 *
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", ...)
10221 * @example
10222 * commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
10223 */
10224 exec: function(command, value) {
10225 var obj = wysihtml5.commands[command],
10226 args = wysihtml5.lang.array(arguments).get(),
10227 method = obj && obj.exec,
10228 result = null;
10229
10230 this.editor.fire("beforecommand:composer");
10231
10232 if (method) {
10233 args.unshift(this.composer);
10234 result = method.apply(obj, args);
10235 } else {
10236 try {
10237 // try/catch for buggy firefox
10238 result = this.doc.execCommand(command, false, value);
10239 } catch(e) {}
10240 }
10241
10242 this.editor.fire("aftercommand:composer");
10243 return result;
10244 },
10245
10246 /**
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
10249 *
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
10253 * @example
10254 * var isCurrentSelectionBold = commands.state("bold");
10255 */
10256 state: function(command, commandValue) {
10257 var obj = wysihtml5.commands[command],
10258 args = wysihtml5.lang.array(arguments).get(),
10259 method = obj && obj.state;
10260 if (method) {
10261 args.unshift(this.composer);
10262 return method.apply(obj, args);
10263 } else {
10264 try {
10265 // try/catch for buggy firefox
10266 return this.doc.queryCommandState(command);
10267 } catch(e) {
10268 return false;
10269 }
10270 }
10271 },
10272
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;
10278 if (method) {
10279 args.unshift(this.composer);
10280 return method.apply(obj, args);
10281 } else {
10282 return false;
10283 }
10284 }
10285 });
10286 ;wysihtml5.commands.bold = {
10287 exec: function(composer, command) {
10288 wysihtml5.commands.formatInline.execWithToggle(composer, command, "b");
10289 },
10290
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");
10298 }
10299 };
10300
10301 ;(function(wysihtml5) {
10302 var undef,
10303 NODE_NAME = "A",
10304 dom = wysihtml5.dom;
10305
10306 function _format(composer, attributes) {
10307 var doc = composer.doc,
10308 tempClass = "_wysihtml5-temp-" + (+new Date()),
10309 tempClassRegExp = /non-matching-class/g,
10310 i = 0,
10311 length,
10312 anchors,
10313 anchor,
10314 hasElementChild,
10315 isEmpty,
10316 elementToSetCaretAfter,
10317 textContent,
10318 whiteSpace,
10319 j;
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]);
10330 }
10331 }
10332 }
10333
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;
10345 }
10346 }
10347 composer.selection.setAfter(elementToSetCaretAfter);
10348 }
10349
10350 // Changes attributes of links
10351 function _changeLinks(composer, anchors, attributes) {
10352 var oldAttrs;
10353 for (var a = anchors.length; a--;) {
10354
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);
10359 }
10360
10361 // Set new attributes
10362 for (var j in attributes) {
10363 if (attributes.hasOwnProperty(j)) {
10364 anchors[a].setAttribute(j, attributes[j]);
10365 }
10366 }
10367
10368 }
10369 }
10370
10371 wysihtml5.commands.createLink = {
10372 /**
10373 * TODO: Use HTMLApplier or formatInline here
10374 *
10375 * Turns selection into a link
10376 * If selection is already a link, it just changes the attributes
10377 *
10378 * @example
10379 * // either ...
10380 * wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de");
10381 * // ... or ...
10382 * wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" });
10383 */
10384 exec: function(composer, command, value) {
10385 var anchors = this.state(composer, command);
10386 if (anchors) {
10387 // Selection contains links then change attributes of these links
10388 composer.selection.executeAndRestore(function() {
10389 _changeLinks(composer, anchors, value);
10390 });
10391 } else {
10392 // Create links
10393 value = typeof(value) === "object" ? value : { href: value };
10394 _format(composer, value);
10395 }
10396 },
10397
10398 state: function(composer, command) {
10399 return wysihtml5.commands.formatInline.state(composer, command, "A");
10400 }
10401 };
10402 })(wysihtml5);
10403 ;(function(wysihtml5) {
10404 var dom = wysihtml5.dom;
10405
10406 function _removeFormat(composer, anchors) {
10407 var length = anchors.length,
10408 i = 0,
10409 anchor,
10410 codeElement,
10411 textContent;
10412 for (; i<length; i++) {
10413 anchor = anchors[i];
10414 codeElement = dom.getParentElement(anchor, { nodeName: "code" });
10415 textContent = dom.getTextContent(anchor);
10416
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");
10422 } else {
10423 dom.replaceWithChildNodes(anchor);
10424 }
10425 }
10426 }
10427
10428 wysihtml5.commands.removeLink = {
10429 /*
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
10432 *
10433 * @example
10434 * wysihtml5.commands.createLink.exec(composer, "removeLink");
10435 */
10436
10437 exec: function(composer, command) {
10438 var anchors = this.state(composer, command);
10439 if (anchors) {
10440 composer.selection.executeAndRestore(function() {
10441 _removeFormat(composer, anchors);
10442 });
10443 }
10444 },
10445
10446 state: function(composer, command) {
10447 return wysihtml5.commands.formatInline.state(composer, command, "A");
10448 }
10449 };
10450 })(wysihtml5);
10451 ;/**
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
10455 */
10456 (function(wysihtml5) {
10457 var REG_EXP = /wysiwyg-font-size-[0-9a-z\-]+/g;
10458
10459 wysihtml5.commands.fontSize = {
10460 exec: function(composer, command, size) {
10461 wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
10462 },
10463
10464 state: function(composer, command, size) {
10465 return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP);
10466 }
10467 };
10468 })(wysihtml5);
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;
10472
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);
10478 }
10479 },
10480
10481 state: function(composer, command, size) {
10482 return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "font-size", REG_EXP);
10483 },
10484
10485 stateValue: function(composer, command) {
10486 var st = this.state(composer, command),
10487 styleStr, fontsizeMatches,
10488 val = false;
10489
10490 if (st && wysihtml5.lang.object(st).isArray()) {
10491 st = st[0];
10492 }
10493 if (st) {
10494 styleStr = st.getAttribute('style');
10495 if (styleStr) {
10496 return wysihtml5.quirks.styleParser.parseFontSize(styleStr);
10497 }
10498 }
10499 return false;
10500 }
10501 };
10502 })(wysihtml5);
10503 ;/**
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
10507 */
10508 (function(wysihtml5) {
10509 var REG_EXP = /wysiwyg-color-[0-9a-z]+/g;
10510
10511 wysihtml5.commands.foreColor = {
10512 exec: function(composer, command, color) {
10513 wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
10514 },
10515
10516 state: function(composer, command, color) {
10517 return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP);
10518 }
10519 };
10520 })(wysihtml5);
10521 ;/**
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
10525 */
10526 (function(wysihtml5) {
10527 var REG_EXP = /(\s|^)color\s*:\s*[^;\s]+;?/gi;
10528
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"),
10532 colString;
10533
10534 if (colorVals) {
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] + ');';
10538 }
10539 wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, colString, REG_EXP);
10540 }
10541 },
10542
10543 state: function(composer, command) {
10544 return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "color", REG_EXP);
10545 },
10546
10547 stateValue: function(composer, command, props) {
10548 var st = this.state(composer, command),
10549 colorStr;
10550
10551 if (st && wysihtml5.lang.object(st).isArray()) {
10552 st = st[0];
10553 }
10554
10555 if (st) {
10556 colorStr = st.getAttribute('style');
10557 if (colorStr) {
10558 if (colorStr) {
10559 val = wysihtml5.quirks.styleParser.parseColor(colorStr, "color");
10560 return wysihtml5.quirks.styleParser.unparseColor(val, props);
10561 }
10562 }
10563 }
10564 return false;
10565 }
10566
10567 };
10568 })(wysihtml5);
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;
10572
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"),
10576 colString;
10577
10578 if (colorVals) {
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] + ');';
10582 }
10583 wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, colString, REG_EXP);
10584 }
10585 },
10586
10587 state: function(composer, command) {
10588 return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "background-color", REG_EXP);
10589 },
10590
10591 stateValue: function(composer, command, props) {
10592 var st = this.state(composer, command),
10593 colorStr,
10594 val = false;
10595
10596 if (st && wysihtml5.lang.object(st).isArray()) {
10597 st = st[0];
10598 }
10599
10600 if (st) {
10601 colorStr = st.getAttribute('style');
10602 if (colorStr) {
10603 val = wysihtml5.quirks.styleParser.parseColor(colorStr, "background-color");
10604 return wysihtml5.quirks.styleParser.unparseColor(val, props);
10605 }
10606 }
10607 return false;
10608 }
10609
10610 };
10611 })(wysihtml5);
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"];
10618
10619 /**
10620 * Remove similiar classes (based on classRegExp)
10621 * and add the desired class name
10622 */
10623 function _addClass(element, className, classRegExp) {
10624 if (element.className) {
10625 _removeClass(element, classRegExp);
10626 element.className = wysihtml5.lang.string(element.className + " " + className).trim();
10627 } else {
10628 element.className = className;
10629 }
10630 }
10631
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());
10636 } else {
10637 element.setAttribute('style', cssStyle);
10638 }
10639 }
10640
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');
10646 }
10647 return ret;
10648 }
10649
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');
10655 }
10656 return ret;
10657 }
10658
10659 function _removeLastChildIfLineBreak(node) {
10660 var lastChild = node.lastChild;
10661 if (lastChild && _isLineBreak(lastChild)) {
10662 lastChild.parentNode.removeChild(lastChild);
10663 }
10664 }
10665
10666 function _isLineBreak(node) {
10667 return node.nodeName === "BR";
10668 }
10669
10670 /**
10671 * Execute native query command
10672 * and if necessary modify the inserted node's className
10673 */
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]);
10679 if (className) {
10680 var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) {
10681 var target = event.target,
10682 displayStyle;
10683 if (target.nodeType !== wysihtml5.ELEMENT_NODE) {
10684 return;
10685 }
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;
10690 }
10691 });
10692 }
10693 doc.execCommand(command, false, nodeName);
10694
10695 if (eventListener) {
10696 eventListener.stop();
10697 }
10698 }
10699 }
10700
10701 function _selectionWrap(composer, options) {
10702 if (composer.selection.isCollapsed()) {
10703 composer.selection.selectLine();
10704 }
10705
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]);
10710 }
10711
10712 // rethink restoring selection
10713 // composer.selection.selectNode(element, wysihtml5.browser.displaysCaretInEmptyContentEditableCorrectly());
10714 }
10715
10716 function _hasClasses(element) {
10717 return !!wysihtml5.lang.string(element.className).trim();
10718 }
10719
10720 function _hasStyles(element) {
10721 return !!wysihtml5.lang.string(element.getAttribute('style') || '').trim();
10722 }
10723
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;
10732
10733 if (blockElements.length) {
10734 composer.selection.executeAndRestoreRangy(function() {
10735 for (var b = blockElements.length; b--;) {
10736 if (classRegExp) {
10737 classRemoveAction = _removeClass(blockElements[b], classRegExp);
10738 }
10739 if (styleRegExp) {
10740 styleRemoveAction = _removeStyle(blockElements[b], styleRegExp);
10741 }
10742
10743 if ((styleRemoveAction || classRemoveAction) && nodeName === null && blockElements[b].nodeName != defaultNodeName) {
10744 // dont rename or remove element when just setting block formating class or style
10745 return;
10746 }
10747
10748 var hasClasses = _hasClasses(blockElements[b]),
10749 hasStyles = _hasStyles(blockElements[b]);
10750
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]);
10756 } else {
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);
10759 }
10760 }
10761 });
10762
10763 return;
10764 }
10765
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
10773 });
10774 if (blockElement == composer.element) {
10775 blockElement = null;
10776 }
10777 if (blockElement) {
10778 // Rename current block element to new block element and add class
10779 if (nodeName) {
10780 blockElement = dom.renameElement(blockElement, nodeName);
10781 }
10782 if (className) {
10783 _addClass(blockElement, className, classRegExp);
10784 }
10785 if (cssStyle) {
10786 _addStyle(blockElement, cssStyle, styleRegExp);
10787 }
10788 blockRenameFound = true;
10789 }
10790 }
10791
10792 });
10793
10794 if (blockRenameFound) {
10795 return;
10796 }
10797 }
10798
10799 _selectionWrap(composer, {
10800 "nodeName": (nodeName || defaultNodeName),
10801 "className": className || null,
10802 "cssStyle": cssStyle || null
10803 });
10804 },
10805
10806 state: function(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp) {
10807 var nodes = composer.selection.getSelectedOwnNodes(),
10808 parents = [],
10809 parent;
10810
10811 nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName;
10812
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
10821 });
10822 if (parent && wysihtml5.lang.array(parents).indexOf(parent) == -1) {
10823 parents.push(parent);
10824 }
10825 }
10826 if (parents.length == 0) {
10827 return false;
10828 }
10829 return parents;
10830 }
10831
10832
10833 };
10834 })(wysihtml5);
10835 ;/* Formats block for as a <pre><code class="classname"></code></pre> block
10836 * Useful in conjuction for sytax highlight utility: highlight.js
10837 *
10838 * Usage:
10839 *
10840 * editorInstance.composer.commands.exec("formatCode", "language-html");
10841 */
10842
10843 wysihtml5.commands.formatCode = {
10844
10845 exec: function(composer, command, classname) {
10846 var pre = this.state(composer),
10847 code, range, selectedNodes;
10848 if (pre) {
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);
10853 if (code) {
10854 wysihtml5.dom.replaceWithChildNodes(code);
10855 }
10856 });
10857 } else {
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");
10863
10864 if (classname) {
10865 code.className = classname;
10866 }
10867
10868 pre.appendChild(code);
10869 code.appendChild(selectedNodes);
10870 range.insertNode(pre);
10871 composer.selection.selectNode(pre);
10872 }
10873 },
10874
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;
10880 } else {
10881 return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "CODE" }) && wysihtml5.dom.getParentElement(selectedNode, { nodeName: "PRE" });
10882 }
10883 }
10884 };;/**
10885 * formatInline scenarios for tag "B" (| = caret, |foo| = selected text)
10886 *
10887 * #1 caret in unformatted text:
10888 * abcdefg|
10889 * output:
10890 * abcdefg<b>|</b>
10891 *
10892 * #2 unformatted text selected:
10893 * abc|deg|h
10894 * output:
10895 * abc<b>|deg|</b>h
10896 *
10897 * #3 unformatted text selected across boundaries:
10898 * ab|c <span>defg|h</span>
10899 * output:
10900 * ab<b>|c </b><span><b>defg</b>|h</span>
10901 *
10902 * #4 formatted text entirely selected
10903 * <b>|abc|</b>
10904 * output:
10905 * |abc|
10906 *
10907 * #5 formatted text partially selected
10908 * <b>ab|c|</b>
10909 * output:
10910 * <b>ab</b>|c|
10911 *
10912 * #6 formatted text selected across boundaries
10913 * <span>ab|c</span> <b>de|fgh</b>
10914 * output:
10915 * <span>ab|c</span> de|<b>fgh</b>
10916 */
10917 (function(wysihtml5) {
10918 var // Treat <b> as <strong> and vice versa
10919 ALIAS_MAPPING = {
10920 "strong": "b",
10921 "em": "i",
10922 "b": "strong",
10923 "i": "em"
10924 },
10925 htmlApplier = {};
10926
10927 function _getTagNames(tagName) {
10928 var alias = ALIAS_MAPPING[tagName];
10929 return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()];
10930 }
10931
10932 function _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, container) {
10933 var identifier = tagName;
10934
10935 if (className) {
10936 identifier += ":" + className;
10937 }
10938 if (cssStyle) {
10939 identifier += ":" + cssStyle;
10940 }
10941
10942 if (!htmlApplier[identifier]) {
10943 htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true, cssStyle, styleRegExp, container);
10944 }
10945
10946 return htmlApplier[identifier];
10947 }
10948
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();
10953
10954 if (!ownRanges || ownRanges.length == 0) {
10955 return false;
10956 }
10957 composer.selection.getSelection().removeAllRanges();
10958
10959 _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, composer.element).toggleRange(ownRanges);
10960
10961 if (!dontRestoreSelect) {
10962 range.setStart(ownRanges[0].startContainer, ownRanges[0].startOffset);
10963 range.setEnd(
10964 ownRanges[ownRanges.length - 1].endContainer,
10965 ownRanges[ownRanges.length - 1].endOffset
10966 );
10967 composer.selection.setSelection(range);
10968 composer.selection.executeAndRestore(function() {
10969 if (!noCleanup) {
10970 composer.cleanUp();
10971 }
10972 }, true, true);
10973 } else if (!noCleanup) {
10974 composer.cleanUp();
10975 }
10976 },
10977
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) {
10982 var that = this;
10983
10984 if (this.state(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) &&
10985 composer.selection.isCollapsed() &&
10986 !composer.selection.caretIsLastInSelection() &&
10987 !composer.selection.caretIsFirstInSelection()
10988 ) {
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);
10994 });
10995 } else {
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);
10999 });
11000 } else {
11001 wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp);
11002 }
11003 }
11004 },
11005
11006 state: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) {
11007 var doc = composer.doc,
11008 aliasTagName = ALIAS_MAPPING[tagName] || tagName,
11009 ownRanges, isApplied;
11010
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)) {
11014 return false;
11015 }
11016
11017 // Check whether the document contains a node with the desired className
11018 if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) {
11019 return false;
11020 }
11021
11022 ownRanges = composer.selection.getOwnRanges();
11023
11024 if (!ownRanges || ownRanges.length === 0) {
11025 return false;
11026 }
11027
11028 isApplied = _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp, composer.element).isAppliedToRange(ownRanges);
11029
11030 return (isApplied && isApplied.elements) ? isApplied.elements : false;
11031 }
11032 };
11033 })(wysihtml5);
11034 ;(function(wysihtml5) {
11035
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;
11041
11042 composer.selection.executeAndRestore(function() {
11043 if (state) {
11044 if (composer.config.useLineBreaks) {
11045 wysihtml5.dom.lineBreaks(state).add();
11046 }
11047 wysihtml5.dom.unwrap(state);
11048 } else {
11049 if (composer.selection.isCollapsed()) {
11050 composer.selection.selectLine();
11051 }
11052
11053 if (endToEndParent) {
11054 var qouteEl = endToEndParent.ownerDocument.createElement('blockquote');
11055 wysihtml5.dom.insert(qouteEl).after(endToEndParent);
11056 qouteEl.appendChild(endToEndParent);
11057 } else {
11058 composer.selection.surround({nodeName: "blockquote"});
11059 }
11060 }
11061 });
11062 },
11063 state: function(composer, command) {
11064 var selectedNode = composer.selection.getSelectedNode(),
11065 node = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "BLOCKQUOTE" }, false, composer.element);
11066
11067 return (node) ? node : false;
11068 }
11069 };
11070
11071 })(wysihtml5);;wysihtml5.commands.insertHTML = {
11072 exec: function(composer, command, html) {
11073 if (composer.commands.support(command)) {
11074 composer.doc.execCommand(command, false, html);
11075 } else {
11076 composer.selection.insertHTML(html);
11077 }
11078 },
11079
11080 state: function() {
11081 return false;
11082 }
11083 };
11084 ;(function(wysihtml5) {
11085 var NODE_NAME = "IMG";
11086
11087 wysihtml5.commands.insertImage = {
11088 /**
11089 * Inserts an <img>
11090 * If selection is already an image link, it removes it
11091 *
11092 * @example
11093 * // either ...
11094 * wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg");
11095 * // ... or ...
11096 * wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" });
11097 */
11098 exec: function(composer, command, value) {
11099 value = typeof(value) === "object" ? value : { src: value };
11100
11101 var doc = composer.doc,
11102 image = this.state(composer),
11103 textNode,
11104 parent;
11105
11106 if (image) {
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);
11111
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);
11117 }
11118
11119 // firefox and ie sometimes don't remove the image handles, even though the image got removed
11120 wysihtml5.quirks.redraw(composer.element);
11121 return;
11122 }
11123
11124 image = doc.createElement(NODE_NAME);
11125
11126 for (var i in value) {
11127 image.setAttribute(i === "className" ? "class" : i, value[i]);
11128 }
11129
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);
11135 } else {
11136 composer.selection.setAfter(image);
11137 }
11138 },
11139
11140 state: function(composer) {
11141 var doc = composer.doc,
11142 selectedNode,
11143 text,
11144 imagesInSelection;
11145
11146 if (!wysihtml5.dom.hasElementWithTagName(doc, NODE_NAME)) {
11147 return false;
11148 }
11149
11150 selectedNode = composer.selection.getSelectedNode();
11151 if (!selectedNode) {
11152 return false;
11153 }
11154
11155 if (selectedNode.nodeName === NODE_NAME) {
11156 // This works perfectly in IE
11157 return selectedNode;
11158 }
11159
11160 if (selectedNode.nodeType !== wysihtml5.ELEMENT_NODE) {
11161 return false;
11162 }
11163
11164 text = composer.selection.getText();
11165 text = wysihtml5.lang.string(text).trim();
11166 if (text) {
11167 return false;
11168 }
11169
11170 imagesInSelection = composer.selection.getNodes(wysihtml5.ELEMENT_NODE, function(node) {
11171 return node.nodeName === "IMG";
11172 });
11173
11174 if (imagesInSelection.length !== 1) {
11175 return false;
11176 }
11177
11178 return imagesInSelection[0];
11179 }
11180 };
11181 })(wysihtml5);
11182 ;(function(wysihtml5) {
11183 var LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : "");
11184
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();
11191 }
11192 } else {
11193 composer.commands.exec("insertHTML", LINE_BREAK);
11194 }
11195 },
11196
11197 state: function() {
11198 return false;
11199 }
11200 };
11201 })(wysihtml5);
11202 ;wysihtml5.commands.insertOrderedList = {
11203 exec: function(composer, command) {
11204 wysihtml5.commands.insertList.exec(composer, command, "OL");
11205 },
11206
11207 state: function(composer, command) {
11208 return wysihtml5.commands.insertList.state(composer, command, "OL");
11209 }
11210 };
11211 ;wysihtml5.commands.insertUnorderedList = {
11212 exec: function(composer, command) {
11213 wysihtml5.commands.insertList.exec(composer, command, "UL");
11214 },
11215
11216 state: function(composer, command) {
11217 return wysihtml5.commands.insertList.state(composer, command, "UL");
11218 }
11219 };
11220 ;wysihtml5.commands.insertList = (function(wysihtml5) {
11221
11222 var isNode = function(node, name) {
11223 if (node && node.nodeName) {
11224 if (typeof name === 'string') {
11225 name = [name];
11226 }
11227 for (var n = name.length; n--;) {
11228 if (node.nodeName === name[n]) {
11229 return true;
11230 }
11231 }
11232 }
11233 return false;
11234 };
11235
11236 var findListEl = function(node, nodeName, composer) {
11237 var ret = {
11238 el: null,
11239 other: false
11240 };
11241
11242 if (node) {
11243 var parentLi = wysihtml5.dom.getParentElement(node, { nodeName: "LI" }),
11244 otherNodeName = (nodeName === "UL") ? "OL" : "UL";
11245
11246 if (isNode(node, nodeName)) {
11247 ret.el = node;
11248 } else if (isNode(node, otherNodeName)) {
11249 ret = {
11250 el: node,
11251 other: true
11252 };
11253 } else if (parentLi) {
11254 if (isNode(parentLi.parentNode, nodeName)) {
11255 ret.el = parentLi.parentNode;
11256 } else if (isNode(parentLi.parentNode, otherNodeName)) {
11257 ret = {
11258 el : parentLi.parentNode,
11259 other: true
11260 };
11261 }
11262 }
11263 }
11264
11265 // do not count list elements outside of composer
11266 if (ret.el && !composer.element.contains(ret.el)) {
11267 ret.el = null;
11268 }
11269
11270 return ret;
11271 };
11272
11273 var handleSameTypeList = function(el, nodeName, composer) {
11274 var otherNodeName = (nodeName === "UL") ? "OL" : "UL",
11275 otherLists, innerLists;
11276 // Unwrap list
11277 // <ul><li>foo</li><li>bar</li></ul>
11278 // becomes:
11279 // foo<br>bar<br>
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());
11285 }
11286 } else {
11287 innerLists = getListsInSelection(['OL', 'UL'], composer);
11288 for (var i = innerLists.length; i--;) {
11289 wysihtml5.dom.resolveList(innerLists[i], composer.config.useLineBreaks);
11290 }
11291 wysihtml5.dom.resolveList(el, composer.config.useLineBreaks);
11292 }
11293 });
11294 };
11295
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>
11300 // becomes:
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));
11305
11306 // All selection inner lists get renamed too
11307 for (var l = renameLists.length; l--;) {
11308 wysihtml5.dom.renameElement(renameLists[l], nodeName.toLowerCase());
11309 }
11310 });
11311 };
11312
11313 var getListsInSelection = function(nodeName, composer) {
11314 var ranges = composer.selection.getOwnRanges(),
11315 renameLists = [];
11316
11317 for (var r = ranges.length; r--;) {
11318 renameLists = renameLists.concat(ranges[r].getNodes([1], function(node) {
11319 return isNode(node, nodeName);
11320 }));
11321 }
11322
11323 return renameLists;
11324 };
11325
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({
11331 "nodeName": "div",
11332 "className": tempClassName
11333 }),
11334 isEmpty, list;
11335
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, "");
11339
11340 if (tempElement) {
11341 isEmpty = wysihtml5.lang.array(["", "<br>", wysihtml5.INVISIBLE_SPACE]).contains(tempElement.innerHTML);
11342 list = wysihtml5.dom.convertToList(tempElement, nodeName.toLowerCase(), composer.parent.config.uneditableContainerClassname);
11343 if (isEmpty) {
11344 composer.selection.selectNode(list.querySelector("li"), true);
11345 }
11346 }
11347 });
11348 };
11349
11350 return {
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);
11356
11357 if (!list.el) {
11358 if (composer.commands.support(cmd)) {
11359 doc.execCommand(cmd, false, null);
11360 } else {
11361 createListFallback(nodeName, composer);
11362 }
11363 } else if (list.other) {
11364 handleOtherTypeList(list.el, nodeName, composer);
11365 } else {
11366 handleSameTypeList(list.el, nodeName, composer);
11367 }
11368 },
11369
11370 state: function(composer, command, nodeName) {
11371 var selectedNode = composer.selection.getSelectedNode(),
11372 list = findListEl(selectedNode, nodeName, composer);
11373
11374 return (list.el && !list.other) ? list.el : false;
11375 }
11376 };
11377
11378 })(wysihtml5);;wysihtml5.commands.italic = {
11379 exec: function(composer, command) {
11380 wysihtml5.commands.formatInline.execWithToggle(composer, command, "i");
11381 },
11382
11383 state: function(composer, command) {
11384 // element.ownerDocument.queryCommandState("italic") results:
11385 // firefox: only <i>
11386 // chrome: <i>, <em>, <blockquote>, ...
11387 // ie: <i>, <em>
11388 // opera: only <i>
11389 return wysihtml5.commands.formatInline.state(composer, command, "i");
11390 }
11391 };
11392 ;(function(wysihtml5) {
11393 var CLASS_NAME = "wysiwyg-text-align-center",
11394 REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
11395
11396 wysihtml5.commands.justifyCenter = {
11397 exec: function(composer, command) {
11398 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11399 },
11400
11401 state: function(composer, command) {
11402 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11403 }
11404 };
11405 })(wysihtml5);
11406 ;(function(wysihtml5) {
11407 var CLASS_NAME = "wysiwyg-text-align-left",
11408 REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
11409
11410 wysihtml5.commands.justifyLeft = {
11411 exec: function(composer, command) {
11412 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11413 },
11414
11415 state: function(composer, command) {
11416 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11417 }
11418 };
11419 })(wysihtml5);
11420 ;(function(wysihtml5) {
11421 var CLASS_NAME = "wysiwyg-text-align-right",
11422 REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
11423
11424 wysihtml5.commands.justifyRight = {
11425 exec: function(composer, command) {
11426 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11427 },
11428
11429 state: function(composer, command) {
11430 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11431 }
11432 };
11433 })(wysihtml5);
11434 ;(function(wysihtml5) {
11435 var CLASS_NAME = "wysiwyg-text-align-justify",
11436 REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g;
11437
11438 wysihtml5.commands.justifyFull = {
11439 exec: function(composer, command) {
11440 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11441 },
11442
11443 state: function(composer, command) {
11444 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP);
11445 }
11446 };
11447 })(wysihtml5);
11448 ;(function(wysihtml5) {
11449 var STYLE_STR = "text-align: right;",
11450 REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
11451
11452 wysihtml5.commands.alignRightStyle = {
11453 exec: function(composer, command) {
11454 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11455 },
11456
11457 state: function(composer, command) {
11458 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11459 }
11460 };
11461 })(wysihtml5);
11462 ;(function(wysihtml5) {
11463 var STYLE_STR = "text-align: left;",
11464 REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
11465
11466 wysihtml5.commands.alignLeftStyle = {
11467 exec: function(composer, command) {
11468 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11469 },
11470
11471 state: function(composer, command) {
11472 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11473 }
11474 };
11475 })(wysihtml5);
11476 ;(function(wysihtml5) {
11477 var STYLE_STR = "text-align: center;",
11478 REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi;
11479
11480 wysihtml5.commands.alignCenterStyle = {
11481 exec: function(composer, command) {
11482 return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11483 },
11484
11485 state: function(composer, command) {
11486 return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP);
11487 }
11488 };
11489 })(wysihtml5);
11490 ;wysihtml5.commands.redo = {
11491 exec: function(composer) {
11492 return composer.undoManager.redo();
11493 },
11494
11495 state: function(composer) {
11496 return false;
11497 }
11498 };
11499 ;wysihtml5.commands.underline = {
11500 exec: function(composer, command) {
11501 wysihtml5.commands.formatInline.execWithToggle(composer, command, "u");
11502 },
11503
11504 state: function(composer, command) {
11505 return wysihtml5.commands.formatInline.state(composer, command, "u");
11506 }
11507 };
11508 ;wysihtml5.commands.undo = {
11509 exec: function(composer) {
11510 return composer.undoManager.undo();
11511 },
11512
11513 state: function(composer) {
11514 return false;
11515 }
11516 };
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 + "\">";
11523 } else {
11524 html = "<table>";
11525 }
11526 html += "<tbody>";
11527 for (row = 0; row < value.rows; row ++) {
11528 html += '<tr>';
11529 for (col = 0; col < value.cols; col ++) {
11530 html += "<td>&nbsp;</td>";
11531 }
11532 html += '</tr>';
11533 }
11534 html += "</tbody></table>";
11535 composer.commands.exec("insertHTML", html);
11536 //composer.selection.insertHTML(html);
11537 }
11538
11539
11540 },
11541
11542 state: function(composer, command) {
11543 return false;
11544 }
11545 };
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);
11551 } else {
11552 wysihtml5.dom.table.mergeCellsBetween(composer.tableSelection.start, composer.tableSelection.end);
11553 }
11554 }
11555 },
11556
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 &&
11562 ((
11563 wysihtml5.dom.getAttribute(start, "colspan") &&
11564 parseInt(wysihtml5.dom.getAttribute(start, "colspan"), 10) > 1
11565 ) || (
11566 wysihtml5.dom.getAttribute(start, "rowspan") &&
11567 parseInt(wysihtml5.dom.getAttribute(start, "rowspan"), 10) > 1
11568 ))
11569 ) {
11570 return [start];
11571 }
11572 }
11573 return false;
11574 }
11575 };
11576 ;wysihtml5.commands.addTableCells = {
11577 exec: function(composer, command, value) {
11578 if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) {
11579
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);
11586 }
11587 setTimeout(function() {
11588 composer.tableSelection.select(tableSelect.start, tableSelect.end);
11589 },0);
11590 }
11591 },
11592
11593 state: function(composer, command) {
11594 return false;
11595 }
11596 };
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),
11602 selCell,
11603 table = composer.tableSelection.table;
11604
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);
11609
11610 if (!selCell){
11611 if (value == "row") {
11612 selCell = wysihtml5.dom.table.findCell(table, {
11613 "row": idx.row - 1,
11614 "col": idx.col
11615 });
11616 }
11617
11618 if (value == "column") {
11619 selCell = wysihtml5.dom.table.findCell(table, {
11620 "row": idx.row,
11621 "col": idx.col - 1
11622 });
11623 }
11624 }
11625 if (selCell) {
11626 composer.tableSelection.select(selCell, selCell);
11627 }
11628 }, 0);
11629
11630 }
11631 },
11632
11633 state: function(composer, command) {
11634 return false;
11635 }
11636 };
11637 ;wysihtml5.commands.indentList = {
11638 exec: function(composer, command, value) {
11639 var listEls = composer.selection.getSelectionParentsByTag('LI');
11640 if (listEls) {
11641 return this.tryToPushLiLevel(listEls, composer.selection);
11642 }
11643 return false;
11644 },
11645
11646 state: function(composer, command) {
11647 return false;
11648 },
11649
11650 tryToPushLiLevel: function(liNodes, selection) {
11651 var listTag, list, prevLi, liNode, prevLiList,
11652 found = false;
11653
11654 selection.executeAndRestoreRangy(function() {
11655
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;
11662
11663 if (prevLi) {
11664 if (prevLiList) {
11665 prevLiList.appendChild(liNode);
11666 } else {
11667 list.appendChild(liNode);
11668 prevLi.appendChild(list);
11669 }
11670 found = true;
11671 }
11672 }
11673
11674 });
11675 return found;
11676 }
11677 };
11678 ;wysihtml5.commands.outdentList = {
11679 exec: function(composer, command, value) {
11680 var listEls = composer.selection.getSelectionParentsByTag('LI');
11681 if (listEls) {
11682 return this.tryToPullLiLevel(listEls, composer);
11683 }
11684 return false;
11685 },
11686
11687 state: function(composer, command) {
11688 return false;
11689 },
11690
11691 tryToPullLiLevel: function(liNodes, composer) {
11692 var listNode, outerListNode, outerLiNode, list, prevLi, liNode, afterList,
11693 found = false,
11694 that = this;
11695
11696 composer.selection.executeAndRestoreRangy(function() {
11697
11698 for (var i = liNodes.length; i--;) {
11699 liNode = liNodes[i];
11700 if (liNode.parentNode) {
11701 listNode = liNode.parentNode;
11702
11703 if (listNode.tagName === 'OL' || listNode.tagName === 'UL') {
11704 found = true;
11705
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);
11708
11709 if (outerListNode && outerLiNode) {
11710
11711 if (liNode.nextSibling) {
11712 afterList = that.getAfterList(listNode, liNode);
11713 liNode.appendChild(afterList);
11714 }
11715 outerListNode.insertBefore(liNode, outerLiNode.nextSibling);
11716
11717 } else {
11718
11719 if (liNode.nextSibling) {
11720 afterList = that.getAfterList(listNode, liNode);
11721 liNode.appendChild(afterList);
11722 }
11723
11724 for (var j = liNode.childNodes.length; j--;) {
11725 listNode.parentNode.insertBefore(liNode.childNodes[j], listNode.nextSibling);
11726 }
11727
11728 listNode.parentNode.insertBefore(document.createElement('br'), listNode.nextSibling);
11729 liNode.parentNode.removeChild(liNode);
11730
11731 }
11732
11733 // cleanup
11734 if (listNode.childNodes.length === 0) {
11735 listNode.parentNode.removeChild(listNode);
11736 }
11737 }
11738 }
11739 }
11740
11741 });
11742 return found;
11743 },
11744
11745 getAfterList: function(listNode, liNode) {
11746 var nodeName = listNode.nodeName,
11747 newList = document.createElement(nodeName);
11748
11749 while (liNode.nextSibling) {
11750 newList.appendChild(liNode.nextSibling);
11751 }
11752 return newList;
11753 }
11754
11755 };;/**
11756 * Undo Manager for wysihtml5
11757 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
11758 */
11759 (function(wysihtml5) {
11760 var Z_KEY = 90,
11761 Y_KEY = 89,
11762 BACKSPACE_KEY = 8,
11763 DELETE_KEY = 46,
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;
11770
11771 function cleanTempElements(doc) {
11772 var tempElement;
11773 while (tempElement = doc.querySelector("._wysihtml5-temp")) {
11774 tempElement.parentNode.removeChild(tempElement);
11775 }
11776 }
11777
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;
11784
11785 this.position = 0;
11786 this.historyStr = [];
11787 this.historyDom = [];
11788
11789 this.transact();
11790
11791 this._observe();
11792 },
11793
11794 _observe: function() {
11795 var that = this,
11796 doc = this.composer.sandbox.getDocument(),
11797 lastKey;
11798
11799 // Catch CTRL+Z and CTRL+Y
11800 dom.observe(this.element, "keydown", function(event) {
11801 if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
11802 return;
11803 }
11804
11805 var keyCode = event.keyCode,
11806 isUndo = keyCode === Z_KEY && !event.shiftKey,
11807 isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
11808
11809 if (isUndo) {
11810 that.undo();
11811 event.preventDefault();
11812 } else if (isRedo) {
11813 that.redo();
11814 event.preventDefault();
11815 }
11816 });
11817
11818 // Catch delete and backspace
11819 dom.observe(this.element, "keydown", function(event) {
11820 var keyCode = event.keyCode;
11821 if (keyCode === lastKey) {
11822 return;
11823 }
11824
11825 lastKey = keyCode;
11826
11827 if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
11828 that.transact();
11829 }
11830 });
11831
11832 this.editor
11833 .on("newword:composer", function() {
11834 that.transact();
11835 })
11836
11837 .on("beforecommand:composer", function() {
11838 that.transact();
11839 });
11840 },
11841
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;
11847
11848 if (currentHtml === previousHtml) {
11849 return;
11850 }
11851
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();
11856 this.position--;
11857 }
11858
11859 this.position++;
11860
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;
11866
11867 if (node.nodeType === wysihtml5.ELEMENT_NODE) {
11868 element = node;
11869 } else {
11870 element = node.parentNode;
11871 position = this.getChildNodeIndex(element, node);
11872 }
11873
11874 element.setAttribute(DATA_ATTR_OFFSET, offset);
11875 if (typeof(position) !== "undefined") {
11876 element.setAttribute(DATA_ATTR_NODE, position);
11877 }
11878 }
11879
11880 var clone = this.element.cloneNode(!!currentHtml);
11881 this.historyDom.push(clone);
11882 this.historyStr.push(currentHtml);
11883
11884 if (element) {
11885 element.removeAttribute(DATA_ATTR_OFFSET);
11886 element.removeAttribute(DATA_ATTR_NODE);
11887 }
11888
11889 },
11890
11891 undo: function() {
11892 this.transact();
11893
11894 if (!this.undoPossible()) {
11895 return;
11896 }
11897
11898 this.set(this.historyDom[--this.position - 1]);
11899 this.editor.fire("undo:composer");
11900 },
11901
11902 redo: function() {
11903 if (!this.redoPossible()) {
11904 return;
11905 }
11906
11907 this.set(this.historyDom[++this.position - 1]);
11908 this.editor.fire("redo:composer");
11909 },
11910
11911 undoPossible: function() {
11912 return this.position > 1;
11913 },
11914
11915 redoPossible: function() {
11916 return this.position < this.historyStr.length;
11917 },
11918
11919 set: function(historyEntry) {
11920 this.element.innerHTML = "";
11921
11922 var i = 0,
11923 childNodes = historyEntry.childNodes,
11924 length = historyEntry.childNodes.length;
11925
11926 for (; i<length; i++) {
11927 this.element.appendChild(childNodes[i].cloneNode(true));
11928 }
11929
11930 // Restore selection
11931 var offset,
11932 node,
11933 position;
11934
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;
11939 } else {
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);
11945 }
11946
11947 if (position !== null) {
11948 node = this.getChildNodeByIndex(node, +position);
11949 }
11950
11951 this.composer.selection.set(node, offset);
11952 },
11953
11954 getChildNodeIndex: function(parent, child) {
11955 var i = 0,
11956 childNodes = parent.childNodes,
11957 length = childNodes.length;
11958 for (; i<length; i++) {
11959 if (childNodes[i] === child) {
11960 return i;
11961 }
11962 }
11963 },
11964
11965 getChildNodeByIndex: function(parent, index) {
11966 return parent.childNodes[index];
11967 }
11968 });
11969 })(wysihtml5);
11970 ;/**
11971 * TODO: the following methods still need unit test coverage
11972 */
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();
11981 }
11982 },
11983
11984 _observeViewChange: function() {
11985 var that = this;
11986 this.parent.on("beforeload", function() {
11987 that.parent.on("change_view", function(view) {
11988 if (view === that.name) {
11989 that.parent.currentView = that;
11990 that.show();
11991 // Using tiny delay here to make sure that the placeholder is set before focusing
11992 setTimeout(function() { that.focus(); }, 0);
11993 } else {
11994 that.hide();
11995 }
11996 });
11997 });
11998 },
11999
12000 focus: function() {
12001 if (this.element.ownerDocument.querySelector(":focus") === this.element) {
12002 return;
12003 }
12004
12005 try { this.element.focus(); } catch(e) {}
12006 },
12007
12008 hide: function() {
12009 this.element.style.display = "none";
12010 },
12011
12012 show: function() {
12013 this.element.style.display = "";
12014 },
12015
12016 disable: function() {
12017 this.element.setAttribute("disabled", "disabled");
12018 },
12019
12020 enable: function() {
12021 this.element.removeAttribute("disabled");
12022 }
12023 });
12024 ;(function(wysihtml5) {
12025 var dom = wysihtml5.dom,
12026 browser = wysihtml5.browser;
12027
12028 wysihtml5.views.Composer = wysihtml5.views.View.extend(
12029 /** @scope wysihtml5.views.Composer.prototype */ {
12030 name: "composer",
12031
12032 // Needed for firefox in order to display a proper caret in an empty contentEditable
12033 CARET_HACK: "<br>",
12034
12035 constructor: function(parent, editableElement, config) {
12036 this.base(parent, editableElement, config);
12037 if (!this.config.noTextarea) {
12038 this.textarea = this.parent.textarea;
12039 } else {
12040 this.editableArea = editableElement;
12041 }
12042 if (this.config.contentEditableMode) {
12043 this._initContentEditableArea();
12044 } else {
12045 this._initSandbox();
12046 }
12047 },
12048
12049 clear: function() {
12050 this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK;
12051 },
12052
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);
12057 }
12058
12059 return value;
12060 },
12061
12062 setValue: function(html, parse) {
12063 if (parse) {
12064 html = this.parent.parse(html);
12065 }
12066
12067 try {
12068 this.element.innerHTML = html;
12069 } catch (e) {
12070 this.element.innerText = html;
12071 }
12072 },
12073
12074 cleanUp: function() {
12075 this.parent.parse(this.element);
12076 },
12077
12078 show: function() {
12079 this.editableArea.style.display = this._displayStyle || "";
12080
12081 if (!this.config.noTextarea && !this.textarea.element.disabled) {
12082 // Firefox needs this, otherwise contentEditable becomes uneditable
12083 this.disable();
12084 this.enable();
12085 }
12086 },
12087
12088 hide: function() {
12089 this._displayStyle = dom.getStyle("display").from(this.editableArea);
12090 if (this._displayStyle === "none") {
12091 this._displayStyle = null;
12092 }
12093 this.editableArea.style.display = "none";
12094 },
12095
12096 disable: function() {
12097 this.parent.fire("disable:composer");
12098 this.element.removeAttribute("contentEditable");
12099 },
12100
12101 enable: function() {
12102 this.parent.fire("enable:composer");
12103 this.element.setAttribute("contentEditable", "true");
12104 },
12105
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()) {
12111 this.clear();
12112 }
12113
12114 this.base();
12115
12116 var lastChild = this.element.lastChild;
12117 if (setToEnd && lastChild && this.selection) {
12118 if (lastChild.nodeName === "BR") {
12119 this.selection.setBefore(this.element.lastChild);
12120 } else {
12121 this.selection.setAfter(this.element.lastChild);
12122 }
12123 }
12124 },
12125
12126 getTextContent: function() {
12127 return dom.getTextContent(this.element);
12128 },
12129
12130 hasPlaceholderSet: function() {
12131 return this.getTextContent() == ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")) && this.placeholderSet;
12132 },
12133
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();
12142 },
12143
12144 _initContentEditableArea: function() {
12145 var that = this;
12146
12147 if (this.config.noTextarea) {
12148 this.sandbox = new dom.ContentEditableArea(function() {
12149 that._create();
12150 }, {}, this.editableArea);
12151 } else {
12152 this.sandbox = new dom.ContentEditableArea(function() {
12153 that._create();
12154 });
12155 this.editableArea = this.sandbox.getContentEditable();
12156 dom.insert(this.editableArea).after(this.textarea.element);
12157 this._createWysiwygFormField();
12158 }
12159 },
12160
12161 _initSandbox: function() {
12162 var that = this;
12163
12164 this.sandbox = new dom.Sandbox(function() {
12165 that._create();
12166 }, {
12167 stylesheets: this.config.stylesheets
12168 });
12169 this.editableArea = this.sandbox.getIframe();
12170
12171 var textareaElement = this.textarea.element;
12172 dom.insert(this.editableArea).after(textareaElement);
12173
12174 this._createWysiwygFormField();
12175 },
12176
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);
12185 }
12186 },
12187
12188 _create: function() {
12189 var that = this;
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);
12195 } else {
12196 this.cleanUp(); // cleans contenteditable on initiation as it may contain html
12197 }
12198
12199 // Make sure our selection handler is ready
12200 this.selection = new wysihtml5.Selection(this.parent, this.element, this.config.uneditableContainerClassname);
12201
12202 // Make sure commands dispatcher is ready
12203 this.commands = new wysihtml5.Commands(this.parent);
12204
12205 if (!this.config.noTextarea) {
12206 dom.copyAttributes([
12207 "className", "spellcheck", "title", "lang", "dir", "accessKey"
12208 ]).from(this.textarea.element).to(this.element);
12209 }
12210
12211 dom.addClass(this.element, this.config.composerClassName);
12212 //
12213 // Make the editor look like the original textarea, by syncing styles
12214 if (this.config.style && !this.config.contentEditableMode) {
12215 this.style();
12216 }
12217
12218 this.observe();
12219
12220 var name = this.config.name;
12221 if (name) {
12222 dom.addClass(this.element, name);
12223 if (!this.config.contentEditableMode) { dom.addClass(this.editableArea, name); }
12224 }
12225
12226 this.enable();
12227
12228 if (!this.config.noTextarea && this.textarea.element.disabled) {
12229 this.disable();
12230 }
12231
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);
12238 }
12239
12240 // Make sure that the browser avoids using inline styles whenever possible
12241 this.commands.exec("styleWithCSS", false);
12242
12243 this._initAutoLinking();
12244 this._initObjectResizing();
12245 this._initUndoManager();
12246 this._initLineBreaking();
12247
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);
12252 }
12253
12254 // IE sometimes leaves a single paragraph, which can't be removed by the user
12255 if (!browser.clearsContentEditableCorrectly()) {
12256 wysihtml5.quirks.ensureProperClearing(this);
12257 }
12258
12259 // Set up a sync that makes sure that textarea and editor have the same content
12260 if (this.initSync && this.config.sync) {
12261 this.initSync();
12262 }
12263
12264 // Okay hide the textarea, we are ready to go
12265 if (!this.config.noTextarea) { this.textarea.hide(); }
12266
12267 // Fire global (before-)load event
12268 this.parent.fire("beforeload").fire("load");
12269 },
12270
12271 _initAutoLinking: function() {
12272 var that = this,
12273 supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
12274 supportsAutoLinking = browser.doesAutoLinkingInContentEditable();
12275 if (supportsDisablingOfAutoLinking) {
12276 this.commands.exec("autoUrlDetect", false);
12277 }
12278
12279 if (!this.config.autoLink) {
12280 return;
12281 }
12282
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;
12291
12292 for (var i = uneditables.length; i--;) {
12293 if (wysihtml5.dom.contains(uneditables[i], endContainer)) {
12294 isInUneditable = true;
12295 }
12296 }
12297
12298 if (!isInUneditable) dom.autoLink(endContainer.parentNode, [that.config.uneditableContainerClassname]);
12299 });
12300 }
12301 });
12302
12303 dom.observe(this.element, "blur", function() {
12304 dom.autoLink(that.element, [that.config.uneditableContainerClassname]);
12305 });
12306 }
12307
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;
12320 }
12321 return textContent;
12322 };
12323
12324 dom.observe(this.element, "keydown", function(event) {
12325 if (!links.length) {
12326 return;
12327 }
12328
12329 var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
12330 link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4),
12331 textContent;
12332
12333 if (!link) {
12334 return;
12335 }
12336
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) {
12343 return;
12344 }
12345
12346 // Only set href when new href looks like a valid url
12347 if (newTextContent.match(urlRegExp)) {
12348 link.setAttribute("href", newTextContent);
12349 }
12350 }, 0);
12351 });
12352 },
12353
12354 _initObjectResizing: function() {
12355 this.commands.exec("enableObjectResizing", true);
12356
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;
12364
12365 dom.observe(element, "resizeend", function(event) {
12366 var target = event.target || event.srcElement,
12367 style = target.style,
12368 i = 0,
12369 property;
12370
12371 if (target.nodeName !== "IMG") {
12372 return;
12373 }
12374
12375 for (; i<propertiesLength; i++) {
12376 property = properties[i];
12377 if (style[property]) {
12378 target.setAttribute(property, parseInt(style[property], 10));
12379 style[property] = "";
12380 }
12381 }
12382
12383 // After resizing IE sometimes forgets to remove the old resize handles
12384 wysihtml5.quirks.redraw(element);
12385 });
12386 }
12387 },
12388
12389 _initUndoManager: function() {
12390 this.undoManager = new wysihtml5.UndoManager(this.parent);
12391 },
12392
12393 _initLineBreaking: function() {
12394 var that = this,
12395 USE_NATIVE_LINE_BREAK_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"],
12396 LIST_TAGS = ["UL", "OL", "MENU"];
12397
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");
12406 }
12407 });
12408 }
12409 }
12410
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);
12420 } else {
12421 that.selection.selectNode(paragraph, true);
12422 }
12423 }
12424 });
12425 }
12426
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);
12434 });
12435 }*/
12436
12437
12438 dom.observe(this.element, "keydown", function(event) {
12439 var keyCode = event.keyCode;
12440
12441 if (event.shiftKey) {
12442 return;
12443 }
12444
12445 if (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY) {
12446 return;
12447 }
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(),
12453 list;
12454
12455 if (blockElement.nodeName === "LI") {
12456 if (!selectedNode) {
12457 return;
12458 }
12459
12460 list = dom.getParentElement(selectedNode, { nodeName: LIST_TAGS }, 2);
12461
12462 if (!list) {
12463 adjust(selectedNode);
12464 }
12465 }
12466
12467 if (keyCode === wysihtml5.ENTER_KEY && blockElement.nodeName.match(/^H[1-6]$/)) {
12468 adjust(selectedNode);
12469 }
12470 }, 0);
12471 return;
12472 }
12473
12474 if (that.config.useLineBreaks && keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) {
12475 event.preventDefault();
12476 that.commands.exec("insertLineBreak");
12477
12478 }
12479 });
12480 }
12481 });
12482 })(wysihtml5);
12483 ;(function(wysihtml5) {
12484 var dom = wysihtml5.dom,
12485 doc = document,
12486 win = window,
12487 HOST_TEMPLATE = doc.createElement("div"),
12488 /**
12489 * Styles to copy from textarea to the composer element
12490 */
12491 TEXT_FORMATTING = [
12492 "background-color",
12493 "color", "cursor",
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"
12498 ],
12499 /**
12500 * Styles to copy from textarea to the iframe
12501 */
12502 BOX_FORMATTING = [
12503 "background-color",
12504 "border-collapse",
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",
12521 "width", "height"
12522 ],
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; }"
12533 ];
12534
12535 /**
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
12538 *
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
12542 */
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) {}
12548 } else {
12549 var elementStyle = element.style,
12550 originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
12551 originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
12552 originalStyles = {
12553 position: elementStyle.position,
12554 top: elementStyle.top,
12555 left: elementStyle.left,
12556 WebkitUserSelect: elementStyle.WebkitUserSelect
12557 };
12558
12559 dom.setStyles({
12560 position: "absolute",
12561 top: "-99999px",
12562 left: "-99999px",
12563 // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
12564 WebkitUserSelect: "none"
12565 }).on(element);
12566
12567 element.focus();
12568
12569 dom.setStyles(originalStyles).on(element);
12570
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);
12576 }
12577 }
12578 };
12579
12580
12581 wysihtml5.views.Composer.prototype.style = function() {
12582 var that = this,
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;
12590
12591 this.focusStylesHost = HOST_TEMPLATE.cloneNode(false);
12592 this.blurStylesHost = HOST_TEMPLATE.cloneNode(false);
12593 this.disabledStylesHost = HOST_TEMPLATE.cloneNode(false);
12594
12595 // Remove placeholder before copying (as the placeholder has an affect on the computed style)
12596 if (hasPlaceholder) {
12597 textareaElement.removeAttribute("placeholder");
12598 }
12599
12600 if (textareaElement === originalActiveElement) {
12601 textareaElement.blur();
12602 }
12603
12604 // enable for copying styles
12605 textareaElement.disabled = false;
12606
12607 // set textarea to display="none" to get cascaded styles via getComputedStyle
12608 textareaElement.style.display = displayValueForCopying = "none";
12609
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;
12613 }
12614
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);
12617
12618 // --------- editor styles ---------
12619 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
12620
12621 // --------- apply standard rules ---------
12622 dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
12623
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;
12629
12630 // --------- :focus styles ---------
12631 textareaElement.style.display = originalDisplayValue;
12632 focusWithoutScrolling(textareaElement);
12633 textareaElement.style.display = displayValueForCopying;
12634
12635 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
12636 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
12637
12638 // reset textarea
12639 textareaElement.style.display = originalDisplayValue;
12640
12641 dom.copyStyles(["display"]).from(textareaElement).to(this.editableArea);
12642
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"]);
12647
12648 // --------- restore focus ---------
12649 if (originalActiveElement) {
12650 originalActiveElement.focus();
12651 } else {
12652 textareaElement.blur();
12653 }
12654
12655 // --------- restore placeholder ---------
12656 if (hasPlaceholder) {
12657 textareaElement.setAttribute("placeholder", originalPlaceholder);
12658 }
12659
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);
12664 });
12665
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);
12669 });
12670
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);
12674 });
12675
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);
12679 });
12680
12681 return this;
12682 };
12683 })(wysihtml5);
12684 ;/**
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
12691 */
12692 (function(wysihtml5) {
12693 var dom = wysihtml5.dom,
12694 browser = wysihtml5.browser,
12695 /**
12696 * Map keyCodes to query commands
12697 */
12698 shortcuts = {
12699 "66": "bold", // B
12700 "73": "italic", // I
12701 "85": "underline" // U
12702 };
12703
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();
12708
12709 if (curNode.nodeType !== 1 && curNode.parentNode !== element) { curNode = curNode.parentNode; }
12710 if (prevNode) {
12711 if (curNode.nodeType == 1) {
12712 var first = curNode.firstChild;
12713
12714 if (prevNode.nodeType == 1) {
12715 while (curNode.firstChild) {
12716 prevNode.appendChild(curNode.firstChild);
12717 }
12718 } else {
12719 while (curNode.firstChild) {
12720 uneditable.parentNode.insertBefore(curNode.firstChild, uneditable);
12721 }
12722 }
12723 if (curNode.parentNode) {
12724 curNode.parentNode.removeChild(curNode);
12725 }
12726 selection.setBefore(first);
12727 } else {
12728 if (prevNode.nodeType == 1) {
12729 prevNode.appendChild(curNode);
12730 } else {
12731 uneditable.parentNode.insertBefore(curNode, uneditable);
12732 }
12733 selection.setBefore(curNode);
12734 }
12735 }
12736 };
12737
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();
12745 } else {
12746
12747 if (selection.caretIsFirstInSelection() &&
12748 selection.getPreviousNode() &&
12749 selection.getPreviousNode().nodeName &&
12750 (/^H\d$/gi).test(selection.getPreviousNode().nodeName)
12751 ) {
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);
12757 } else {
12758 var range = prevNode.ownerDocument.createRange();
12759 range.selectNodeContents(prevNode);
12760 range.collapse(false);
12761 selection.setSelection(range);
12762 }
12763 }
12764
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);
12770 }
12771 }
12772 } else {
12773 if (selection.containsUneditable()) {
12774 event.preventDefault();
12775 selection.deleteContents();
12776 }
12777 }
12778 };
12779
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;
12785 }
12786
12787 // Is &emsp; close enough to tab. Could not find enough counter arguments for now.
12788 composer.commands.exec("insertHTML", "&emsp;");
12789 };
12790
12791 wysihtml5.views.Composer.prototype.observe = function() {
12792 var that = this,
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"];
12799
12800 // --------- destroy:composer event ---------
12801 dom.observe(container, "DOMNodeRemoved", function() {
12802 clearInterval(domNodeRemovedInterval);
12803 that.parent.fire("destroy:composer");
12804 });
12805
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");
12812 }
12813 }, 250);
12814 }
12815
12816 // --------- User interaction tracking --
12817
12818 dom.observe(focusBlurElement, interactionEvents, function() {
12819 setTimeout(function() {
12820 that.parent.fire("interaction").fire("interaction:composer");
12821 }, 0);
12822 });
12823
12824
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();
12832 });
12833 } else {
12834 setTimeout(function() {
12835 that.doc.execCommand("enableObjectResizing", false, "false");
12836 that.doc.execCommand("enableInlineTableEditing", false, "false");
12837 }, 0);
12838 }
12839 }
12840 this.tableSelection = wysihtml5.quirks.tableCellsSelection(element, that.parent);
12841 }
12842
12843 // --------- Focus & blur logic ---------
12844 dom.observe(focusBlurElement, "focus", function(event) {
12845 that.parent.fire("focus", event).fire("focus:composer", event);
12846
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);
12850 });
12851
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' } });
12858 }
12859 that.parent.fire("change", changeevent).fire("change:composer", changeevent);
12860 }
12861 that.parent.fire("blur", event).fire("blur:composer", event);
12862 });
12863
12864 // --------- Drag & Drop logic ---------
12865 dom.observe(element, "dragenter", function() {
12866 that.parent.fire("unset_placeholder");
12867 });
12868
12869 dom.observe(element, pasteEvents, function(event) {
12870 that.parent.fire(event.type, event).fire(event.type + ":composer", event);
12871 });
12872
12873
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();
12881 }
12882 that.parent.fire(event.type, event).fire(event.type + ":composer", event);
12883 });
12884 }
12885
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");
12891 }
12892 });
12893
12894 this.parent.on("paste:composer", function() {
12895 setTimeout(function() { that.parent.fire("newword:composer"); }, 0);
12896 });
12897
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);
12905
12906 if (target.nodeName === "IMG" && wysihtml5.lang.array(myImages).contains(target)) {
12907 that.selection.selectNode(target);
12908 }
12909 });
12910 }
12911
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();
12917 }, 0);
12918 });
12919 }
12920
12921 if (browser.hasHistoryIssue() && browser.supportsSelectionModify()) {
12922 dom.observe(element, "keydown", function(event) {
12923 if (!event.metaKey && !event.ctrlKey) {
12924 return;
12925 }
12926
12927 var keyCode = event.keyCode,
12928 win = element.ownerDocument.defaultView,
12929 selection = win.getSelection();
12930
12931 if (keyCode === 37 || keyCode === 39) {
12932 if (keyCode === 37) {
12933 selection.modify("extend", "left", "lineboundary");
12934 if (!event.shiftKey) {
12935 selection.collapseToStart();
12936 }
12937 }
12938 if (keyCode === 39) {
12939 selection.modify("extend", "right", "lineboundary");
12940 if (!event.shiftKey) {
12941 selection.collapseToEnd();
12942 }
12943 }
12944 event.preventDefault();
12945 }
12946 });
12947 }
12948
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();
12956 }
12957 if (keyCode === 8) {
12958 // delete key
12959 handleDeleteKeyPress(event, that.selection, element, that);
12960 } else if (that.config.handleTabKey && keyCode === 9) {
12961 event.preventDefault();
12962 handleTabKeyDown(that, element);
12963 }
12964 });
12965
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,
12970 parent;
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);
12978 }
12979
12980 setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0);
12981 event.preventDefault();
12982 }
12983 });
12984
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) {
12990 that.focus();
12991 }
12992 }, 0);
12993 });
12994
12995 dom.observe(this.element, "blur", function() {
12996 setTimeout(function() {
12997 that.selection.getSelection().removeAllRanges();
12998 }, 0);
12999 });
13000 }
13001
13002 // --------- Show url in tooltip when hovering links or images ---------
13003 var titlePrefixes = {
13004 IMG: "Image: ",
13005 A: "Link: "
13006 };
13007
13008 dom.observe(element, "mouseover", function(event) {
13009 var target = event.target,
13010 nodeName = target.nodeName,
13011 title;
13012 if (nodeName !== "A" && nodeName !== "IMG") {
13013 return;
13014 }
13015 var hasTitle = target.hasAttribute("title");
13016 if(!hasTitle){
13017 title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src"));
13018 target.setAttribute("title", title);
13019 }
13020 });
13021 };
13022 })(wysihtml5);
13023 ;/**
13024 * Class that takes care that the value of the composer and the textarea is always in sync
13025 */
13026 (function(wysihtml5) {
13027 var INTERVAL = 400;
13028
13029 wysihtml5.views.Synchronizer = Base.extend(
13030 /** @scope wysihtml5.views.Synchronizer.prototype */ {
13031
13032 constructor: function(editor, textarea, composer) {
13033 this.editor = editor;
13034 this.textarea = textarea;
13035 this.composer = composer;
13036
13037 this._observe();
13038 },
13039
13040 /**
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
13044 */
13045 fromComposerToTextarea: function(shouldParseHtml) {
13046 this.textarea.setValue(wysihtml5.lang.string(this.composer.getValue(false, false)).trim(), shouldParseHtml);
13047 },
13048
13049 /**
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
13053 */
13054 fromTextareaToComposer: function(shouldParseHtml) {
13055 var textareaValue = this.textarea.getValue(false, false);
13056 if (textareaValue) {
13057 this.composer.setValue(textareaValue, shouldParseHtml);
13058 } else {
13059 this.composer.clear();
13060 this.editor.fire("set_placeholder");
13061 }
13062 },
13063
13064 /**
13065 * Invoke syncing based on view state
13066 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
13067 */
13068 sync: function(shouldParseHtml) {
13069 if (this.editor.currentView.name === "textarea") {
13070 this.fromTextareaToComposer(shouldParseHtml);
13071 } else {
13072 this.fromComposerToTextarea(shouldParseHtml);
13073 }
13074 },
13075
13076 /**
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
13080 */
13081 _observe: function() {
13082 var interval,
13083 that = this,
13084 form = this.textarea.element.form,
13085 startInterval = function() {
13086 interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
13087 },
13088 stopInterval = function() {
13089 clearInterval(interval);
13090 interval = null;
13091 };
13092
13093 startInterval();
13094
13095 if (form) {
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() {
13099 that.sync(true);
13100 });
13101 wysihtml5.dom.observe(form, "reset", function() {
13102 setTimeout(function() { that.fromTextareaToComposer(); }, 0);
13103 });
13104 }
13105
13106 this.editor.on("change_view", function(view) {
13107 if (view === "composer" && !interval) {
13108 that.fromTextareaToComposer(true);
13109 startInterval();
13110 } else if (view === "textarea") {
13111 that.fromComposerToTextarea(true);
13112 stopInterval();
13113 }
13114 });
13115
13116 this.editor.on("destroy:composer", stopInterval);
13117 }
13118 });
13119 })(wysihtml5);
13120 ;wysihtml5.views.Textarea = wysihtml5.views.View.extend(
13121 /** @scope wysihtml5.views.Textarea.prototype */ {
13122 name: "textarea",
13123
13124 constructor: function(parent, textareaElement, config) {
13125 this.base(parent, textareaElement, config);
13126
13127 this._observe();
13128 },
13129
13130 clear: function() {
13131 this.element.value = "";
13132 },
13133
13134 getValue: function(parse) {
13135 var value = this.isEmpty() ? "" : this.element.value;
13136 if (parse !== false) {
13137 value = this.parent.parse(value);
13138 }
13139 return value;
13140 },
13141
13142 setValue: function(html, parse) {
13143 if (parse) {
13144 html = this.parent.parse(html);
13145 }
13146 this.element.value = html;
13147 },
13148
13149 cleanUp: function() {
13150 var html = this.parent.parse(this.element.value);
13151 this.element.value = html;
13152 },
13153
13154 hasPlaceholderSet: function() {
13155 var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element),
13156 placeholderText = this.element.getAttribute("placeholder") || null,
13157 value = this.element.value,
13158 isEmpty = !value;
13159 return (supportsPlaceholder && isEmpty) || (value === placeholderText);
13160 },
13161
13162 isEmpty: function() {
13163 return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
13164 },
13165
13166 _observe: function() {
13167 var element = this.element,
13168 parent = this.parent,
13169 eventMapping = {
13170 focusin: "focus",
13171 focusout: "blur"
13172 },
13173 /**
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
13176 */
13177 events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
13178
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");
13183 });
13184
13185 wysihtml5.dom.observe(element, ["paste", "drop"], function() {
13186 setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
13187 });
13188 });
13189 }
13190 });
13191 ;/**
13192 * WYSIHTML5 Editor
13193 *
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
13196 *
13197 * @events
13198 * load
13199 * beforeload (for internal use only)
13200 * focus
13201 * focus:composer
13202 * focus:textarea
13203 * blur
13204 * blur:composer
13205 * blur:textarea
13206 * change
13207 * change:composer
13208 * change:textarea
13209 * paste
13210 * paste:composer
13211 * paste:textarea
13212 * newword:composer
13213 * destroy:composer
13214 * undo:composer
13215 * redo:composer
13216 * beforecommand:composer
13217 * aftercommand:composer
13218 * enable:composer
13219 * disable:composer
13220 * change_view
13221 */
13222 (function(wysihtml5) {
13223 var undef;
13224
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
13227 name: undef,
13228 // Whether the editor should look like the textarea (by adopting styles)
13229 style: true,
13230 // Id of the toolbar element, pass falsey value if you don't want any toolbar logic
13231 toolbar: undef,
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
13236 autoLink: true,
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
13255 stylesheets: [],
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
13261 cleanUp: true,
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">'
13272 };
13273
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();
13280
13281 if (this.editableElement.nodeName.toLowerCase() != "textarea") {
13282 this.config.contentEditableMode = true;
13283 this.config.noTextarea = true;
13284 }
13285 if (!this.config.noTextarea) {
13286 this.textarea = new wysihtml5.views.Textarea(this, this.editableElement, this.config);
13287 this.currentView = this.textarea;
13288 }
13289
13290 // Sort out unsupported/unwanted browsers here
13291 if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) {
13292 var that = this;
13293 setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
13294 return;
13295 }
13296
13297 // Add class name to body, to indicate that the editor is supported
13298 wysihtml5.dom.addClass(document.body, this.config.bodyClassName);
13299
13300 this.composer = new wysihtml5.views.Composer(this, this.editableElement, this.config);
13301 this.currentView = this.composer;
13302
13303 if (typeof(this.config.parser) === "function") {
13304 this._initParser();
13305 }
13306
13307 this.on("beforeload", this.handleBeforeLoad);
13308 },
13309
13310 handleBeforeLoad: function() {
13311 if (!this.config.noTextarea) {
13312 this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer);
13313 }
13314 if (this.config.toolbar) {
13315 this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar, this.config.showToolbarAfterInit);
13316 }
13317 },
13318
13319 isCompatible: function() {
13320 return this._isCompatible;
13321 },
13322
13323 clear: function() {
13324 this.currentView.clear();
13325 return this;
13326 },
13327
13328 getValue: function(parse, clearInternals) {
13329 return this.currentView.getValue(parse, clearInternals);
13330 },
13331
13332 setValue: function(html, parse) {
13333 this.fire("unset_placeholder");
13334
13335 if (!html) {
13336 return this.clear();
13337 }
13338
13339 this.currentView.setValue(html, parse);
13340 return this;
13341 },
13342
13343 cleanUp: function() {
13344 this.currentView.cleanUp();
13345 },
13346
13347 focus: function(setToEnd) {
13348 this.currentView.focus(setToEnd);
13349 return this;
13350 },
13351
13352 /**
13353 * Deactivate editor (make it readonly)
13354 */
13355 disable: function() {
13356 this.currentView.disable();
13357 return this;
13358 },
13359
13360 /**
13361 * Activate editor
13362 */
13363 enable: function() {
13364 this.currentView.enable();
13365 return this;
13366 },
13367
13368 isEmpty: function() {
13369 return this.currentView.isEmpty();
13370 },
13371
13372 hasPlaceholderSet: function() {
13373 return this.currentView.hasPlaceholderSet();
13374 },
13375
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
13384 });
13385 if (typeof(htmlOrElement) === "object") {
13386 wysihtml5.quirks.redraw(htmlOrElement);
13387 }
13388 return returnValue;
13389 },
13390
13391 /**
13392 * Prepare html parser logic
13393 * - Observes for paste and drop
13394 */
13395 _initParser: function() {
13396 var that = this,
13397 oldHtml,
13398 cleanHtml;
13399
13400 if (wysihtml5.browser.supportsModenPaste()) {
13401 this.on("paste:composer", function(event) {
13402 event.preventDefault();
13403 oldHtml = wysihtml5.dom.getPastedHtml(event);
13404 if (oldHtml) {
13405 that._cleanAndPaste(oldHtml);
13406 }
13407 });
13408
13409 } else {
13410 this.on("beforepaste:composer", function(event) {
13411 event.preventDefault();
13412 wysihtml5.dom.getPastedHtmlWithDiv(that.composer, function(pastedHTML) {
13413 if (pastedHTML) {
13414 that._cleanAndPaste(pastedHTML);
13415 }
13416 });
13417 });
13418
13419 }
13420 },
13421
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
13427 });
13428 this.composer.selection.deleteContents();
13429 this.composer.selection.insertHTML(cleanHtml);
13430 }
13431 });
13432 })(wysihtml5);
13433 ;/**
13434 * Toolbar Dialog
13435 *
13436 * @param {Element} link The toolbar link which causes the dialog to show up
13437 * @param {Element} container The dialog container
13438 *
13439 * @example
13440 * <!-- Toolbar link -->
13441 * <a data-wysihtml5-command="insertImage">insert an image</a>
13442 *
13443 * <!-- Dialog -->
13444 * <div data-wysihtml5-dialog="insertImage" style="display: none;">
13445 * <label>
13446 * URL: <input data-wysihtml5-dialog-field="src" value="http://">
13447 * </label>
13448 * <label>
13449 * Alternative text: <input data-wysihtml5-dialog-field="alt" value="">
13450 * </label>
13451 * </div>
13452 *
13453 * <script>
13454 * var dialog = new wysihtml5.toolbar.Dialog(
13455 * document.querySelector("[data-wysihtml5-command='insertImage']"),
13456 * document.querySelector("[data-wysihtml5-dialog='insertImage']")
13457 * );
13458 * dialog.observe("save", function(attributes) {
13459 * // do something
13460 * });
13461 * </script>
13462 */
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";
13469
13470
13471 wysihtml5.toolbar.Dialog = wysihtml5.lang.Dispatcher.extend(
13472 /** @scope wysihtml5.toolbar.Dialog.prototype */ {
13473 constructor: function(link, container) {
13474 this.link = link;
13475 this.container = container;
13476 },
13477
13478 _observe: function() {
13479 if (this._observed) {
13480 return;
13481 }
13482
13483 var that = this,
13484 callbackWrapper = function(event) {
13485 var attributes = that._serialize();
13486 if (attributes == that.elementToChange) {
13487 that.fire("edit", attributes);
13488 } else {
13489 that.fire("save", attributes);
13490 }
13491 that.hide();
13492 event.preventDefault();
13493 event.stopPropagation();
13494 };
13495
13496 dom.observe(that.link, "click", function() {
13497 if (dom.hasClass(that.link, CLASS_NAME_OPENED)) {
13498 setTimeout(function() { that.hide(); }, 0);
13499 }
13500 });
13501
13502 dom.observe(this.container, "keydown", function(event) {
13503 var keyCode = event.keyCode;
13504 if (keyCode === wysihtml5.ENTER_KEY) {
13505 callbackWrapper(event);
13506 }
13507 if (keyCode === wysihtml5.ESCAPE_KEY) {
13508 that.fire("cancel");
13509 that.hide();
13510 }
13511 });
13512
13513 dom.delegate(this.container, "[data-wysihtml5-dialog-action=save]", "click", callbackWrapper);
13514
13515 dom.delegate(this.container, "[data-wysihtml5-dialog-action=cancel]", "click", function(event) {
13516 that.fire("cancel");
13517 that.hide();
13518 event.preventDefault();
13519 event.stopPropagation();
13520 });
13521
13522 var formElements = this.container.querySelectorAll(SELECTOR_FORM_ELEMENTS),
13523 i = 0,
13524 length = formElements.length,
13525 _clearInterval = function() { clearInterval(that.interval); };
13526 for (; i<length; i++) {
13527 dom.observe(formElements[i], "change", _clearInterval);
13528 }
13529
13530 this._observed = true;
13531 },
13532
13533 /**
13534 * Grabs all fields in the dialog and puts them in key=>value style in an object which
13535 * then gets returned
13536 */
13537 _serialize: function() {
13538 var data = this.elementToChange || {},
13539 fields = this.container.querySelectorAll(SELECTOR_FIELDS),
13540 length = fields.length,
13541 i = 0;
13542
13543 for (; i<length; i++) {
13544 data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
13545 }
13546 return data;
13547 },
13548
13549 /**
13550 * Takes the attributes of the "elementToChange"
13551 * and inserts them in their corresponding dialog input fields
13552 *
13553 * Assume the "elementToChange" looks like this:
13554 * <a href="http://www.google.com" target="_blank">foo</a>
13555 *
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="">
13559 *
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">
13563 *
13564 * Basically it adopted the attribute values into the corresponding input fields
13565 *
13566 */
13567 _interpolate: function(avoidHiddenFields) {
13568 var field,
13569 fieldName,
13570 newValue,
13571 focusedElement = document.querySelector(":focus"),
13572 fields = this.container.querySelectorAll(SELECTOR_FIELDS),
13573 length = fields.length,
13574 i = 0;
13575 for (; i<length; i++) {
13576 field = fields[i];
13577
13578 // Never change elements where the user is currently typing in
13579 if (field === focusedElement) {
13580 continue;
13581 }
13582
13583 // Don't update hidden fields
13584 // See https://github.com/xing/wysihtml5/pull/14
13585 if (avoidHiddenFields && field.type === "hidden") {
13586 continue;
13587 }
13588
13589 fieldName = field.getAttribute(ATTRIBUTE_FIELDS);
13590 newValue = (this.elementToChange && typeof(this.elementToChange) !== 'boolean') ? (this.elementToChange.getAttribute(fieldName) || "") : field.defaultValue;
13591 field.value = newValue;
13592 }
13593 },
13594
13595 /**
13596 * Show the dialog element
13597 */
13598 show: function(elementToChange) {
13599 if (dom.hasClass(this.link, CLASS_NAME_OPENED)) {
13600 return;
13601 }
13602
13603 var that = this,
13604 firstField = this.container.querySelector(SELECTOR_FORM_ELEMENTS);
13605 this.elementToChange = elementToChange;
13606 this._observe();
13607 this._interpolate();
13608 if (elementToChange) {
13609 this.interval = setInterval(function() { that._interpolate(true); }, 500);
13610 }
13611 dom.addClass(this.link, CLASS_NAME_OPENED);
13612 this.container.style.display = "";
13613 this.fire("show");
13614 if (firstField && !elementToChange) {
13615 try {
13616 firstField.focus();
13617 } catch(e) {}
13618 }
13619 },
13620
13621 /**
13622 * Hide the dialog element
13623 */
13624 hide: function() {
13625 clearInterval(this.interval);
13626 this.elementToChange = null;
13627 dom.removeClass(this.link, CLASS_NAME_OPENED);
13628 this.container.style.display = "none";
13629 this.fire("hide");
13630 }
13631 });
13632 })(wysihtml5);
13633 ;/**
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
13636 *
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
13639 *
13640 * Current HTML5 draft can be found here
13641 * http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html
13642 *
13643 * "Accessing Google Speech API Chrome 11"
13644 * http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
13645 */
13646 (function(wysihtml5) {
13647 var dom = wysihtml5.dom;
13648
13649 var linkStyles = {
13650 position: "relative"
13651 };
13652
13653 var wrapperStyles = {
13654 left: 0,
13655 margin: 0,
13656 opacity: 0,
13657 overflow: "hidden",
13658 padding: 0,
13659 position: "absolute",
13660 top: 0,
13661 zIndex: 1
13662 };
13663
13664 var inputStyles = {
13665 cursor: "inherit",
13666 fontSize: "50px",
13667 height: "50px",
13668 marginTop: "-25px",
13669 outline: 0,
13670 padding: 0,
13671 position: "absolute",
13672 right: "-4px",
13673 top: "50%"
13674 };
13675
13676 var inputAttributes = {
13677 "x-webkit-speech": "",
13678 "speech": ""
13679 };
13680
13681 wysihtml5.toolbar.Speech = function(parent, link) {
13682 var input = document.createElement("input");
13683 if (!wysihtml5.browser.supportsSpeechApiOn(input)) {
13684 link.style.display = "none";
13685 return;
13686 }
13687 var lang = parent.editor.textarea.element.getAttribute("lang");
13688 if (lang) {
13689 inputAttributes.lang = lang;
13690 }
13691
13692 var wrapper = document.createElement("div");
13693
13694 wysihtml5.lang.object(wrapperStyles).merge({
13695 width: link.offsetWidth + "px",
13696 height: link.offsetHeight + "px"
13697 });
13698
13699 dom.insert(input).into(wrapper);
13700 dom.insert(wrapper).into(link);
13701
13702 dom.setStyles(inputStyles).on(input);
13703 dom.setAttributes(inputAttributes).on(input);
13704
13705 dom.setStyles(wrapperStyles).on(wrapper);
13706 dom.setStyles(linkStyles).on(link);
13707
13708 var eventName = "onwebkitspeechchange" in input ? "webkitspeechchange" : "speechchange";
13709 dom.observe(input, eventName, function() {
13710 parent.execCommand("insertText", input.value);
13711 input.value = "";
13712 });
13713
13714 dom.observe(input, "click", function(event) {
13715 if (dom.hasClass(link, "wysihtml5-command-disabled")) {
13716 event.preventDefault();
13717 }
13718
13719 event.stopPropagation();
13720 });
13721 };
13722 })(wysihtml5);
13723 ;/**
13724 * Toolbar
13725 *
13726 * @param {Object} parent Reference to instance of Editor instance
13727 * @param {Element} container Reference to the toolbar container element
13728 *
13729 * @example
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>
13733 * </div>
13734 *
13735 * <script>
13736 * var toolbar = new wysihtml5.toolbar.Toolbar(editor, document.getElementById("toolbar"));
13737 * </script>
13738 */
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;
13745
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;
13752
13753 this._getLinks("command");
13754 this._getLinks("action");
13755
13756 this._observe();
13757 if (showOnInit) { this.show(); }
13758
13759 if (editor.config.classNameCommandDisabled != null) {
13760 CLASS_NAME_COMMAND_DISABLED = editor.config.classNameCommandDisabled;
13761 }
13762 if (editor.config.classNameCommandsDisabled != null) {
13763 CLASS_NAME_COMMANDS_DISABLED = editor.config.classNameCommandsDisabled;
13764 }
13765 if (editor.config.classNameCommandActive != null) {
13766 CLASS_NAME_COMMAND_ACTIVE = editor.config.classNameCommandActive;
13767 }
13768 if (editor.config.classNameActionActive != null) {
13769 CLASS_NAME_ACTION_ACTIVE = editor.config.classNameActionActive;
13770 }
13771
13772 var speechInputLinks = this.container.querySelectorAll("[data-wysihtml5-command=insertSpeech]"),
13773 length = speechInputLinks.length,
13774 i = 0;
13775 for (; i<length; i++) {
13776 new wysihtml5.toolbar.Speech(this, speechInputLinks[i]);
13777 }
13778 },
13779
13780 _getLinks: function(type) {
13781 var links = this[type + "Links"] = wysihtml5.lang.array(this.container.querySelectorAll("[data-wysihtml5-" + type + "]")).get(),
13782 length = links.length,
13783 i = 0,
13784 mapping = this[type + "Mapping"] = {},
13785 link,
13786 group,
13787 name,
13788 value,
13789 dialog;
13790 for (; i<length; i++) {
13791 link = links[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);
13796
13797 mapping[name + ":" + value] = {
13798 link: link,
13799 group: group,
13800 name: name,
13801 value: value,
13802 dialog: dialog,
13803 state: false
13804 };
13805 }
13806 },
13807
13808 _getDialog: function(link, command) {
13809 var that = this,
13810 dialogElement = this.container.querySelector("[data-wysihtml5-dialog='" + command + "']"),
13811 dialog,
13812 caretBookmark;
13813
13814 if (dialogElement) {
13815 if (wysihtml5.toolbar["Dialog_" + command]) {
13816 dialog = new wysihtml5.toolbar["Dialog_" + command](link, dialogElement);
13817 } else {
13818 dialog = new wysihtml5.toolbar.Dialog(link, dialogElement);
13819 }
13820
13821 dialog.on("show", function() {
13822 caretBookmark = that.composer.selection.getBookmark();
13823
13824 that.editor.fire("show:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
13825 });
13826
13827 dialog.on("save", function(attributes) {
13828 if (caretBookmark) {
13829 that.composer.selection.setBookmark(caretBookmark);
13830 }
13831 that._execCommand(command, attributes);
13832
13833 that.editor.fire("save:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
13834 });
13835
13836 dialog.on("cancel", function() {
13837 that.editor.focus(false);
13838 that.editor.fire("cancel:dialog", { command: command, dialogContainer: dialogElement, commandLink: link });
13839 });
13840 }
13841 return dialog;
13842 },
13843
13844 /**
13845 * @example
13846 * var toolbar = new wysihtml5.Toolbar();
13847 * // Insert a <blockquote> element or wrap current selection in <blockquote>
13848 * toolbar.execCommand("formatBlock", "blockquote");
13849 */
13850 execCommand: function(command, commandValue) {
13851 if (this.commandsDisabled) {
13852 return;
13853 }
13854
13855 var commandObj = this.commandMapping[command + ":" + commandValue];
13856
13857 // Show dialog when available
13858 if (commandObj && commandObj.dialog && !commandObj.state) {
13859 commandObj.dialog.show();
13860 } else {
13861 this._execCommand(command, commandValue);
13862 }
13863 },
13864
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);
13868
13869 this.composer.commands.exec(command, commandValue);
13870 this._updateLinkStates();
13871 },
13872
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");
13879 } else {
13880 editor.fire("change_view", "textarea");
13881 }
13882 }
13883 }
13884 if (action == "showSource") {
13885 editor.fire("showSource");
13886 }
13887 },
13888
13889 _observe: function() {
13890 var that = this,
13891 editor = this.editor,
13892 container = this.container,
13893 links = this.commandLinks.concat(this.actionLinks),
13894 length = links.length,
13895 i = 0;
13896
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:;",
13903 unselectable: "on"
13904 }).on(links[i]);
13905 } else {
13906 dom.setAttributes({ unselectable: "on" }).on(links[i]);
13907 }
13908 }
13909
13910 // Needed for opera and chrome
13911 dom.delegate(container, "[data-wysihtml5-command], [data-wysihtml5-action]", "mousedown", function(event) { event.preventDefault(); });
13912
13913 dom.delegate(container, "[data-wysihtml5-command]", "click", function(event) {
13914 var link = this,
13915 command = link.getAttribute("data-wysihtml5-command"),
13916 commandValue = link.getAttribute("data-wysihtml5-command-value");
13917 that.execCommand(command, commandValue);
13918 event.preventDefault();
13919 });
13920
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();
13925 });
13926
13927 editor.on("interaction:composer", function() {
13928 that._updateLinkStates();
13929 });
13930
13931 editor.on("focus:composer", function() {
13932 that.bookmark = null;
13933 });
13934
13935 if (this.editor.config.handleTables) {
13936 editor.on("tableselect:composer", function() {
13937 that.container.querySelectorAll('[data-wysihtml5-hiddentools="table"]')[0].style.display = "";
13938 });
13939 editor.on("tableunselect:composer", function() {
13940 that.container.querySelectorAll('[data-wysihtml5-hiddentools="table"]')[0].style.display = "none";
13941 });
13942 }
13943
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);
13952 } else {
13953 dom.removeClass(container, CLASS_NAME_COMMANDS_DISABLED);
13954 }
13955 }, 0);
13956 }
13957 });
13958 },
13959
13960 _updateLinkStates: function() {
13961
13962 var commandMapping = this.commandMapping,
13963 actionMapping = this.actionMapping,
13964 i,
13965 state,
13966 action,
13967 command;
13968 // every millisecond counts... this is executed quite often
13969 for (i in commandMapping) {
13970 command = commandMapping[i];
13971 if (this.commandsDisabled) {
13972 state = false;
13973 dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
13974 if (command.group) {
13975 dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
13976 }
13977 if (command.dialog) {
13978 command.dialog.hide();
13979 }
13980 } else {
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);
13985 }
13986 }
13987 if (command.state === state) {
13988 continue;
13989 }
13990
13991 command.state = state;
13992 if (state) {
13993 dom.addClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
13994 if (command.group) {
13995 dom.addClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
13996 }
13997 if (command.dialog) {
13998 if (typeof(state) === "object" || wysihtml5.lang.object(state).isArray()) {
13999
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;
14007 }
14008 command.dialog.show(state);
14009 } else {
14010 command.dialog.hide();
14011 }
14012 }
14013 } else {
14014 dom.removeClass(command.link, CLASS_NAME_COMMAND_ACTIVE);
14015 if (command.group) {
14016 dom.removeClass(command.group, CLASS_NAME_COMMAND_ACTIVE);
14017 }
14018 if (command.dialog) {
14019 command.dialog.hide();
14020 }
14021 }
14022 }
14023
14024 for (i in actionMapping) {
14025 action = actionMapping[i];
14026
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);
14031 } else {
14032 dom.removeClass(action.link, CLASS_NAME_ACTION_ACTIVE);
14033 }
14034 }
14035 }
14036 },
14037
14038 show: function() {
14039 this.container.style.display = "";
14040 },
14041
14042 hide: function() {
14043 this.container.style.display = "none";
14044 }
14045 });
14046
14047 })(wysihtml5);
14048 ;(function(wysihtml5) {
14049 wysihtml5.toolbar.Dialog_createTable = wysihtml5.toolbar.Dialog.extend({
14050 show: function(elementToChange) {
14051 this.base(elementToChange);
14052
14053 }
14054
14055 });
14056 })(wysihtml5);
14057 ;(function(wysihtml5) {
14058 var dom = wysihtml5.dom,
14059 SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]",
14060 ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field";
14061
14062 wysihtml5.toolbar.Dialog_foreColorStyle = wysihtml5.toolbar.Dialog.extend({
14063 multiselect: true,
14064
14065 _serialize: function() {
14066 var data = {},
14067 fields = this.container.querySelectorAll(SELECTOR_FIELDS),
14068 length = fields.length,
14069 i = 0;
14070
14071 for (; i<length; i++) {
14072 data[fields[i].getAttribute(ATTRIBUTE_FIELDS)] = fields[i].value;
14073 }
14074 return data;
14075 },
14076
14077 _interpolate: function(avoidHiddenFields) {
14078 var field,
14079 fieldName,
14080 newValue,
14081 focusedElement = document.querySelector(":focus"),
14082 fields = this.container.querySelectorAll(SELECTOR_FIELDS),
14083 length = fields.length,
14084 i = 0,
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;
14088
14089 for (; i<length; i++) {
14090 field = fields[i];
14091 // Never change elements where the user is currently typing in
14092 if (field === focusedElement) {
14093 continue;
14094 }
14095 // Don't update hidden fields3
14096 if (avoidHiddenFields && field.type === "hidden") {
14097 continue;
14098 }
14099 if (field.getAttribute(ATTRIBUTE_FIELDS) === "color") {
14100 if (color) {
14101 if (color[3] && color[3] != 1) {
14102 field.value = "rgba(" + color[0] + "," + color[1] + "," + color[2] + "," + color[3] + ");";
14103 } else {
14104 field.value = "rgb(" + color[0] + "," + color[1] + "," + color[2] + ");";
14105 }
14106 } else {
14107 field.value = "rgb(0,0,0);";
14108 }
14109 }
14110 }
14111 }
14112
14113 });
14114 })(wysihtml5);
14115 ;(function(wysihtml5) {
14116 var dom = wysihtml5.dom,
14117 SELECTOR_FIELDS = "[data-wysihtml5-dialog-field]",
14118 ATTRIBUTE_FIELDS = "data-wysihtml5-dialog-field";
14119
14120 wysihtml5.toolbar.Dialog_fontSizeStyle = wysihtml5.toolbar.Dialog.extend({
14121 multiselect: true,
14122
14123 _serialize: function() {
14124 return {"size" : this.container.querySelector('[data-wysihtml5-dialog-field="size"]').value};
14125 },
14126
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;
14133
14134 if (field && field !== focusedElement && size && !(/^\s*$/).test(size)) {
14135 field.value = size;
14136 }
14137 }
14138
14139 });
14140 })(wysihtml5);
14141 /*!
14142
14143 handlebars v1.3.0
14144
14145 Copyright (C) 2011 by Yehuda Katz
14146
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:
14153
14154 The above copyright notice and this permission notice shall be included in
14155 all copies or substantial portions of the Software.
14156
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
14163 THE SOFTWARE.
14164
14165 @license
14166 */
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]||"&amp;"}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={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},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"] || {};
14169
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;
14174
14175 function program1(depth0,data) {
14176
14177 var buffer = "", stack1;
14178 buffer += "btn-"
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));
14180 return buffer;
14181 }
14182
14183 function program3(depth0,data) {
14184
14185
14186 return " \n <span class=\"fa fa-quote-left\"></span>\n ";
14187 }
14188
14189 function program5(depth0,data) {
14190
14191
14192 return "\n <span class=\"glyphicon glyphicon-quote\"></span>\n ";
14193 }
14194
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";
14202 return buffer;
14203 });
14204
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;
14209
14210 function program1(depth0,data) {
14211
14212 var buffer = "", stack1;
14213 buffer += "btn-"
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));
14215 return buffer;
14216 }
14217
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";
14246 return buffer;
14247 });
14248
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;
14253
14254 function program1(depth0,data) {
14255
14256 var buffer = "", stack1;
14257 buffer += "btn-"
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));
14259 return buffer;
14260 }
14261
14262 function program3(depth0,data) {
14263
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))
14270 + "</a>\n ";
14271 return buffer;
14272 }
14273
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))
14289 + "</a>\n ";
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";
14293 return buffer;
14294 });
14295
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;
14300
14301 function program1(depth0,data) {
14302
14303 var buffer = "", stack1;
14304 buffer += "btn-"
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));
14306 return buffer;
14307 }
14308
14309 function program3(depth0,data) {
14310
14311
14312 return "\n <span class=\"fa fa-font\"></span>\n ";
14313 }
14314
14315 function program5(depth0,data) {
14316
14317
14318 return "\n <span class=\"glyphicon glyphicon-font\"></span>\n ";
14319 }
14320
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";
14344 return buffer;
14345 });
14346
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;
14351
14352 function program1(depth0,data) {
14353
14354 var buffer = "", stack1;
14355 buffer += "btn-"
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));
14357 return buffer;
14358 }
14359
14360 function program3(depth0,data) {
14361
14362
14363 return "\n <span class=\"fa fa-pencil\"></span>\n ";
14364 }
14365
14366 function program5(depth0,data) {
14367
14368
14369 return "\n <span class=\"glyphicon glyphicon-pencil\"></span>\n ";
14370 }
14371
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";
14381 return buffer;
14382 });
14383
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;
14388
14389 function program1(depth0,data) {
14390
14391
14392 return "modal-sm";
14393 }
14394
14395 function program3(depth0,data) {
14396
14397 var buffer = "", stack1;
14398 buffer += "btn-"
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));
14400 return buffer;
14401 }
14402
14403 function program5(depth0,data) {
14404
14405
14406 return "\n <span class=\"fa fa-file-image-o\"></span>\n ";
14407 }
14408
14409 function program7(depth0,data) {
14410
14411
14412 return "\n <span class=\"glyphicon glyphicon-picture\"></span>\n ";
14413 }
14414
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\">&times;</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";
14433 return buffer;
14434 });
14435
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;
14440
14441 function program1(depth0,data) {
14442
14443
14444 return "modal-sm";
14445 }
14446
14447 function program3(depth0,data) {
14448
14449 var buffer = "", stack1;
14450 buffer += "btn-"
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));
14452 return buffer;
14453 }
14454
14455 function program5(depth0,data) {
14456
14457
14458 return "\n <span class=\"fa fa-share-square-o\"></span>\n ";
14459 }
14460
14461 function program7(depth0,data) {
14462
14463
14464 return "\n <span class=\"glyphicon glyphicon-share\"></span>\n ";
14465 }
14466
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\">&times;</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";
14487 return buffer;
14488 });
14489
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;
14494
14495 function program1(depth0,data) {
14496
14497 var buffer = "", stack1;
14498 buffer += "btn-"
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));
14500 return buffer;
14501 }
14502
14503 function program3(depth0,data) {
14504
14505
14506 return "\n <span class=\"fa fa-list-ul\"></span>\n ";
14507 }
14508
14509 function program5(depth0,data) {
14510
14511
14512 return "\n <span class=\"glyphicon glyphicon-list\"></span>\n ";
14513 }
14514
14515 function program7(depth0,data) {
14516
14517
14518 return "\n <span class=\"fa fa-list-ol\"></span>\n ";
14519 }
14520
14521 function program9(depth0,data) {
14522
14523
14524 return "\n <span class=\"glyphicon glyphicon-th-list\"></span>\n ";
14525 }
14526
14527 function program11(depth0,data) {
14528
14529
14530 return "\n <span class=\"fa fa-outdent\"></span>\n ";
14531 }
14532
14533 function program13(depth0,data) {
14534
14535
14536 return "\n <span class=\"glyphicon glyphicon-indent-right\"></span>\n ";
14537 }
14538
14539 function program15(depth0,data) {
14540
14541
14542 return "\n <span class=\"fa fa-indent\"></span>\n ";
14543 }
14544
14545 function program17(depth0,data) {
14546
14547
14548 return "\n <span class=\"glyphicon glyphicon-indent-left\"></span>\n ";
14549 }
14550
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";
14584 return buffer;
14585 });(function (factory) {
14586 'use strict';
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);
14590 } else {
14591 // Browser globals
14592 factory(jQuery, wysihtml5); // jshint ignore:line
14593 }
14594 }(function ($, wysihtml5) {
14595 'use strict';
14596 var bsWysihtml5 = function($, wysihtml5) {
14597
14598 var templates = function(key, locale, options) {
14599 if(wysihtml5.tpl[key]) {
14600 return wysihtml5.tpl[key]({locale: locale, options: options});
14601 }
14602 };
14603
14604 var Wysihtml5 = function(el, options) {
14605 this.el = el;
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];
14610 }
14611 }
14612 this.toolbar = this.createToolbar(el, toolbarOpts);
14613 this.editor = this.createEditor(toolbarOpts);
14614 };
14615
14616 Wysihtml5.prototype = {
14617
14618 constructor: Wysihtml5,
14619
14620 createEditor: function(options) {
14621 options = options || {};
14622
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];
14627
14628 this.initializeEditor(this.el[0], options);
14629 },
14630
14631
14632 initializeEditor: function(el, options) {
14633 var editor = new wysihtml5.Editor(this.el[0], options);
14634
14635 editor.on('beforeload', this.syncBootstrapDialogEvents);
14636 editor.on('beforeload', this.loadParserRules);
14637
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);
14644 } else {
14645 this.addMoreShortcuts(editor, editor.composer.editableArea, options.shortcuts);
14646 }
14647
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]);
14652 }
14653 }
14654 }
14655
14656 return editor;
14657 },
14658
14659 loadParserRules: function() {
14660 if($.type(this.config.parserRules) === 'string') {
14661 $.ajax({
14662 dataType: 'json',
14663 url: this.config.parserRules,
14664 context: this,
14665 error: function (jqXHR, textStatus, errorThrown) {
14666 console.log(errorThrown);
14667 },
14668 success: function (parserRules) {
14669 this.config.parserRules = parserRules;
14670 console.log('parserrules loaded');
14671 }
14672 });
14673 }
14674
14675 if(this.config.pasteParserRulesets && $.type(this.config.pasteParserRulesets) === 'string') {
14676 $.ajax({
14677 dataType: 'json',
14678 url: this.config.pasteParserRulesets,
14679 context: this,
14680 error: function (jqXHR, textStatus, errorThrown) {
14681 console.log(errorThrown);
14682 },
14683 success: function (pasteParserRulesets) {
14684 this.config.pasteParserRulesets = pasteParserRulesets;
14685 }
14686 });
14687 }
14688 },
14689
14690 //sync wysihtml5 events for dialogs with bootstrap events
14691 syncBootstrapDialogEvents: function() {
14692 var editor = this;
14693 $.map(this.toolbar.commandMapping, function(value) {
14694 return [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');
14702 });
14703 dialog.on('hide', function() {
14704 $(this.container).modal('hide');
14705 setTimeout(editor.composer.focus, 0);
14706 });
14707 $(dialog.container).on('shown.bs.modal', function () {
14708 $(this).find('input, select, textarea').first().focus();
14709 });
14710 });
14711 this.on('change_view', function() {
14712 $(this.toolbar.container.children).find('a.btn').not('[data-wysihtml5-action="change_view"]').toggleClass('disabled');
14713 });
14714 },
14715
14716 createToolbar: function(el, options) {
14717 var self = this;
14718 var toolbar = $('<ul/>', {
14719 'class' : 'wysihtml5-toolbar',
14720 'style': 'display:none'
14721 });
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\'.');
14725 culture = 'en';
14726 }
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));
14731 }
14732 }
14733
14734 toolbar.find('a[data-wysihtml5-command="formatBlock"]').click(function(e) {
14735 var target = e.delegateTarget || e.target || e.srcElement,
14736 el = $(target),
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);
14741 }
14742 });
14743
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());
14748 });
14749
14750 this.el.before(toolbar);
14751
14752 return toolbar;
14753 },
14754
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();
14764 } else {
14765 wysihtml5.commands[command].exec(editor.composer, command);
14766 }
14767 event.preventDefault();
14768 }
14769 });
14770 }
14771 };
14772
14773 // these define our public api
14774 var methods = {
14775 resetDefaults: function() {
14776 $.fn.wysihtml5.defaultOptions = $.extend(true, {}, $.fn.wysihtml5.defaultOptionsCache);
14777 },
14778 bypassDefaults: function(options) {
14779 return this.each(function () {
14780 var $this = $(this);
14781 $this.data('wysihtml5', new Wysihtml5($this, options));
14782 });
14783 },
14784 shallowExtend: function (options) {
14785 var settings = $.extend({}, $.fn.wysihtml5.defaultOptions, options || {}, $(this).data());
14786 var that = this;
14787 return methods.bypassDefaults.apply(that, [settings]);
14788 },
14789 deepExtend: function(options) {
14790 var settings = $.extend(true, {}, $.fn.wysihtml5.defaultOptions, options || {});
14791 var that = this;
14792 return methods.bypassDefaults.apply(that, [settings]);
14793 },
14794 init: function(options) {
14795 var that = this;
14796 return methods.shallowExtend.apply(that, [options]);
14797 }
14798 };
14799
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 );
14805 } else {
14806 $.error( 'Method ' + method + ' does not exist on jQuery.wysihtml5' );
14807 }
14808 };
14809
14810 $.fn.wysihtml5.Constructor = Wysihtml5;
14811
14812 var defaultOptions = $.fn.wysihtml5.defaultOptions = {
14813 toolbar: {
14814 'font-styles': true,
14815 'color': false,
14816 'emphasis': {
14817 'small': true
14818 },
14819 'blockquote': true,
14820 'lists': true,
14821 'html': false,
14822 'link': true,
14823 'image': true,
14824 'smallmodals': false
14825 },
14826 useLineBreaks: false,
14827 parserRules: {
14828 classes: {
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
14845 },
14846 tags: {
14847 'b': {},
14848 'i': {},
14849 'strong': {},
14850 'em': {},
14851 'p': {},
14852 'br': {},
14853 'ol': {},
14854 'ul': {},
14855 'li': {},
14856 'h1': {},
14857 'h2': {},
14858 'h3': {},
14859 'h4': {},
14860 'h5': {},
14861 'h6': {},
14862 'blockquote': {},
14863 'u': 1,
14864 'img': {
14865 'check_attributes': {
14866 'width': 'numbers',
14867 'alt': 'alt',
14868 'src': 'url',
14869 'height': 'numbers'
14870 }
14871 },
14872 'a': {
14873 'check_attributes': {
14874 'href': 'url'
14875 },
14876 'set_attributes': {
14877 'target': '_blank',
14878 'rel': 'nofollow'
14879 }
14880 },
14881 'span': 1,
14882 'div': 1,
14883 'small': 1,
14884 'code': 1,
14885 'pre': 1
14886 }
14887 },
14888 locale: 'en',
14889 shortcuts: {
14890 '83': 'small',// S
14891 '75': 'createLink'// K
14892 }
14893 };
14894
14895 if (typeof $.fn.wysihtml5.defaultOptionsCache === 'undefined') {
14896 $.fn.wysihtml5.defaultOptionsCache = $.extend(true, {}, $.fn.wysihtml5.defaultOptions);
14897 }
14898
14899 var locale = $.fn.wysihtml5.locale = {};
14900 };
14901 bsWysihtml5($, wysihtml5);
14902 }));
14903 (function(wysihtml5) {
14904 wysihtml5.commands.small = {
14905 exec: function(composer, command) {
14906 return wysihtml5.commands.formatInline.exec(composer, command, "small");
14907 },
14908
14909 state: function(composer, command) {
14910 return wysihtml5.commands.formatInline.state(composer, command, "small");
14911 }
14912 };
14913 })(wysihtml5);
14914
14915 /**
14916 * English translation for bootstrap-wysihtml5
14917 */
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);
14922 } else {
14923 // Browser globals
14924 factory(jQuery);
14925 }
14926 }(function ($) {
14927 $.fn.wysihtml5.locale.en = $.fn.wysihtml5.locale['en-US'] = {
14928 font_styles: {
14929 normal: 'Normal text',
14930 h1: 'Heading 1',
14931 h2: 'Heading 2',
14932 h3: 'Heading 3',
14933 h4: 'Heading 4',
14934 h5: 'Heading 5',
14935 h6: 'Heading 6'
14936 },
14937 emphasis: {
14938 bold: 'Bold',
14939 italic: 'Italic',
14940 underline: 'Underline',
14941 small: 'Small'
14942 },
14943 lists: {
14944 unordered: 'Unordered list',
14945 ordered: 'Ordered list',
14946 outdent: 'Outdent',
14947 indent: 'Indent'
14948 },
14949 link: {
14950 insert: 'Insert link',
14951 cancel: 'Cancel',
14952 target: 'Open link in new window'
14953 },
14954 image: {
14955 insert: 'Insert image',
14956 cancel: 'Cancel'
14957 },
14958 html: {
14959 edit: 'Edit HTML'
14960 },
14961 colours: {
14962 black: 'Black',
14963 silver: 'Silver',
14964 gray: 'Grey',
14965 maroon: 'Maroon',
14966 red: 'Red',
14967 purple: 'Purple',
14968 green: 'Green',
14969 olive: 'Olive',
14970 navy: 'Navy',
14971 blue: 'Blue',
14972 orange: 'Orange'
14973 }
14974 };
14975 }));