liquid_feedback_frontend

view static/wysihtml/wysihtml.js @ 1800:b87997219042

Updated spanish translation
author bsw
date Thu Oct 21 15:22:29 2021 +0200 (2021-10-21)
parents 32cc544d5a5b
children
line source
1 /**
2 * @license wysihtml v0.6.0-beta1
3 * https://github.com/Voog/wysihtml
4 *
5 * Author: Christopher Blum (https://github.com/tiff)
6 * Secondary author of extended features: Oliver Pulges (https://github.com/pulges)
7 *
8 * Copyright (C) 2012 XING AG
9 * Licensed under the MIT license (MIT)
10 *
11 */
12 var wysihtml = {
13 version: '0.6.0-beta1',
15 // namespaces
16 commands: {},
17 dom: {},
18 quirks: {},
19 toolbar: {},
20 lang: {},
21 selection: {},
22 views: {},
24 editorExtenders: [],
25 extendEditor: function(extender) {
26 this.editorExtenders.push(extender);
27 },
29 INVISIBLE_SPACE: '\uFEFF',
30 INVISIBLE_SPACE_REG_EXP: /\uFEFF/g,
32 VOID_ELEMENTS: 'area, base, br, col, embed, hr, img, input, keygen, link, meta, param, source, track, wbr',
33 PERMITTED_PHRASING_CONTENT_ONLY: 'h1, h2, h3, h4, h5, h6, p, pre',
35 EMPTY_FUNCTION: function() {},
37 ELEMENT_NODE: 1,
38 TEXT_NODE: 3,
40 BACKSPACE_KEY: 8,
41 ENTER_KEY: 13,
42 ESCAPE_KEY: 27,
43 SPACE_KEY: 32,
44 TAB_KEY: 9,
45 DELETE_KEY: 46
46 };
48 wysihtml.polyfills = function(win, doc) {
50 var methods = {
52 // Safary has a bug of not restoring selection after node.normalize correctly.
53 // Detects the misbegaviour and patches it
54 normalizeHasCaretError: function() {
55 if ("createRange" in doc && "getSelection" in win) {
56 var originalTarget,
57 scrollTop = window.pageYOffset,
58 scrollLeft = window.pageXOffset,
59 e = doc.createElement('div'),
60 t1 = doc.createTextNode('a'),
61 t2 = doc.createTextNode('a'),
62 t3 = doc.createTextNode('a'),
63 r = doc.createRange(),
64 s, ret;
66 if (document.activeElement) {
67 if (document.activeElement.nodeType === 1 && ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].indexOf(document.activeElement.nodeName) > -1) {
68 originalTarget = {
69 type: 'form',
70 node: document.activeElement,
71 start: document.activeElement.selectionStart,
72 end: document.activeElement.selectionEnd,
73 };
74 } else {
75 s = win.getSelection();
76 if (s && s.anchorNode) {
77 originalTarget = {
78 type: 'range',
79 anchorNode: s.anchorNode,
80 anchorOffset: s.anchorOffset,
81 focusNode: s.focusNode,
82 focusOffset: s.focusOffset
83 };
84 }
85 }
86 }
88 e.setAttribute('contenteditable', 'true');
89 e.appendChild(t1);
90 e.appendChild(t2);
91 e.appendChild(t3);
92 doc.body.appendChild(e);
93 r.setStart(t2, 1);
94 r.setEnd(t2, 1);
96 s = win.getSelection();
97 s.removeAllRanges();
98 s.addRange(r);
99 e.normalize();
100 s = win.getSelection();
102 ret = (e.childNodes.length !== 1 || s.anchorNode !== e.firstChild || s.anchorOffset !== 2);
103 e.parentNode.removeChild(e);
104 s.removeAllRanges();
106 if (originalTarget) {
107 if (originalTarget.type === 'form') {
108 // The selection parameters are not present for all form elements
109 if (typeof originalTarget.start !== 'undefined' && typeof originalTarget.end !== 'undefined') {
110 originalTarget.node.setSelectionRange(originalTarget.start, originalTarget.end);
111 }
112 originalTarget.node.focus();
113 } else if (originalTarget.type === 'range') {
114 r = doc.createRange();
115 r.setStart(originalTarget.anchorNode, originalTarget.anchorOffset);
116 r.setEnd(originalTarget.focusNode, originalTarget.focusOffset);
117 s.addRange(r);
118 }
119 }
121 if (scrollTop !== window.pageYOffset || scrollLeft !== window.pageXOffset) {
122 win.scrollTo(scrollLeft, scrollTop);
123 }
125 return ret;
126 }
127 },
129 apply: function() {
130 // closest, matches, and remove polyfill
131 // https://github.com/jonathantneal/closest
132 (function (ELEMENT) {
133 ELEMENT.matches = ELEMENT.matches || ELEMENT.mozMatchesSelector || ELEMENT.msMatchesSelector || ELEMENT.oMatchesSelector || ELEMENT.webkitMatchesSelector || function matches(selector) {
134 var
135 element = this,
136 elements = (element.document || element.ownerDocument).querySelectorAll(selector),
137 index = 0;
139 while (elements[index] && elements[index] !== element) {
140 ++index;
141 }
143 return elements[index] ? true : false;
144 };
146 ELEMENT.closest = ELEMENT.closest || function closest(selector) {
147 var element = this;
149 while (element) {
150 if (element.matches(selector)) {
151 break;
152 }
154 element = element.parentElement;
155 }
157 return element;
158 };
160 ELEMENT.remove = ELEMENT.remove || function remove() {
161 if (this.parentNode) {
162 this.parentNode.removeChild(this);
163 }
164 };
166 }(win.Element.prototype));
168 if (!('classList' in doc.documentElement) && win.Object.defineProperty && typeof win.HTMLElement !== 'undefined') {
169 win.Object.defineProperty(win.HTMLElement.prototype, 'classList', {
170 get: function() {
171 var self = this;
172 function update(fn) {
173 return function(value) {
174 var classes = self.className.split(/\s+/),
175 index = classes.indexOf(value);
177 fn(classes, index, value);
178 self.className = classes.join(' ');
179 };
180 }
182 var ret = {
183 add: update(function(classes, index, value) {
184 ~index || classes.push(value);
185 }),
187 remove: update(function(classes, index) {
188 ~index && classes.splice(index, 1);
189 }),
191 toggle: update(function(classes, index, value) {
192 ~index ? classes.splice(index, 1) : classes.push(value);
193 }),
195 contains: function(value) {
196 return !!~self.className.split(/\s+/).indexOf(value);
197 },
199 item: function(i) {
200 return self.className.split(/\s+/)[i] || null;
201 }
202 };
204 win.Object.defineProperty(ret, 'length', {
205 get: function() {
206 return self.className.split(/\s+/).length;
207 }
208 });
210 return ret;
211 }
212 });
213 }
215 var getTextNodes = function(node){
216 var all = [];
217 for (node=node.firstChild;node;node=node.nextSibling){
218 if (node.nodeType == 3) {
219 all.push(node);
220 } else {
221 all = all.concat(getTextNodes(node));
222 }
223 }
224 return all;
225 };
227 var isInDom = function(node) {
228 var doc = node.ownerDocument,
229 n = node;
231 do {
232 if (n === doc) {
233 return true;
234 }
235 n = n.parentNode;
236 } while(n);
238 return false;
239 };
241 var normalizeFix = function() {
242 var f = win.Node.prototype.normalize;
243 var nf = function() {
244 var texts = getTextNodes(this),
245 s = this.ownerDocument.defaultView.getSelection(),
246 anode = s.anchorNode,
247 aoffset = s.anchorOffset,
248 aelement = anode && anode.nodeType === 1 && anode.childNodes.length > 0 ? anode.childNodes[aoffset] : undefined,
249 fnode = s.focusNode,
250 foffset = s.focusOffset,
251 felement = fnode && fnode.nodeType === 1 && foffset > 0 ? fnode.childNodes[foffset -1] : undefined,
252 r = this.ownerDocument.createRange(),
253 prevTxt = texts.shift(),
254 curText = prevTxt ? texts.shift() : null;
256 if (felement && felement.nodeType === 3) {
257 fnode = felement;
258 foffset = felement.nodeValue.length;
259 felement = undefined;
260 }
262 if (aelement && aelement.nodeType === 3) {
263 anode = aelement;
264 aoffset = 0;
265 aelement = undefined;
266 }
268 if ((anode === fnode && foffset < aoffset) || (anode !== fnode && (anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_PRECEDING) && !(anode.compareDocumentPosition(fnode) & win.Node.DOCUMENT_POSITION_CONTAINS))) {
269 fnode = [anode, anode = fnode][0];
270 foffset = [aoffset, aoffset = foffset][0];
271 }
273 while(prevTxt && curText) {
274 if (curText.previousSibling && curText.previousSibling === prevTxt) {
275 if (anode === curText) {
276 anode = prevTxt;
277 aoffset = prevTxt.nodeValue.length + aoffset;
278 }
279 if (fnode === curText) {
280 fnode = prevTxt;
281 foffset = prevTxt.nodeValue.length + foffset;
282 }
283 prevTxt.nodeValue = prevTxt.nodeValue + curText.nodeValue;
284 curText.parentNode.removeChild(curText);
285 curText = texts.shift();
286 } else {
287 prevTxt = curText;
288 curText = texts.shift();
289 }
290 }
292 if (felement) {
293 foffset = Array.prototype.indexOf.call(felement.parentNode.childNodes, felement) + 1;
294 }
296 if (aelement) {
297 aoffset = Array.prototype.indexOf.call(aelement.parentNode.childNodes, aelement);
298 }
300 if (isInDom(this) && anode && anode.parentNode && fnode && fnode.parentNode) {
301 r.setStart(anode, aoffset);
302 r.setEnd(fnode, foffset);
303 s.removeAllRanges();
304 s.addRange(r);
305 }
306 };
307 win.Node.prototype.normalize = nf;
308 };
310 var F = function() {
311 win.removeEventListener("load", F);
312 if ("Node" in win && "normalize" in win.Node.prototype && methods.normalizeHasCaretError()) {
313 normalizeFix();
314 }
315 };
317 if (doc.readyState !== "complete") {
318 win.addEventListener("load", F);
319 } else {
320 F();
321 }
323 // CustomEvent for ie9 and up
324 function nativeCustomEventSupported() {
325 try {
326 var p = new win.CustomEvent('cat', {detail: {foo: 'bar'}});
327 return 'cat' === p.type && 'bar' === p.detail.foo;
328 } catch (e) {}
329 return false;
330 }
332 // Polyfills CustomEvent object for IE9 and up
333 (function() {
334 if (!nativeCustomEventSupported() && "CustomEvent" in win) {
335 function CustomEvent(event, params) {
336 params = params || {bubbles: false, cancelable: false, detail: undefined};
337 var evt = doc.createEvent('CustomEvent');
338 evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
339 return evt;
340 }
341 CustomEvent.prototype = win.Event.prototype;
342 win.CustomEvent = CustomEvent;
343 }
344 })();
345 }
346 }
348 return methods;
349 };
351 wysihtml.polyfills(window, document).apply();
353 /*
354 Base.js, version 1.1a
355 Copyright 2006-2010, Dean Edwards
356 License: http://www.opensource.org/licenses/mit-license.php
357 */
359 var Base = function() {
360 // dummy
361 };
363 Base.extend = function(_instance, _static) { // subclass
364 var extend = Base.prototype.extend;
366 // build the prototype
367 Base._prototyping = true;
368 var proto = new this;
369 extend.call(proto, _instance);
370 proto.base = function() {
371 // call this method from any other method to invoke that method's ancestor
372 };
373 delete Base._prototyping;
375 // create the wrapper for the constructor function
376 //var constructor = proto.constructor.valueOf(); //-dean
377 var constructor = proto.constructor;
378 var klass = proto.constructor = function() {
379 if (!Base._prototyping) {
380 if (this._constructing || this.constructor == klass) { // instantiation
381 this._constructing = true;
382 constructor.apply(this, arguments);
383 delete this._constructing;
384 } else if (arguments[0] != null) { // casting
385 return (arguments[0].extend || extend).call(arguments[0], proto);
386 }
387 }
388 };
390 // build the class interface
391 klass.ancestor = this;
392 klass.extend = this.extend;
393 klass.forEach = this.forEach;
394 klass.implement = this.implement;
395 klass.prototype = proto;
396 klass.toString = this.toString;
397 klass.valueOf = function(type) {
398 //return (type == "object") ? klass : constructor; //-dean
399 return (type == "object") ? klass : constructor.valueOf();
400 };
401 extend.call(klass, _static);
402 // class initialisation
403 if (typeof klass.init == "function") klass.init();
404 return klass;
405 };
407 Base.prototype = {
408 extend: function(source, value) {
409 if (arguments.length > 1) { // extending with a name/value pair
410 var ancestor = this[source];
411 if (ancestor && (typeof value == "function") && // overriding a method?
412 // the valueOf() comparison is to avoid circular references
413 (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) &&
414 /\bbase\b/.test(value)) {
415 // get the underlying method
416 var method = value.valueOf();
417 // override
418 value = function() {
419 var previous = this.base || Base.prototype.base;
420 this.base = ancestor;
421 var returnValue = method.apply(this, arguments);
422 this.base = previous;
423 return returnValue;
424 };
425 // point to the underlying method
426 value.valueOf = function(type) {
427 return (type == "object") ? value : method;
428 };
429 value.toString = Base.toString;
430 }
431 this[source] = value;
432 } else if (source) { // extending with an object literal
433 var extend = Base.prototype.extend;
434 // if this object has a customised extend method then use it
435 if (!Base._prototyping && typeof this != "function") {
436 extend = this.extend || extend;
437 }
438 var proto = {toSource: null};
439 // do the "toString" and other methods manually
440 var hidden = ["constructor", "toString", "valueOf"];
441 // if we are prototyping then include the constructor
442 var i = Base._prototyping ? 0 : 1;
443 while (key = hidden[i++]) {
444 if (source[key] != proto[key]) {
445 extend.call(this, key, source[key]);
447 }
448 }
449 // copy each of the source object's properties to this object
450 for (var key in source) {
451 if (!proto[key]) extend.call(this, key, source[key]);
452 }
453 }
454 return this;
455 }
456 };
458 // initialise
459 Base = Base.extend({
460 constructor: function() {
461 this.extend(arguments[0]);
462 }
463 }, {
464 ancestor: Object,
465 version: "1.1",
467 forEach: function(object, block, context) {
468 for (var key in object) {
469 if (this.prototype[key] === undefined) {
470 block.call(context, object[key], key, object);
471 }
472 }
473 },
475 implement: function() {
476 for (var i = 0; i < arguments.length; i++) {
477 if (typeof arguments[i] == "function") {
478 // if it's a function, call it
479 arguments[i](this.prototype);
480 } else {
481 // add the interface using the extend method
482 this.prototype.extend(arguments[i]);
483 }
484 }
485 return this;
486 },
488 toString: function() {
489 return String(this.valueOf());
490 }
491 });
492 /**
493 * Rangy, a cross-browser JavaScript range and selection library
494 * https://github.com/timdown/rangy
495 *
496 * Copyright 2015, Tim Down
497 * Licensed under the MIT license.
498 * Version: 1.3.1-dev
499 * Build date: 20 May 2015
500 *
501 * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
502 */
503 var rangy;
505 (function() {
506 var OBJECT = "object", FUNCTION = "function", UNDEFINED = "undefined";
508 // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START
509 // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113.
510 var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
511 "commonAncestorContainer"];
513 // Minimal set of methods required for DOM Level 2 Range compliance
514 var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore",
515 "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents",
516 "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"];
518 var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"];
520 // Subset of TextRange's full set of methods that we're interested in
521 var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select",
522 "setEndPoint", "getBoundingClientRect"];
524 /*----------------------------------------------------------------------------------------------------------------*/
526 // Trio of functions taken from Peter Michaux's article:
527 // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
528 function isHostMethod(o, p) {
529 var t = typeof o[p];
530 return t == FUNCTION || (!!(t == OBJECT && o[p])) || t == "unknown";
531 }
533 function isHostObject(o, p) {
534 return !!(typeof o[p] == OBJECT && o[p]);
535 }
537 function isHostProperty(o, p) {
538 return typeof o[p] != UNDEFINED;
539 }
541 // Creates a convenience function to save verbose repeated calls to tests functions
542 function createMultiplePropertyTest(testFunc) {
543 return function(o, props) {
544 var i = props.length;
545 while (i--) {
546 if (!testFunc(o, props[i])) {
547 return false;
548 }
549 }
550 return true;
551 };
552 }
554 // Next trio of functions are a convenience to save verbose repeated calls to previous two functions
555 var areHostMethods = createMultiplePropertyTest(isHostMethod);
556 var areHostObjects = createMultiplePropertyTest(isHostObject);
557 var areHostProperties = createMultiplePropertyTest(isHostProperty);
559 function isTextRange(range) {
560 return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties);
561 }
563 function getBody(doc) {
564 return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0];
565 }
567 var forEach = [].forEach ?
568 function(arr, func) {
569 arr.forEach(func);
570 } :
571 function(arr, func) {
572 for (var i = 0, len = arr.length; i < len; ++i) {
573 func(arr[i], i);
574 }
575 };
577 var modules = {};
579 var isBrowser = (typeof window != UNDEFINED && typeof document != UNDEFINED);
581 var util = {
582 isHostMethod: isHostMethod,
583 isHostObject: isHostObject,
584 isHostProperty: isHostProperty,
585 areHostMethods: areHostMethods,
586 areHostObjects: areHostObjects,
587 areHostProperties: areHostProperties,
588 isTextRange: isTextRange,
589 getBody: getBody,
590 forEach: forEach
591 };
593 var api = {
594 version: "1.3.1-dev",
595 initialized: false,
596 isBrowser: isBrowser,
597 supported: true,
598 util: util,
599 features: {},
600 modules: modules,
601 config: {
602 alertOnFail: false,
603 alertOnWarn: false,
604 preferTextRange: false,
605 autoInitialize: (typeof rangyAutoInitialize == UNDEFINED) ? true : rangyAutoInitialize
606 }
607 };
609 function consoleLog(msg) {
610 if (typeof console != UNDEFINED && isHostMethod(console, "log")) {
611 console.log(msg);
612 }
613 }
615 function alertOrLog(msg, shouldAlert) {
616 if (isBrowser && shouldAlert) {
617 alert(msg);
618 } else {
619 consoleLog(msg);
620 }
621 }
623 function fail(reason) {
624 api.initialized = true;
625 api.supported = false;
626 alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail);
627 }
629 api.fail = fail;
631 function warn(msg) {
632 alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn);
633 }
635 api.warn = warn;
637 // Add utility extend() method
638 var extend;
639 if ({}.hasOwnProperty) {
640 util.extend = extend = function(obj, props, deep) {
641 var o, p;
642 for (var i in props) {
643 if (props.hasOwnProperty(i)) {
644 o = obj[i];
645 p = props[i];
646 if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") {
647 extend(o, p, true);
648 }
649 obj[i] = p;
650 }
651 }
652 // Special case for toString, which does not show up in for...in loops in IE <= 8
653 if (props.hasOwnProperty("toString")) {
654 obj.toString = props.toString;
655 }
656 return obj;
657 };
659 util.createOptions = function(optionsParam, defaults) {
660 var options = {};
661 extend(options, defaults);
662 if (optionsParam) {
663 extend(options, optionsParam);
664 }
665 return options;
666 };
667 } else {
668 fail("hasOwnProperty not supported");
669 }
671 // Test whether we're in a browser and bail out if not
672 if (!isBrowser) {
673 fail("Rangy can only run in a browser");
674 }
676 // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not
677 (function() {
678 var toArray;
680 if (isBrowser) {
681 var el = document.createElement("div");
682 el.appendChild(document.createElement("span"));
683 var slice = [].slice;
684 try {
685 if (slice.call(el.childNodes, 0)[0].nodeType == 1) {
686 toArray = function(arrayLike) {
687 return slice.call(arrayLike, 0);
688 };
689 }
690 } catch (e) {}
691 }
693 if (!toArray) {
694 toArray = function(arrayLike) {
695 var arr = [];
696 for (var i = 0, len = arrayLike.length; i < len; ++i) {
697 arr[i] = arrayLike[i];
698 }
699 return arr;
700 };
701 }
703 util.toArray = toArray;
704 })();
706 // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or
707 // normalization of event properties
708 var addListener;
709 if (isBrowser) {
710 if (isHostMethod(document, "addEventListener")) {
711 addListener = function(obj, eventType, listener) {
712 obj.addEventListener(eventType, listener, false);
713 };
714 } else if (isHostMethod(document, "attachEvent")) {
715 addListener = function(obj, eventType, listener) {
716 obj.attachEvent("on" + eventType, listener);
717 };
718 } else {
719 fail("Document does not have required addEventListener or attachEvent method");
720 }
722 util.addListener = addListener;
723 }
725 var initListeners = [];
727 function getErrorDesc(ex) {
728 return ex.message || ex.description || String(ex);
729 }
731 // Initialization
732 function init() {
733 if (!isBrowser || api.initialized) {
734 return;
735 }
736 var testRange;
737 var implementsDomRange = false, implementsTextRange = false;
739 // First, perform basic feature tests
741 if (isHostMethod(document, "createRange")) {
742 testRange = document.createRange();
743 if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) {
744 implementsDomRange = true;
745 }
746 }
748 var body = getBody(document);
749 if (!body || body.nodeName.toLowerCase() != "body") {
750 fail("No body element found");
751 return;
752 }
754 if (body && isHostMethod(body, "createTextRange")) {
755 testRange = body.createTextRange();
756 if (isTextRange(testRange)) {
757 implementsTextRange = true;
758 }
759 }
761 if (!implementsDomRange && !implementsTextRange) {
762 fail("Neither Range nor TextRange are available");
763 return;
764 }
766 api.initialized = true;
767 api.features = {
768 implementsDomRange: implementsDomRange,
769 implementsTextRange: implementsTextRange
770 };
772 // Initialize modules
773 var module, errorMessage;
774 for (var moduleName in modules) {
775 if ( (module = modules[moduleName]) instanceof Module ) {
776 module.init(module, api);
777 }
778 }
780 // Call init listeners
781 for (var i = 0, len = initListeners.length; i < len; ++i) {
782 try {
783 initListeners[i](api);
784 } catch (ex) {
785 errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex);
786 consoleLog(errorMessage);
787 }
788 }
789 }
791 function deprecationNotice(deprecated, replacement, module) {
792 if (module) {
793 deprecated += " in module " + module.name;
794 }
795 api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " +
796 replacement + " instead.");
797 }
799 function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) {
800 owner[deprecated] = function() {
801 deprecationNotice(deprecated, replacement, module);
802 return owner[replacement].apply(owner, util.toArray(arguments));
803 };
804 }
806 util.deprecationNotice = deprecationNotice;
807 util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod;
809 // Allow external scripts to initialize this library in case it's loaded after the document has loaded
810 api.init = init;
812 // Execute listener immediately if already initialized
813 api.addInitListener = function(listener) {
814 if (api.initialized) {
815 listener(api);
816 } else {
817 initListeners.push(listener);
818 }
819 };
821 var shimListeners = [];
823 api.addShimListener = function(listener) {
824 shimListeners.push(listener);
825 };
827 function shim(win) {
828 win = win || window;
829 init();
831 // Notify listeners
832 for (var i = 0, len = shimListeners.length; i < len; ++i) {
833 shimListeners[i](win);
834 }
835 }
837 if (isBrowser) {
838 api.shim = api.createMissingNativeApi = shim;
839 createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim");
840 }
842 function Module(name, dependencies, initializer) {
843 this.name = name;
844 this.dependencies = dependencies;
845 this.initialized = false;
846 this.supported = false;
847 this.initializer = initializer;
848 }
850 Module.prototype = {
851 init: function() {
852 var requiredModuleNames = this.dependencies || [];
853 for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {
854 moduleName = requiredModuleNames[i];
856 requiredModule = modules[moduleName];
857 if (!requiredModule || !(requiredModule instanceof Module)) {
858 throw new Error("required module '" + moduleName + "' not found");
859 }
861 requiredModule.init();
863 if (!requiredModule.supported) {
864 throw new Error("required module '" + moduleName + "' not supported");
865 }
866 }
868 // Now run initializer
869 this.initializer(this);
870 },
872 fail: function(reason) {
873 this.initialized = true;
874 this.supported = false;
875 throw new Error(reason);
876 },
878 warn: function(msg) {
879 api.warn("Module " + this.name + ": " + msg);
880 },
882 deprecationNotice: function(deprecated, replacement) {
883 api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " +
884 replacement + " instead");
885 },
887 createError: function(msg) {
888 return new Error("Error in Rangy " + this.name + " module: " + msg);
889 }
890 };
892 function createModule(name, dependencies, initFunc) {
893 var newModule = new Module(name, dependencies, function(module) {
894 if (!module.initialized) {
895 module.initialized = true;
896 try {
897 initFunc(api, module);
898 module.supported = true;
899 } catch (ex) {
900 var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex);
901 consoleLog(errorMessage);
902 if (ex.stack) {
903 consoleLog(ex.stack);
904 }
905 }
906 }
907 });
908 modules[name] = newModule;
909 return newModule;
910 }
912 api.createModule = function(name) {
913 // Allow 2 or 3 arguments (second argument is an optional array of dependencies)
914 var initFunc, dependencies;
915 if (arguments.length == 2) {
916 initFunc = arguments[1];
917 dependencies = [];
918 } else {
919 initFunc = arguments[2];
920 dependencies = arguments[1];
921 }
923 var module = createModule(name, dependencies, initFunc);
925 // Initialize the module immediately if the core is already initialized
926 if (api.initialized && api.supported) {
927 module.init();
928 }
929 };
931 api.createCoreModule = function(name, dependencies, initFunc) {
932 createModule(name, dependencies, initFunc);
933 };
935 /*----------------------------------------------------------------------------------------------------------------*/
937 // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately
939 function RangePrototype() {}
940 api.RangePrototype = RangePrototype;
941 api.rangePrototype = new RangePrototype();
943 function SelectionPrototype() {}
944 api.selectionPrototype = new SelectionPrototype();
946 /*----------------------------------------------------------------------------------------------------------------*/
948 // DOM utility methods used by Rangy
949 api.createCoreModule("DomUtil", [], function(api, module) {
950 var UNDEF = "undefined";
951 var util = api.util;
952 var getBody = util.getBody;
954 // Perform feature tests
955 if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) {
956 module.fail("document missing a Node creation method");
957 }
959 if (!util.isHostMethod(document, "getElementsByTagName")) {
960 module.fail("document missing getElementsByTagName method");
961 }
963 var el = document.createElement("div");
964 if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] ||
965 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) {
966 module.fail("Incomplete Element implementation");
967 }
969 // innerHTML is required for Range's createContextualFragment method
970 if (!util.isHostProperty(el, "innerHTML")) {
971 module.fail("Element is missing innerHTML property");
972 }
974 var textNode = document.createTextNode("test");
975 if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] ||
976 !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) ||
977 !util.areHostProperties(textNode, ["data"]))) {
978 module.fail("Incomplete Text Node implementation");
979 }
981 /*----------------------------------------------------------------------------------------------------------------*/
983 // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been
984 // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that
985 // contains just the document as a single element and the value searched for is the document.
986 var arrayContains = /*Array.prototype.indexOf ?
987 function(arr, val) {
988 return arr.indexOf(val) > -1;
989 }:*/
991 function(arr, val) {
992 var i = arr.length;
993 while (i--) {
994 if (arr[i] === val) {
995 return true;
996 }
997 }
998 return false;
999 };
1001 // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI
1002 function isHtmlNamespace(node) {
1003 var ns;
1004 return typeof node.namespaceURI == UNDEF || ((ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml");
1007 function parentElement(node) {
1008 var parent = node.parentNode;
1009 return (parent.nodeType == 1) ? parent : null;
1012 function getNodeIndex(node) {
1013 var i = 0;
1014 while( (node = node.previousSibling) ) {
1015 ++i;
1017 return i;
1020 function getNodeLength(node) {
1021 switch (node.nodeType) {
1022 case 7:
1023 case 10:
1024 return 0;
1025 case 3:
1026 case 8:
1027 return node.length;
1028 default:
1029 return node.childNodes.length;
1033 function getCommonAncestor(node1, node2) {
1034 var ancestors = [], n;
1035 for (n = node1; n; n = n.parentNode) {
1036 ancestors.push(n);
1039 for (n = node2; n; n = n.parentNode) {
1040 if (arrayContains(ancestors, n)) {
1041 return n;
1045 return null;
1048 function isAncestorOf(ancestor, descendant, selfIsAncestor) {
1049 var n = selfIsAncestor ? descendant : descendant.parentNode;
1050 while (n) {
1051 if (n === ancestor) {
1052 return true;
1053 } else {
1054 n = n.parentNode;
1057 return false;
1060 function isOrIsAncestorOf(ancestor, descendant) {
1061 return isAncestorOf(ancestor, descendant, true);
1064 function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
1065 var p, n = selfIsAncestor ? node : node.parentNode;
1066 while (n) {
1067 p = n.parentNode;
1068 if (p === ancestor) {
1069 return n;
1071 n = p;
1073 return null;
1076 function isCharacterDataNode(node) {
1077 var t = node.nodeType;
1078 return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
1081 function isTextOrCommentNode(node) {
1082 if (!node) {
1083 return false;
1085 var t = node.nodeType;
1086 return t == 3 || t == 8 ; // Text or Comment
1089 function insertAfter(node, precedingNode) {
1090 var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
1091 if (nextNode) {
1092 parent.insertBefore(node, nextNode);
1093 } else {
1094 parent.appendChild(node);
1096 return node;
1099 // Note that we cannot use splitText() because it is bugridden in IE 9.
1100 function splitDataNode(node, index, positionsToPreserve) {
1101 var newNode = node.cloneNode(false);
1102 newNode.deleteData(0, index);
1103 node.deleteData(index, node.length - index);
1104 insertAfter(newNode, node);
1106 // Preserve positions
1107 if (positionsToPreserve) {
1108 for (var i = 0, position; position = positionsToPreserve[i++]; ) {
1109 // Handle case where position was inside the portion of node after the split point
1110 if (position.node == node && position.offset > index) {
1111 position.node = newNode;
1112 position.offset -= index;
1114 // Handle the case where the position is a node offset within node's parent
1115 else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) {
1116 ++position.offset;
1120 return newNode;
1123 function getDocument(node) {
1124 if (node.nodeType == 9) {
1125 return node;
1126 } else if (typeof node.ownerDocument != UNDEF) {
1127 return node.ownerDocument;
1128 } else if (typeof node.document != UNDEF) {
1129 return node.document;
1130 } else if (node.parentNode) {
1131 return getDocument(node.parentNode);
1132 } else {
1133 throw module.createError("getDocument: no document found for node");
1137 function getWindow(node) {
1138 var doc = getDocument(node);
1139 if (typeof doc.defaultView != UNDEF) {
1140 return doc.defaultView;
1141 } else if (typeof doc.parentWindow != UNDEF) {
1142 return doc.parentWindow;
1143 } else {
1144 throw module.createError("Cannot get a window object for node");
1148 function getIframeDocument(iframeEl) {
1149 if (typeof iframeEl.contentDocument != UNDEF) {
1150 return iframeEl.contentDocument;
1151 } else if (typeof iframeEl.contentWindow != UNDEF) {
1152 return iframeEl.contentWindow.document;
1153 } else {
1154 throw module.createError("getIframeDocument: No Document object found for iframe element");
1158 function getIframeWindow(iframeEl) {
1159 if (typeof iframeEl.contentWindow != UNDEF) {
1160 return iframeEl.contentWindow;
1161 } else if (typeof iframeEl.contentDocument != UNDEF) {
1162 return iframeEl.contentDocument.defaultView;
1163 } else {
1164 throw module.createError("getIframeWindow: No Window object found for iframe element");
1168 // This looks bad. Is it worth it?
1169 function isWindow(obj) {
1170 return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document");
1173 function getContentDocument(obj, module, methodName) {
1174 var doc;
1176 if (!obj) {
1177 doc = document;
1180 // Test if a DOM node has been passed and obtain a document object for it if so
1181 else if (util.isHostProperty(obj, "nodeType")) {
1182 doc = (obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe") ?
1183 getIframeDocument(obj) : getDocument(obj);
1186 // Test if the doc parameter appears to be a Window object
1187 else if (isWindow(obj)) {
1188 doc = obj.document;
1191 if (!doc) {
1192 throw module.createError(methodName + "(): Parameter must be a Window object or DOM node");
1195 return doc;
1198 function getRootContainer(node) {
1199 var parent;
1200 while ( (parent = node.parentNode) ) {
1201 node = parent;
1203 return node;
1206 function comparePoints(nodeA, offsetA, nodeB, offsetB) {
1207 // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing
1208 var nodeC, root, childA, childB, n;
1209 if (nodeA == nodeB) {
1210 // Case 1: nodes are the same
1211 return offsetA === offsetB ? 0 : (offsetA < offsetB) ? -1 : 1;
1212 } else if ( (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) ) {
1213 // Case 2: node C (container B or an ancestor) is a child node of A
1214 return offsetA <= getNodeIndex(nodeC) ? -1 : 1;
1215 } else if ( (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) ) {
1216 // Case 3: node C (container A or an ancestor) is a child node of B
1217 return getNodeIndex(nodeC) < offsetB ? -1 : 1;
1218 } else {
1219 root = getCommonAncestor(nodeA, nodeB);
1220 if (!root) {
1221 throw new Error("comparePoints error: nodes have no common ancestor");
1224 // Case 4: containers are siblings or descendants of siblings
1225 childA = (nodeA === root) ? root : getClosestAncestorIn(nodeA, root, true);
1226 childB = (nodeB === root) ? root : getClosestAncestorIn(nodeB, root, true);
1228 if (childA === childB) {
1229 // This shouldn't be possible
1230 throw module.createError("comparePoints got to case 4 and childA and childB are the same!");
1231 } else {
1232 n = root.firstChild;
1233 while (n) {
1234 if (n === childA) {
1235 return -1;
1236 } else if (n === childB) {
1237 return 1;
1239 n = n.nextSibling;
1245 /*----------------------------------------------------------------------------------------------------------------*/
1247 // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried
1248 var crashyTextNodes = false;
1250 function isBrokenNode(node) {
1251 var n;
1252 try {
1253 n = node.parentNode;
1254 return false;
1255 } catch (e) {
1256 return true;
1260 (function() {
1261 var el = document.createElement("b");
1262 el.innerHTML = "1";
1263 var textNode = el.firstChild;
1264 el.innerHTML = "<br />";
1265 crashyTextNodes = isBrokenNode(textNode);
1267 api.features.crashyTextNodes = crashyTextNodes;
1268 })();
1270 /*----------------------------------------------------------------------------------------------------------------*/
1272 function inspectNode(node) {
1273 if (!node) {
1274 return "[No node]";
1276 if (crashyTextNodes && isBrokenNode(node)) {
1277 return "[Broken node]";
1279 if (isCharacterDataNode(node)) {
1280 return '"' + node.data + '"';
1282 if (node.nodeType == 1) {
1283 var idAttr = node.id ? ' id="' + node.id + '"' : "";
1284 return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]";
1286 return node.nodeName;
1289 function fragmentFromNodeChildren(node) {
1290 var fragment = getDocument(node).createDocumentFragment(), child;
1291 while ( (child = node.firstChild) ) {
1292 fragment.appendChild(child);
1294 return fragment;
1297 var getComputedStyleProperty;
1298 if (typeof window.getComputedStyle != UNDEF) {
1299 getComputedStyleProperty = function(el, propName) {
1300 return getWindow(el).getComputedStyle(el, null)[propName];
1301 };
1302 } else if (typeof document.documentElement.currentStyle != UNDEF) {
1303 getComputedStyleProperty = function(el, propName) {
1304 return el.currentStyle ? el.currentStyle[propName] : "";
1305 };
1306 } else {
1307 module.fail("No means of obtaining computed style properties found");
1310 function createTestElement(doc, html, contentEditable) {
1311 var body = getBody(doc);
1312 var el = doc.createElement("div");
1313 el.contentEditable = "" + !!contentEditable;
1314 if (html) {
1315 el.innerHTML = html;
1318 // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292)
1319 var bodyFirstChild = body.firstChild;
1320 if (bodyFirstChild) {
1321 body.insertBefore(el, bodyFirstChild);
1322 } else {
1323 body.appendChild(el);
1326 return el;
1329 function removeNode(node) {
1330 return node.parentNode.removeChild(node);
1333 function NodeIterator(root) {
1334 this.root = root;
1335 this._next = root;
1338 NodeIterator.prototype = {
1339 _current: null,
1341 hasNext: function() {
1342 return !!this._next;
1343 },
1345 next: function() {
1346 var n = this._current = this._next;
1347 var child, next;
1348 if (this._current) {
1349 child = n.firstChild;
1350 if (child) {
1351 this._next = child;
1352 } else {
1353 next = null;
1354 while ((n !== this.root) && !(next = n.nextSibling)) {
1355 n = n.parentNode;
1357 this._next = next;
1360 return this._current;
1361 },
1363 detach: function() {
1364 this._current = this._next = this.root = null;
1366 };
1368 function createIterator(root) {
1369 return new NodeIterator(root);
1372 function DomPosition(node, offset) {
1373 this.node = node;
1374 this.offset = offset;
1377 DomPosition.prototype = {
1378 equals: function(pos) {
1379 return !!pos && this.node === pos.node && this.offset == pos.offset;
1380 },
1382 inspect: function() {
1383 return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]";
1384 },
1386 toString: function() {
1387 return this.inspect();
1389 };
1391 function DOMException(codeName) {
1392 this.code = this[codeName];
1393 this.codeName = codeName;
1394 this.message = "DOMException: " + this.codeName;
1397 DOMException.prototype = {
1398 INDEX_SIZE_ERR: 1,
1399 HIERARCHY_REQUEST_ERR: 3,
1400 WRONG_DOCUMENT_ERR: 4,
1401 NO_MODIFICATION_ALLOWED_ERR: 7,
1402 NOT_FOUND_ERR: 8,
1403 NOT_SUPPORTED_ERR: 9,
1404 INVALID_STATE_ERR: 11,
1405 INVALID_NODE_TYPE_ERR: 24
1406 };
1408 DOMException.prototype.toString = function() {
1409 return this.message;
1410 };
1412 api.dom = {
1413 arrayContains: arrayContains,
1414 isHtmlNamespace: isHtmlNamespace,
1415 parentElement: parentElement,
1416 getNodeIndex: getNodeIndex,
1417 getNodeLength: getNodeLength,
1418 getCommonAncestor: getCommonAncestor,
1419 isAncestorOf: isAncestorOf,
1420 isOrIsAncestorOf: isOrIsAncestorOf,
1421 getClosestAncestorIn: getClosestAncestorIn,
1422 isCharacterDataNode: isCharacterDataNode,
1423 isTextOrCommentNode: isTextOrCommentNode,
1424 insertAfter: insertAfter,
1425 splitDataNode: splitDataNode,
1426 getDocument: getDocument,
1427 getWindow: getWindow,
1428 getIframeWindow: getIframeWindow,
1429 getIframeDocument: getIframeDocument,
1430 getBody: getBody,
1431 isWindow: isWindow,
1432 getContentDocument: getContentDocument,
1433 getRootContainer: getRootContainer,
1434 comparePoints: comparePoints,
1435 isBrokenNode: isBrokenNode,
1436 inspectNode: inspectNode,
1437 getComputedStyleProperty: getComputedStyleProperty,
1438 createTestElement: createTestElement,
1439 removeNode: removeNode,
1440 fragmentFromNodeChildren: fragmentFromNodeChildren,
1441 createIterator: createIterator,
1442 DomPosition: DomPosition
1443 };
1445 api.DOMException = DOMException;
1446 });
1448 /*----------------------------------------------------------------------------------------------------------------*/
1450 // Pure JavaScript implementation of DOM Range
1451 api.createCoreModule("DomRange", ["DomUtil"], function(api, module) {
1452 var dom = api.dom;
1453 var util = api.util;
1454 var DomPosition = dom.DomPosition;
1455 var DOMException = api.DOMException;
1457 var isCharacterDataNode = dom.isCharacterDataNode;
1458 var getNodeIndex = dom.getNodeIndex;
1459 var isOrIsAncestorOf = dom.isOrIsAncestorOf;
1460 var getDocument = dom.getDocument;
1461 var comparePoints = dom.comparePoints;
1462 var splitDataNode = dom.splitDataNode;
1463 var getClosestAncestorIn = dom.getClosestAncestorIn;
1464 var getNodeLength = dom.getNodeLength;
1465 var arrayContains = dom.arrayContains;
1466 var getRootContainer = dom.getRootContainer;
1467 var crashyTextNodes = api.features.crashyTextNodes;
1469 var removeNode = dom.removeNode;
1471 /*----------------------------------------------------------------------------------------------------------------*/
1473 // Utility functions
1475 function isNonTextPartiallySelected(node, range) {
1476 return (node.nodeType != 3) &&
1477 (isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer));
1480 function getRangeDocument(range) {
1481 return range.document || getDocument(range.startContainer);
1484 function getRangeRoot(range) {
1485 return getRootContainer(range.startContainer);
1488 function getBoundaryBeforeNode(node) {
1489 return new DomPosition(node.parentNode, getNodeIndex(node));
1492 function getBoundaryAfterNode(node) {
1493 return new DomPosition(node.parentNode, getNodeIndex(node) + 1);
1496 function insertNodeAtPosition(node, n, o) {
1497 var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node;
1498 if (isCharacterDataNode(n)) {
1499 if (o == n.length) {
1500 dom.insertAfter(node, n);
1501 } else {
1502 n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o));
1504 } else if (o >= n.childNodes.length) {
1505 n.appendChild(node);
1506 } else {
1507 n.insertBefore(node, n.childNodes[o]);
1509 return firstNodeInserted;
1512 function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) {
1513 assertRangeValid(rangeA);
1514 assertRangeValid(rangeB);
1516 if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) {
1517 throw new DOMException("WRONG_DOCUMENT_ERR");
1520 var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset),
1521 endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset);
1523 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
1526 function cloneSubtree(iterator) {
1527 var partiallySelected;
1528 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1529 partiallySelected = iterator.isPartiallySelectedSubtree();
1530 node = node.cloneNode(!partiallySelected);
1531 if (partiallySelected) {
1532 subIterator = iterator.getSubtreeIterator();
1533 node.appendChild(cloneSubtree(subIterator));
1534 subIterator.detach();
1537 if (node.nodeType == 10) { // DocumentType
1538 throw new DOMException("HIERARCHY_REQUEST_ERR");
1540 frag.appendChild(node);
1542 return frag;
1545 function iterateSubtree(rangeIterator, func, iteratorState) {
1546 var it, n;
1547 iteratorState = iteratorState || { stop: false };
1548 for (var node, subRangeIterator; node = rangeIterator.next(); ) {
1549 if (rangeIterator.isPartiallySelectedSubtree()) {
1550 if (func(node) === false) {
1551 iteratorState.stop = true;
1552 return;
1553 } else {
1554 // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of
1555 // the node selected by the Range.
1556 subRangeIterator = rangeIterator.getSubtreeIterator();
1557 iterateSubtree(subRangeIterator, func, iteratorState);
1558 subRangeIterator.detach();
1559 if (iteratorState.stop) {
1560 return;
1563 } else {
1564 // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its
1565 // descendants
1566 it = dom.createIterator(node);
1567 while ( (n = it.next()) ) {
1568 if (func(n) === false) {
1569 iteratorState.stop = true;
1570 return;
1577 function deleteSubtree(iterator) {
1578 var subIterator;
1579 while (iterator.next()) {
1580 if (iterator.isPartiallySelectedSubtree()) {
1581 subIterator = iterator.getSubtreeIterator();
1582 deleteSubtree(subIterator);
1583 subIterator.detach();
1584 } else {
1585 iterator.remove();
1590 function extractSubtree(iterator) {
1591 for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next(); ) {
1593 if (iterator.isPartiallySelectedSubtree()) {
1594 node = node.cloneNode(false);
1595 subIterator = iterator.getSubtreeIterator();
1596 node.appendChild(extractSubtree(subIterator));
1597 subIterator.detach();
1598 } else {
1599 iterator.remove();
1601 if (node.nodeType == 10) { // DocumentType
1602 throw new DOMException("HIERARCHY_REQUEST_ERR");
1604 frag.appendChild(node);
1606 return frag;
1609 function getNodesInRange(range, nodeTypes, filter) {
1610 var filterNodeTypes = !!(nodeTypes && nodeTypes.length), regex;
1611 var filterExists = !!filter;
1612 if (filterNodeTypes) {
1613 regex = new RegExp("^(" + nodeTypes.join("|") + ")$");
1616 var nodes = [];
1617 iterateSubtree(new RangeIterator(range, false), function(node) {
1618 if (filterNodeTypes && !regex.test(node.nodeType)) {
1619 return;
1621 if (filterExists && !filter(node)) {
1622 return;
1624 // Don't include a boundary container if it is a character data node and the range does not contain any
1625 // of its character data. See issue 190.
1626 var sc = range.startContainer;
1627 if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) {
1628 return;
1631 var ec = range.endContainer;
1632 if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) {
1633 return;
1636 nodes.push(node);
1637 });
1638 return nodes;
1641 function inspect(range) {
1642 var name = (typeof range.getName == "undefined") ? "Range" : range.getName();
1643 return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " +
1644 dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]";
1647 /*----------------------------------------------------------------------------------------------------------------*/
1649 // RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange)
1651 function RangeIterator(range, clonePartiallySelectedTextNodes) {
1652 this.range = range;
1653 this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
1656 if (!range.collapsed) {
1657 this.sc = range.startContainer;
1658 this.so = range.startOffset;
1659 this.ec = range.endContainer;
1660 this.eo = range.endOffset;
1661 var root = range.commonAncestorContainer;
1663 if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
1664 this.isSingleCharacterDataNode = true;
1665 this._first = this._last = this._next = this.sc;
1666 } else {
1667 this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
1668 this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
1669 this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
1670 this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
1675 RangeIterator.prototype = {
1676 _current: null,
1677 _next: null,
1678 _first: null,
1679 _last: null,
1680 isSingleCharacterDataNode: false,
1682 reset: function() {
1683 this._current = null;
1684 this._next = this._first;
1685 },
1687 hasNext: function() {
1688 return !!this._next;
1689 },
1691 next: function() {
1692 // Move to next node
1693 var current = this._current = this._next;
1694 if (current) {
1695 this._next = (current !== this._last) ? current.nextSibling : null;
1697 // Check for partially selected text nodes
1698 if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) {
1699 if (current === this.ec) {
1700 (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo);
1702 if (this._current === this.sc) {
1703 (current = current.cloneNode(true)).deleteData(0, this.so);
1708 return current;
1709 },
1711 remove: function() {
1712 var current = this._current, start, end;
1714 if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) {
1715 start = (current === this.sc) ? this.so : 0;
1716 end = (current === this.ec) ? this.eo : current.length;
1717 if (start != end) {
1718 current.deleteData(start, end - start);
1720 } else {
1721 if (current.parentNode) {
1722 removeNode(current);
1723 } else {
1726 },
1728 // Checks if the current node is partially selected
1729 isPartiallySelectedSubtree: function() {
1730 var current = this._current;
1731 return isNonTextPartiallySelected(current, this.range);
1732 },
1734 getSubtreeIterator: function() {
1735 var subRange;
1736 if (this.isSingleCharacterDataNode) {
1737 subRange = this.range.cloneRange();
1738 subRange.collapse(false);
1739 } else {
1740 subRange = new Range(getRangeDocument(this.range));
1741 var current = this._current;
1742 var startContainer = current, startOffset = 0, endContainer = current, endOffset = getNodeLength(current);
1744 if (isOrIsAncestorOf(current, this.sc)) {
1745 startContainer = this.sc;
1746 startOffset = this.so;
1748 if (isOrIsAncestorOf(current, this.ec)) {
1749 endContainer = this.ec;
1750 endOffset = this.eo;
1753 updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset);
1755 return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes);
1756 },
1758 detach: function() {
1759 this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null;
1761 };
1763 /*----------------------------------------------------------------------------------------------------------------*/
1765 var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10];
1766 var rootContainerNodeTypes = [2, 9, 11];
1767 var readonlyNodeTypes = [5, 6, 10, 12];
1768 var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11];
1769 var surroundNodeTypes = [1, 3, 4, 5, 7, 8];
1771 function createAncestorFinder(nodeTypes) {
1772 return function(node, selfIsAncestor) {
1773 var t, n = selfIsAncestor ? node : node.parentNode;
1774 while (n) {
1775 t = n.nodeType;
1776 if (arrayContains(nodeTypes, t)) {
1777 return n;
1779 n = n.parentNode;
1781 return null;
1782 };
1785 var getDocumentOrFragmentContainer = createAncestorFinder( [9, 11] );
1786 var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes);
1787 var getDocTypeNotationEntityAncestor = createAncestorFinder( [6, 10, 12] );
1789 function assertNoDocTypeNotationEntityAncestor(node, allowSelf) {
1790 if (getDocTypeNotationEntityAncestor(node, allowSelf)) {
1791 throw new DOMException("INVALID_NODE_TYPE_ERR");
1795 function assertValidNodeType(node, invalidTypes) {
1796 if (!arrayContains(invalidTypes, node.nodeType)) {
1797 throw new DOMException("INVALID_NODE_TYPE_ERR");
1801 function assertValidOffset(node, offset) {
1802 if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) {
1803 throw new DOMException("INDEX_SIZE_ERR");
1807 function assertSameDocumentOrFragment(node1, node2) {
1808 if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) {
1809 throw new DOMException("WRONG_DOCUMENT_ERR");
1813 function assertNodeNotReadOnly(node) {
1814 if (getReadonlyAncestor(node, true)) {
1815 throw new DOMException("NO_MODIFICATION_ALLOWED_ERR");
1819 function assertNode(node, codeName) {
1820 if (!node) {
1821 throw new DOMException(codeName);
1825 function isValidOffset(node, offset) {
1826 return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length);
1829 function isRangeValid(range) {
1830 return (!!range.startContainer && !!range.endContainer &&
1831 !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) &&
1832 getRootContainer(range.startContainer) == getRootContainer(range.endContainer) &&
1833 isValidOffset(range.startContainer, range.startOffset) &&
1834 isValidOffset(range.endContainer, range.endOffset));
1837 function assertRangeValid(range) {
1838 if (!isRangeValid(range)) {
1839 throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")");
1843 /*----------------------------------------------------------------------------------------------------------------*/
1845 // Test the browser's innerHTML support to decide how to implement createContextualFragment
1846 var styleEl = document.createElement("style");
1847 var htmlParsingConforms = false;
1848 try {
1849 styleEl.innerHTML = "<b>x</b>";
1850 htmlParsingConforms = (styleEl.firstChild.nodeType == 3); // Opera incorrectly creates an element node
1851 } catch (e) {
1852 // IE 6 and 7 throw
1855 api.features.htmlParsingConforms = htmlParsingConforms;
1857 var createContextualFragment = htmlParsingConforms ?
1859 // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See
1860 // discussion and base code for this implementation at issue 67.
1861 // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface
1862 // Thanks to Aleks Williams.
1863 function(fragmentStr) {
1864 // "Let node the context object's start's node."
1865 var node = this.startContainer;
1866 var doc = getDocument(node);
1868 // "If the context object's start's node is null, raise an INVALID_STATE_ERR
1869 // exception and abort these steps."
1870 if (!node) {
1871 throw new DOMException("INVALID_STATE_ERR");
1874 // "Let element be as follows, depending on node's interface:"
1875 // Document, Document Fragment: null
1876 var el = null;
1878 // "Element: node"
1879 if (node.nodeType == 1) {
1880 el = node;
1882 // "Text, Comment: node's parentElement"
1883 } else if (isCharacterDataNode(node)) {
1884 el = dom.parentElement(node);
1887 // "If either element is null or element's ownerDocument is an HTML document
1888 // and element's local name is "html" and element's namespace is the HTML
1889 // namespace"
1890 if (el === null || (
1891 el.nodeName == "HTML" &&
1892 dom.isHtmlNamespace(getDocument(el).documentElement) &&
1893 dom.isHtmlNamespace(el)
1894 )) {
1896 // "let element be a new Element with "body" as its local name and the HTML
1897 // namespace as its namespace.""
1898 el = doc.createElement("body");
1899 } else {
1900 el = el.cloneNode(false);
1903 // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm."
1904 // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm."
1905 // "In either case, the algorithm must be invoked with fragment as the input
1906 // and element as the context element."
1907 el.innerHTML = fragmentStr;
1909 // "If this raises an exception, then abort these steps. Otherwise, let new
1910 // children be the nodes returned."
1912 // "Let fragment be a new DocumentFragment."
1913 // "Append all new children to fragment."
1914 // "Return fragment."
1915 return dom.fragmentFromNodeChildren(el);
1916 } :
1918 // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that
1919 // previous versions of Rangy used (with the exception of using a body element rather than a div)
1920 function(fragmentStr) {
1921 var doc = getRangeDocument(this);
1922 var el = doc.createElement("body");
1923 el.innerHTML = fragmentStr;
1925 return dom.fragmentFromNodeChildren(el);
1926 };
1928 function splitRangeBoundaries(range, positionsToPreserve) {
1929 assertRangeValid(range);
1931 var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
1932 var startEndSame = (sc === ec);
1934 if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
1935 splitDataNode(ec, eo, positionsToPreserve);
1938 if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
1939 sc = splitDataNode(sc, so, positionsToPreserve);
1940 if (startEndSame) {
1941 eo -= so;
1942 ec = sc;
1943 } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
1944 eo++;
1946 so = 0;
1948 range.setStartAndEnd(sc, so, ec, eo);
1951 function rangeToHtml(range) {
1952 assertRangeValid(range);
1953 var container = range.commonAncestorContainer.parentNode.cloneNode(false);
1954 container.appendChild( range.cloneContents() );
1955 return container.innerHTML;
1958 /*----------------------------------------------------------------------------------------------------------------*/
1960 var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed",
1961 "commonAncestorContainer"];
1963 var s2s = 0, s2e = 1, e2e = 2, e2s = 3;
1964 var n_b = 0, n_a = 1, n_b_a = 2, n_i = 3;
1966 util.extend(api.rangePrototype, {
1967 compareBoundaryPoints: function(how, range) {
1968 assertRangeValid(this);
1969 assertSameDocumentOrFragment(this.startContainer, range.startContainer);
1971 var nodeA, offsetA, nodeB, offsetB;
1972 var prefixA = (how == e2s || how == s2s) ? "start" : "end";
1973 var prefixB = (how == s2e || how == s2s) ? "start" : "end";
1974 nodeA = this[prefixA + "Container"];
1975 offsetA = this[prefixA + "Offset"];
1976 nodeB = range[prefixB + "Container"];
1977 offsetB = range[prefixB + "Offset"];
1978 return comparePoints(nodeA, offsetA, nodeB, offsetB);
1979 },
1981 insertNode: function(node) {
1982 assertRangeValid(this);
1983 assertValidNodeType(node, insertableNodeTypes);
1984 assertNodeNotReadOnly(this.startContainer);
1986 if (isOrIsAncestorOf(node, this.startContainer)) {
1987 throw new DOMException("HIERARCHY_REQUEST_ERR");
1990 // No check for whether the container of the start of the Range is of a type that does not allow
1991 // children of the type of node: the browser's DOM implementation should do this for us when we attempt
1992 // to add the node
1994 var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset);
1995 this.setStartBefore(firstNodeInserted);
1996 },
1998 cloneContents: function() {
1999 assertRangeValid(this);
2001 var clone, frag;
2002 if (this.collapsed) {
2003 return getRangeDocument(this).createDocumentFragment();
2004 } else {
2005 if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) {
2006 clone = this.startContainer.cloneNode(true);
2007 clone.data = clone.data.slice(this.startOffset, this.endOffset);
2008 frag = getRangeDocument(this).createDocumentFragment();
2009 frag.appendChild(clone);
2010 return frag;
2011 } else {
2012 var iterator = new RangeIterator(this, true);
2013 clone = cloneSubtree(iterator);
2014 iterator.detach();
2016 return clone;
2018 },
2020 canSurroundContents: function() {
2021 assertRangeValid(this);
2022 assertNodeNotReadOnly(this.startContainer);
2023 assertNodeNotReadOnly(this.endContainer);
2025 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
2026 // no non-text nodes.
2027 var iterator = new RangeIterator(this, true);
2028 var boundariesInvalid = (iterator._first && (isNonTextPartiallySelected(iterator._first, this)) ||
2029 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
2030 iterator.detach();
2031 return !boundariesInvalid;
2032 },
2034 surroundContents: function(node) {
2035 assertValidNodeType(node, surroundNodeTypes);
2037 if (!this.canSurroundContents()) {
2038 throw new DOMException("INVALID_STATE_ERR");
2041 // Extract the contents
2042 var content = this.extractContents();
2044 // Clear the children of the node
2045 if (node.hasChildNodes()) {
2046 while (node.lastChild) {
2047 node.removeChild(node.lastChild);
2051 // Insert the new node and add the extracted contents
2052 insertNodeAtPosition(node, this.startContainer, this.startOffset);
2053 node.appendChild(content);
2055 this.selectNode(node);
2056 },
2058 cloneRange: function() {
2059 assertRangeValid(this);
2060 var range = new Range(getRangeDocument(this));
2061 var i = rangeProperties.length, prop;
2062 while (i--) {
2063 prop = rangeProperties[i];
2064 range[prop] = this[prop];
2066 return range;
2067 },
2069 toString: function() {
2070 assertRangeValid(this);
2071 var sc = this.startContainer;
2072 if (sc === this.endContainer && isCharacterDataNode(sc)) {
2073 return (sc.nodeType == 3 || sc.nodeType == 4) ? sc.data.slice(this.startOffset, this.endOffset) : "";
2074 } else {
2075 var textParts = [], iterator = new RangeIterator(this, true);
2076 iterateSubtree(iterator, function(node) {
2077 // Accept only text or CDATA nodes, not comments
2078 if (node.nodeType == 3 || node.nodeType == 4) {
2079 textParts.push(node.data);
2081 });
2082 iterator.detach();
2083 return textParts.join("");
2085 },
2087 // The methods below are all non-standard. The following batch were introduced by Mozilla but have since
2088 // been removed from Mozilla.
2090 compareNode: function(node) {
2091 assertRangeValid(this);
2093 var parent = node.parentNode;
2094 var nodeIndex = getNodeIndex(node);
2096 if (!parent) {
2097 throw new DOMException("NOT_FOUND_ERR");
2100 var startComparison = this.comparePoint(parent, nodeIndex),
2101 endComparison = this.comparePoint(parent, nodeIndex + 1);
2103 if (startComparison < 0) { // Node starts before
2104 return (endComparison > 0) ? n_b_a : n_b;
2105 } else {
2106 return (endComparison > 0) ? n_a : n_i;
2108 },
2110 comparePoint: function(node, offset) {
2111 assertRangeValid(this);
2112 assertNode(node, "HIERARCHY_REQUEST_ERR");
2113 assertSameDocumentOrFragment(node, this.startContainer);
2115 if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) {
2116 return -1;
2117 } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) {
2118 return 1;
2120 return 0;
2121 },
2123 createContextualFragment: createContextualFragment,
2125 toHtml: function() {
2126 return rangeToHtml(this);
2127 },
2129 // touchingIsIntersecting determines whether this method considers a node that borders a range intersects
2130 // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default)
2131 intersectsNode: function(node, touchingIsIntersecting) {
2132 assertRangeValid(this);
2133 if (getRootContainer(node) != getRangeRoot(this)) {
2134 return false;
2137 var parent = node.parentNode, offset = getNodeIndex(node);
2138 if (!parent) {
2139 return true;
2142 var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset),
2143 endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset);
2145 return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0;
2146 },
2148 isPointInRange: function(node, offset) {
2149 assertRangeValid(this);
2150 assertNode(node, "HIERARCHY_REQUEST_ERR");
2151 assertSameDocumentOrFragment(node, this.startContainer);
2153 return (comparePoints(node, offset, this.startContainer, this.startOffset) >= 0) &&
2154 (comparePoints(node, offset, this.endContainer, this.endOffset) <= 0);
2155 },
2157 // The methods below are non-standard and invented by me.
2159 // Sharing a boundary start-to-end or end-to-start does not count as intersection.
2160 intersectsRange: function(range) {
2161 return rangesIntersect(this, range, false);
2162 },
2164 // Sharing a boundary start-to-end or end-to-start does count as intersection.
2165 intersectsOrTouchesRange: function(range) {
2166 return rangesIntersect(this, range, true);
2167 },
2169 intersection: function(range) {
2170 if (this.intersectsRange(range)) {
2171 var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset),
2172 endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset);
2174 var intersectionRange = this.cloneRange();
2175 if (startComparison == -1) {
2176 intersectionRange.setStart(range.startContainer, range.startOffset);
2178 if (endComparison == 1) {
2179 intersectionRange.setEnd(range.endContainer, range.endOffset);
2181 return intersectionRange;
2183 return null;
2184 },
2186 union: function(range) {
2187 if (this.intersectsOrTouchesRange(range)) {
2188 var unionRange = this.cloneRange();
2189 if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) {
2190 unionRange.setStart(range.startContainer, range.startOffset);
2192 if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) {
2193 unionRange.setEnd(range.endContainer, range.endOffset);
2195 return unionRange;
2196 } else {
2197 throw new DOMException("Ranges do not intersect");
2199 },
2201 containsNode: function(node, allowPartial) {
2202 if (allowPartial) {
2203 return this.intersectsNode(node, false);
2204 } else {
2205 return this.compareNode(node) == n_i;
2207 },
2209 containsNodeContents: function(node) {
2210 return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0;
2211 },
2213 containsRange: function(range) {
2214 var intersection = this.intersection(range);
2215 return intersection !== null && range.equals(intersection);
2216 },
2218 containsNodeText: function(node) {
2219 var nodeRange = this.cloneRange();
2220 nodeRange.selectNode(node);
2221 var textNodes = nodeRange.getNodes([3]);
2222 if (textNodes.length > 0) {
2223 nodeRange.setStart(textNodes[0], 0);
2224 var lastTextNode = textNodes.pop();
2225 nodeRange.setEnd(lastTextNode, lastTextNode.length);
2226 return this.containsRange(nodeRange);
2227 } else {
2228 return this.containsNodeContents(node);
2230 },
2232 getNodes: function(nodeTypes, filter) {
2233 assertRangeValid(this);
2234 return getNodesInRange(this, nodeTypes, filter);
2235 },
2237 getDocument: function() {
2238 return getRangeDocument(this);
2239 },
2241 collapseBefore: function(node) {
2242 this.setEndBefore(node);
2243 this.collapse(false);
2244 },
2246 collapseAfter: function(node) {
2247 this.setStartAfter(node);
2248 this.collapse(true);
2249 },
2251 getBookmark: function(containerNode) {
2252 var doc = getRangeDocument(this);
2253 var preSelectionRange = api.createRange(doc);
2254 containerNode = containerNode || dom.getBody(doc);
2255 preSelectionRange.selectNodeContents(containerNode);
2256 var range = this.intersection(preSelectionRange);
2257 var start = 0, end = 0;
2258 if (range) {
2259 preSelectionRange.setEnd(range.startContainer, range.startOffset);
2260 start = preSelectionRange.toString().length;
2261 end = start + range.toString().length;
2264 return {
2265 start: start,
2266 end: end,
2267 containerNode: containerNode
2268 };
2269 },
2271 moveToBookmark: function(bookmark) {
2272 var containerNode = bookmark.containerNode;
2273 var charIndex = 0;
2274 this.setStart(containerNode, 0);
2275 this.collapse(true);
2276 var nodeStack = [containerNode], node, foundStart = false, stop = false;
2277 var nextCharIndex, i, childNodes;
2279 while (!stop && (node = nodeStack.pop())) {
2280 if (node.nodeType == 3) {
2281 nextCharIndex = charIndex + node.length;
2282 if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) {
2283 this.setStart(node, bookmark.start - charIndex);
2284 foundStart = true;
2286 if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) {
2287 this.setEnd(node, bookmark.end - charIndex);
2288 stop = true;
2290 charIndex = nextCharIndex;
2291 } else {
2292 childNodes = node.childNodes;
2293 i = childNodes.length;
2294 while (i--) {
2295 nodeStack.push(childNodes[i]);
2299 },
2301 getName: function() {
2302 return "DomRange";
2303 },
2305 equals: function(range) {
2306 return Range.rangesEqual(this, range);
2307 },
2309 isValid: function() {
2310 return isRangeValid(this);
2311 },
2313 inspect: function() {
2314 return inspect(this);
2315 },
2317 detach: function() {
2318 // In DOM4, detach() is now a no-op.
2320 });
2322 function copyComparisonConstantsToObject(obj) {
2323 obj.START_TO_START = s2s;
2324 obj.START_TO_END = s2e;
2325 obj.END_TO_END = e2e;
2326 obj.END_TO_START = e2s;
2328 obj.NODE_BEFORE = n_b;
2329 obj.NODE_AFTER = n_a;
2330 obj.NODE_BEFORE_AND_AFTER = n_b_a;
2331 obj.NODE_INSIDE = n_i;
2334 function copyComparisonConstants(constructor) {
2335 copyComparisonConstantsToObject(constructor);
2336 copyComparisonConstantsToObject(constructor.prototype);
2339 function createRangeContentRemover(remover, boundaryUpdater) {
2340 return function() {
2341 assertRangeValid(this);
2343 var sc = this.startContainer, so = this.startOffset, root = this.commonAncestorContainer;
2345 var iterator = new RangeIterator(this, true);
2347 // Work out where to position the range after content removal
2348 var node, boundary;
2349 if (sc !== root) {
2350 node = getClosestAncestorIn(sc, root, true);
2351 boundary = getBoundaryAfterNode(node);
2352 sc = boundary.node;
2353 so = boundary.offset;
2356 // Check none of the range is read-only
2357 iterateSubtree(iterator, assertNodeNotReadOnly);
2359 iterator.reset();
2361 // Remove the content
2362 var returnValue = remover(iterator);
2363 iterator.detach();
2365 // Move to the new position
2366 boundaryUpdater(this, sc, so, sc, so);
2368 return returnValue;
2369 };
2372 function createPrototypeRange(constructor, boundaryUpdater) {
2373 function createBeforeAfterNodeSetter(isBefore, isStart) {
2374 return function(node) {
2375 assertValidNodeType(node, beforeAfterNodeTypes);
2376 assertValidNodeType(getRootContainer(node), rootContainerNodeTypes);
2378 var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node);
2379 (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset);
2380 };
2383 function setRangeStart(range, node, offset) {
2384 var ec = range.endContainer, eo = range.endOffset;
2385 if (node !== range.startContainer || offset !== range.startOffset) {
2386 // Check the root containers of the range and the new boundary, and also check whether the new boundary
2387 // is after the current end. In either case, collapse the range to the new position
2388 if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) {
2389 ec = node;
2390 eo = offset;
2392 boundaryUpdater(range, node, offset, ec, eo);
2396 function setRangeEnd(range, node, offset) {
2397 var sc = range.startContainer, so = range.startOffset;
2398 if (node !== range.endContainer || offset !== range.endOffset) {
2399 // Check the root containers of the range and the new boundary, and also check whether the new boundary
2400 // is after the current end. In either case, collapse the range to the new position
2401 if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) {
2402 sc = node;
2403 so = offset;
2405 boundaryUpdater(range, sc, so, node, offset);
2409 // Set up inheritance
2410 var F = function() {};
2411 F.prototype = api.rangePrototype;
2412 constructor.prototype = new F();
2414 util.extend(constructor.prototype, {
2415 setStart: function(node, offset) {
2416 assertNoDocTypeNotationEntityAncestor(node, true);
2417 assertValidOffset(node, offset);
2419 setRangeStart(this, node, offset);
2420 },
2422 setEnd: function(node, offset) {
2423 assertNoDocTypeNotationEntityAncestor(node, true);
2424 assertValidOffset(node, offset);
2426 setRangeEnd(this, node, offset);
2427 },
2429 /**
2430 * Convenience method to set a range's start and end boundaries. Overloaded as follows:
2431 * - Two parameters (node, offset) creates a collapsed range at that position
2432 * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at
2433 * startOffset and ending at endOffset
2434 * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in
2435 * startNode and ending at endOffset in endNode
2436 */
2437 setStartAndEnd: function() {
2438 var args = arguments;
2439 var sc = args[0], so = args[1], ec = sc, eo = so;
2441 switch (args.length) {
2442 case 3:
2443 eo = args[2];
2444 break;
2445 case 4:
2446 ec = args[2];
2447 eo = args[3];
2448 break;
2451 boundaryUpdater(this, sc, so, ec, eo);
2452 },
2454 setBoundary: function(node, offset, isStart) {
2455 this["set" + (isStart ? "Start" : "End")](node, offset);
2456 },
2458 setStartBefore: createBeforeAfterNodeSetter(true, true),
2459 setStartAfter: createBeforeAfterNodeSetter(false, true),
2460 setEndBefore: createBeforeAfterNodeSetter(true, false),
2461 setEndAfter: createBeforeAfterNodeSetter(false, false),
2463 collapse: function(isStart) {
2464 assertRangeValid(this);
2465 if (isStart) {
2466 boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset);
2467 } else {
2468 boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset);
2470 },
2472 selectNodeContents: function(node) {
2473 assertNoDocTypeNotationEntityAncestor(node, true);
2475 boundaryUpdater(this, node, 0, node, getNodeLength(node));
2476 },
2478 selectNode: function(node) {
2479 assertNoDocTypeNotationEntityAncestor(node, false);
2480 assertValidNodeType(node, beforeAfterNodeTypes);
2482 var start = getBoundaryBeforeNode(node), end = getBoundaryAfterNode(node);
2483 boundaryUpdater(this, start.node, start.offset, end.node, end.offset);
2484 },
2486 extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater),
2488 deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater),
2490 canSurroundContents: function() {
2491 assertRangeValid(this);
2492 assertNodeNotReadOnly(this.startContainer);
2493 assertNodeNotReadOnly(this.endContainer);
2495 // Check if the contents can be surrounded. Specifically, this means whether the range partially selects
2496 // no non-text nodes.
2497 var iterator = new RangeIterator(this, true);
2498 var boundariesInvalid = (iterator._first && isNonTextPartiallySelected(iterator._first, this) ||
2499 (iterator._last && isNonTextPartiallySelected(iterator._last, this)));
2500 iterator.detach();
2501 return !boundariesInvalid;
2502 },
2504 splitBoundaries: function() {
2505 splitRangeBoundaries(this);
2506 },
2508 splitBoundariesPreservingPositions: function(positionsToPreserve) {
2509 splitRangeBoundaries(this, positionsToPreserve);
2510 },
2512 normalizeBoundaries: function() {
2513 assertRangeValid(this);
2515 var sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
2517 var mergeForward = function(node) {
2518 var sibling = node.nextSibling;
2519 if (sibling && sibling.nodeType == node.nodeType) {
2520 ec = node;
2521 eo = node.length;
2522 node.appendData(sibling.data);
2523 removeNode(sibling);
2525 };
2527 var mergeBackward = function(node) {
2528 var sibling = node.previousSibling;
2529 if (sibling && sibling.nodeType == node.nodeType) {
2530 sc = node;
2531 var nodeLength = node.length;
2532 so = sibling.length;
2533 node.insertData(0, sibling.data);
2534 removeNode(sibling);
2535 if (sc == ec) {
2536 eo += so;
2537 ec = sc;
2538 } else if (ec == node.parentNode) {
2539 var nodeIndex = getNodeIndex(node);
2540 if (eo == nodeIndex) {
2541 ec = node;
2542 eo = nodeLength;
2543 } else if (eo > nodeIndex) {
2544 eo--;
2548 };
2550 var normalizeStart = true;
2551 var sibling;
2553 if (isCharacterDataNode(ec)) {
2554 if (eo == ec.length) {
2555 mergeForward(ec);
2556 } else if (eo == 0) {
2557 sibling = ec.previousSibling;
2558 if (sibling && sibling.nodeType == ec.nodeType) {
2559 eo = sibling.length;
2560 if (sc == ec) {
2561 normalizeStart = false;
2563 sibling.appendData(ec.data);
2564 removeNode(ec);
2565 ec = sibling;
2568 } else {
2569 if (eo > 0) {
2570 var endNode = ec.childNodes[eo - 1];
2571 if (endNode && isCharacterDataNode(endNode)) {
2572 mergeForward(endNode);
2575 normalizeStart = !this.collapsed;
2578 if (normalizeStart) {
2579 if (isCharacterDataNode(sc)) {
2580 if (so == 0) {
2581 mergeBackward(sc);
2582 } else if (so == sc.length) {
2583 sibling = sc.nextSibling;
2584 if (sibling && sibling.nodeType == sc.nodeType) {
2585 if (ec == sibling) {
2586 ec = sc;
2587 eo += sc.length;
2589 sc.appendData(sibling.data);
2590 removeNode(sibling);
2593 } else {
2594 if (so < sc.childNodes.length) {
2595 var startNode = sc.childNodes[so];
2596 if (startNode && isCharacterDataNode(startNode)) {
2597 mergeBackward(startNode);
2601 } else {
2602 sc = ec;
2603 so = eo;
2606 boundaryUpdater(this, sc, so, ec, eo);
2607 },
2609 collapseToPoint: function(node, offset) {
2610 assertNoDocTypeNotationEntityAncestor(node, true);
2611 assertValidOffset(node, offset);
2612 this.setStartAndEnd(node, offset);
2614 });
2616 copyComparisonConstants(constructor);
2619 /*----------------------------------------------------------------------------------------------------------------*/
2621 // Updates commonAncestorContainer and collapsed after boundary change
2622 function updateCollapsedAndCommonAncestor(range) {
2623 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2624 range.commonAncestorContainer = range.collapsed ?
2625 range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer);
2628 function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) {
2629 range.startContainer = startContainer;
2630 range.startOffset = startOffset;
2631 range.endContainer = endContainer;
2632 range.endOffset = endOffset;
2633 range.document = dom.getDocument(startContainer);
2635 updateCollapsedAndCommonAncestor(range);
2638 function Range(doc) {
2639 this.startContainer = doc;
2640 this.startOffset = 0;
2641 this.endContainer = doc;
2642 this.endOffset = 0;
2643 this.document = doc;
2644 updateCollapsedAndCommonAncestor(this);
2647 createPrototypeRange(Range, updateBoundaries);
2649 util.extend(Range, {
2650 rangeProperties: rangeProperties,
2651 RangeIterator: RangeIterator,
2652 copyComparisonConstants: copyComparisonConstants,
2653 createPrototypeRange: createPrototypeRange,
2654 inspect: inspect,
2655 toHtml: rangeToHtml,
2656 getRangeDocument: getRangeDocument,
2657 rangesEqual: function(r1, r2) {
2658 return r1.startContainer === r2.startContainer &&
2659 r1.startOffset === r2.startOffset &&
2660 r1.endContainer === r2.endContainer &&
2661 r1.endOffset === r2.endOffset;
2663 });
2665 api.DomRange = Range;
2666 });
2668 /*----------------------------------------------------------------------------------------------------------------*/
2670 // Wrappers for the browser's native DOM Range and/or TextRange implementation
2671 api.createCoreModule("WrappedRange", ["DomRange"], function(api, module) {
2672 var WrappedRange, WrappedTextRange;
2673 var dom = api.dom;
2674 var util = api.util;
2675 var DomPosition = dom.DomPosition;
2676 var DomRange = api.DomRange;
2677 var getBody = dom.getBody;
2678 var getContentDocument = dom.getContentDocument;
2679 var isCharacterDataNode = dom.isCharacterDataNode;
2682 /*----------------------------------------------------------------------------------------------------------------*/
2684 if (api.features.implementsDomRange) {
2685 // This is a wrapper around the browser's native DOM Range. It has two aims:
2686 // - Provide workarounds for specific browser bugs
2687 // - provide convenient extensions, which are inherited from Rangy's DomRange
2689 (function() {
2690 var rangeProto;
2691 var rangeProperties = DomRange.rangeProperties;
2693 function updateRangeProperties(range) {
2694 var i = rangeProperties.length, prop;
2695 while (i--) {
2696 prop = rangeProperties[i];
2697 range[prop] = range.nativeRange[prop];
2699 // Fix for broken collapsed property in IE 9.
2700 range.collapsed = (range.startContainer === range.endContainer && range.startOffset === range.endOffset);
2703 function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) {
2704 var startMoved = (range.startContainer !== startContainer || range.startOffset != startOffset);
2705 var endMoved = (range.endContainer !== endContainer || range.endOffset != endOffset);
2706 var nativeRangeDifferent = !range.equals(range.nativeRange);
2708 // Always set both boundaries for the benefit of IE9 (see issue 35)
2709 if (startMoved || endMoved || nativeRangeDifferent) {
2710 range.setEnd(endContainer, endOffset);
2711 range.setStart(startContainer, startOffset);
2715 var createBeforeAfterNodeSetter;
2717 WrappedRange = function(range) {
2718 if (!range) {
2719 throw module.createError("WrappedRange: Range must be specified");
2721 this.nativeRange = range;
2722 updateRangeProperties(this);
2723 };
2725 DomRange.createPrototypeRange(WrappedRange, updateNativeRange);
2727 rangeProto = WrappedRange.prototype;
2729 rangeProto.selectNode = function(node) {
2730 this.nativeRange.selectNode(node);
2731 updateRangeProperties(this);
2732 };
2734 rangeProto.cloneContents = function() {
2735 return this.nativeRange.cloneContents();
2736 };
2738 // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect,
2739 // insertNode() is never delegated to the native range.
2741 rangeProto.surroundContents = function(node) {
2742 this.nativeRange.surroundContents(node);
2743 updateRangeProperties(this);
2744 };
2746 rangeProto.collapse = function(isStart) {
2747 this.nativeRange.collapse(isStart);
2748 updateRangeProperties(this);
2749 };
2751 rangeProto.cloneRange = function() {
2752 return new WrappedRange(this.nativeRange.cloneRange());
2753 };
2755 rangeProto.refresh = function() {
2756 updateRangeProperties(this);
2757 };
2759 rangeProto.toString = function() {
2760 return this.nativeRange.toString();
2761 };
2763 // Create test range and node for feature detection
2765 var testTextNode = document.createTextNode("test");
2766 getBody(document).appendChild(testTextNode);
2767 var range = document.createRange();
2769 /*--------------------------------------------------------------------------------------------------------*/
2771 // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and
2772 // correct for it
2774 range.setStart(testTextNode, 0);
2775 range.setEnd(testTextNode, 0);
2777 try {
2778 range.setStart(testTextNode, 1);
2780 rangeProto.setStart = function(node, offset) {
2781 this.nativeRange.setStart(node, offset);
2782 updateRangeProperties(this);
2783 };
2785 rangeProto.setEnd = function(node, offset) {
2786 this.nativeRange.setEnd(node, offset);
2787 updateRangeProperties(this);
2788 };
2790 createBeforeAfterNodeSetter = function(name) {
2791 return function(node) {
2792 this.nativeRange[name](node);
2793 updateRangeProperties(this);
2794 };
2795 };
2797 } catch(ex) {
2799 rangeProto.setStart = function(node, offset) {
2800 try {
2801 this.nativeRange.setStart(node, offset);
2802 } catch (ex) {
2803 this.nativeRange.setEnd(node, offset);
2804 this.nativeRange.setStart(node, offset);
2806 updateRangeProperties(this);
2807 };
2809 rangeProto.setEnd = function(node, offset) {
2810 try {
2811 this.nativeRange.setEnd(node, offset);
2812 } catch (ex) {
2813 this.nativeRange.setStart(node, offset);
2814 this.nativeRange.setEnd(node, offset);
2816 updateRangeProperties(this);
2817 };
2819 createBeforeAfterNodeSetter = function(name, oppositeName) {
2820 return function(node) {
2821 try {
2822 this.nativeRange[name](node);
2823 } catch (ex) {
2824 this.nativeRange[oppositeName](node);
2825 this.nativeRange[name](node);
2827 updateRangeProperties(this);
2828 };
2829 };
2832 rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore");
2833 rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter");
2834 rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore");
2835 rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter");
2837 /*--------------------------------------------------------------------------------------------------------*/
2839 // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing
2840 // whether the native implementation can be trusted
2841 rangeProto.selectNodeContents = function(node) {
2842 this.setStartAndEnd(node, 0, dom.getNodeLength(node));
2843 };
2845 /*--------------------------------------------------------------------------------------------------------*/
2847 // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for
2848 // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738
2850 range.selectNodeContents(testTextNode);
2851 range.setEnd(testTextNode, 3);
2853 var range2 = document.createRange();
2854 range2.selectNodeContents(testTextNode);
2855 range2.setEnd(testTextNode, 4);
2856 range2.setStart(testTextNode, 2);
2858 if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 &&
2859 range.compareBoundaryPoints(range.END_TO_START, range2) == 1) {
2860 // This is the wrong way round, so correct for it
2862 rangeProto.compareBoundaryPoints = function(type, range) {
2863 range = range.nativeRange || range;
2864 if (type == range.START_TO_END) {
2865 type = range.END_TO_START;
2866 } else if (type == range.END_TO_START) {
2867 type = range.START_TO_END;
2869 return this.nativeRange.compareBoundaryPoints(type, range);
2870 };
2871 } else {
2872 rangeProto.compareBoundaryPoints = function(type, range) {
2873 return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range);
2874 };
2877 /*--------------------------------------------------------------------------------------------------------*/
2879 // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107.
2881 var el = document.createElement("div");
2882 el.innerHTML = "123";
2883 var textNode = el.firstChild;
2884 var body = getBody(document);
2885 body.appendChild(el);
2887 range.setStart(textNode, 1);
2888 range.setEnd(textNode, 2);
2889 range.deleteContents();
2891 if (textNode.data == "13") {
2892 // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and
2893 // extractContents()
2894 rangeProto.deleteContents = function() {
2895 this.nativeRange.deleteContents();
2896 updateRangeProperties(this);
2897 };
2899 rangeProto.extractContents = function() {
2900 var frag = this.nativeRange.extractContents();
2901 updateRangeProperties(this);
2902 return frag;
2903 };
2904 } else {
2907 body.removeChild(el);
2908 body = null;
2910 /*--------------------------------------------------------------------------------------------------------*/
2912 // Test for existence of createContextualFragment and delegate to it if it exists
2913 if (util.isHostMethod(range, "createContextualFragment")) {
2914 rangeProto.createContextualFragment = function(fragmentStr) {
2915 return this.nativeRange.createContextualFragment(fragmentStr);
2916 };
2919 /*--------------------------------------------------------------------------------------------------------*/
2921 // Clean up
2922 getBody(document).removeChild(testTextNode);
2924 rangeProto.getName = function() {
2925 return "WrappedRange";
2926 };
2928 api.WrappedRange = WrappedRange;
2930 api.createNativeRange = function(doc) {
2931 doc = getContentDocument(doc, module, "createNativeRange");
2932 return doc.createRange();
2933 };
2934 })();
2937 if (api.features.implementsTextRange) {
2938 /*
2939 This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement()
2940 method. For example, in the following (where pipes denote the selection boundaries):
2942 <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul>
2944 var range = document.selection.createRange();
2945 alert(range.parentElement().id); // Should alert "ul" but alerts "b"
2947 This method returns the common ancestor node of the following:
2948 - the parentElement() of the textRange
2949 - the parentElement() of the textRange after calling collapse(true)
2950 - the parentElement() of the textRange after calling collapse(false)
2951 */
2952 var getTextRangeContainerElement = function(textRange) {
2953 var parentEl = textRange.parentElement();
2954 var range = textRange.duplicate();
2955 range.collapse(true);
2956 var startEl = range.parentElement();
2957 range = textRange.duplicate();
2958 range.collapse(false);
2959 var endEl = range.parentElement();
2960 var startEndContainer = (startEl == endEl) ? startEl : dom.getCommonAncestor(startEl, endEl);
2962 return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer);
2963 };
2965 var textRangeIsCollapsed = function(textRange) {
2966 return textRange.compareEndPoints("StartToEnd", textRange) == 0;
2967 };
2969 // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started
2970 // out as an improved version of code found in Tim Cameron Ryan's IERange (http://code.google.com/p/ierange/)
2971 // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange
2972 // bugs, handling for inputs and images, plus optimizations.
2973 var getTextRangeBoundaryPosition = function(textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) {
2974 var workingRange = textRange.duplicate();
2975 workingRange.collapse(isStart);
2976 var containerElement = workingRange.parentElement();
2978 // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so
2979 // check for that
2980 if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) {
2981 containerElement = wholeRangeContainerElement;
2985 // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and
2986 // similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx
2987 if (!containerElement.canHaveHTML) {
2988 var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement));
2989 return {
2990 boundaryPosition: pos,
2991 nodeInfo: {
2992 nodeIndex: pos.offset,
2993 containerElement: pos.node
2995 };
2998 var workingNode = dom.getDocument(containerElement).createElement("span");
3000 // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5
3001 // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64
3002 if (workingNode.parentNode) {
3003 dom.removeNode(workingNode);
3006 var comparison, workingComparisonType = isStart ? "StartToStart" : "StartToEnd";
3007 var previousNode, nextNode, boundaryPosition, boundaryNode;
3008 var start = (startInfo && startInfo.containerElement == containerElement) ? startInfo.nodeIndex : 0;
3009 var childNodeCount = containerElement.childNodes.length;
3010 var end = childNodeCount;
3012 // Check end first. Code within the loop assumes that the endth child node of the container is definitely
3013 // after the range boundary.
3014 var nodeIndex = end;
3016 while (true) {
3017 if (nodeIndex == childNodeCount) {
3018 containerElement.appendChild(workingNode);
3019 } else {
3020 containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]);
3022 workingRange.moveToElementText(workingNode);
3023 comparison = workingRange.compareEndPoints(workingComparisonType, textRange);
3024 if (comparison == 0 || start == end) {
3025 break;
3026 } else if (comparison == -1) {
3027 if (end == start + 1) {
3028 // We know the endth child node is after the range boundary, so we must be done.
3029 break;
3030 } else {
3031 start = nodeIndex;
3033 } else {
3034 end = (end == start + 1) ? start : nodeIndex;
3036 nodeIndex = Math.floor((start + end) / 2);
3037 containerElement.removeChild(workingNode);
3041 // We've now reached or gone past the boundary of the text range we're interested in
3042 // so have identified the node we want
3043 boundaryNode = workingNode.nextSibling;
3045 if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) {
3046 // This is a character data node (text, comment, cdata). The working range is collapsed at the start of
3047 // the node containing the text range's boundary, so we move the end of the working range to the
3048 // boundary point and measure the length of its text to get the boundary's offset within the node.
3049 workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange);
3051 var offset;
3053 if (/[\r\n]/.test(boundaryNode.data)) {
3054 /*
3055 For the particular case of a boundary within a text node containing rendered line breaks (within a
3056 <pre> element, for example), we need a slightly complicated approach to get the boundary's offset in
3057 IE. The facts:
3059 - Each line break is represented as \r in the text node's data/nodeValue properties
3060 - Each line break is represented as \r\n in the TextRange's 'text' property
3061 - The 'text' property of the TextRange does not contain trailing line breaks
3063 To get round the problem presented by the final fact above, we can use the fact that TextRange's
3064 moveStart() and moveEnd() methods return the actual number of characters moved, which is not
3065 necessarily the same as the number of characters it was instructed to move. The simplest approach is
3066 to use this to store the characters moved when moving both the start and end of the range to the
3067 start of the document body and subtracting the start offset from the end offset (the
3068 "move-negative-gazillion" method). However, this is extremely slow when the document is large and
3069 the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
3070 the end of the document) has the same problem.
3072 Another approach that works is to use moveStart() to move the start boundary of the range up to the
3073 end boundary one character at a time and incrementing a counter with the value returned by the
3074 moveStart() call. However, the check for whether the start boundary has reached the end boundary is
3075 expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
3076 by the location of the range within the document).
3078 The approach used below is a hybrid of the two methods above. It uses the fact that a string
3079 containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
3080 be longer than the text of the TextRange, so the start of the range is moved that length initially
3081 and then a character at a time to make up for any trailing line breaks not contained in the 'text'
3082 property. This has good performance in most situations compared to the previous two methods.
3083 */
3084 var tempRange = workingRange.duplicate();
3085 var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length;
3087 offset = tempRange.moveStart("character", rangeLength);
3088 while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) {
3089 offset++;
3090 tempRange.moveStart("character", 1);
3092 } else {
3093 offset = workingRange.text.length;
3095 boundaryPosition = new DomPosition(boundaryNode, offset);
3096 } else {
3098 // If the boundary immediately follows a character data node and this is the end boundary, we should favour
3099 // a position within that, and likewise for a start boundary preceding a character data node
3100 previousNode = (isCollapsed || !isStart) && workingNode.previousSibling;
3101 nextNode = (isCollapsed || isStart) && workingNode.nextSibling;
3102 if (nextNode && isCharacterDataNode(nextNode)) {
3103 boundaryPosition = new DomPosition(nextNode, 0);
3104 } else if (previousNode && isCharacterDataNode(previousNode)) {
3105 boundaryPosition = new DomPosition(previousNode, previousNode.data.length);
3106 } else {
3107 boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode));
3111 // Clean up
3112 dom.removeNode(workingNode);
3114 return {
3115 boundaryPosition: boundaryPosition,
3116 nodeInfo: {
3117 nodeIndex: nodeIndex,
3118 containerElement: containerElement
3120 };
3121 };
3123 // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that
3124 // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange
3125 // (http://code.google.com/p/ierange/)
3126 var createBoundaryTextRange = function(boundaryPosition, isStart) {
3127 var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset;
3128 var doc = dom.getDocument(boundaryPosition.node);
3129 var workingNode, childNodes, workingRange = getBody(doc).createTextRange();
3130 var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node);
3132 if (nodeIsDataNode) {
3133 boundaryNode = boundaryPosition.node;
3134 boundaryParent = boundaryNode.parentNode;
3135 } else {
3136 childNodes = boundaryPosition.node.childNodes;
3137 boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null;
3138 boundaryParent = boundaryPosition.node;
3141 // Position the range immediately before the node containing the boundary
3142 workingNode = doc.createElement("span");
3144 // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
3145 // the element rather than immediately before or after it
3146 workingNode.innerHTML = "&#feff;";
3148 // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
3149 // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
3150 if (boundaryNode) {
3151 boundaryParent.insertBefore(workingNode, boundaryNode);
3152 } else {
3153 boundaryParent.appendChild(workingNode);
3156 workingRange.moveToElementText(workingNode);
3157 workingRange.collapse(!isStart);
3159 // Clean up
3160 boundaryParent.removeChild(workingNode);
3162 // Move the working range to the text offset, if required
3163 if (nodeIsDataNode) {
3164 workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset);
3167 return workingRange;
3168 };
3170 /*------------------------------------------------------------------------------------------------------------*/
3172 // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a
3173 // prototype
3175 WrappedTextRange = function(textRange) {
3176 this.textRange = textRange;
3177 this.refresh();
3178 };
3180 WrappedTextRange.prototype = new DomRange(document);
3182 WrappedTextRange.prototype.refresh = function() {
3183 var start, end, startBoundary;
3185 // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that.
3186 var rangeContainerElement = getTextRangeContainerElement(this.textRange);
3188 if (textRangeIsCollapsed(this.textRange)) {
3189 end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true,
3190 true).boundaryPosition;
3191 } else {
3192 startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false);
3193 start = startBoundary.boundaryPosition;
3195 // An optimization used here is that if the start and end boundaries have the same parent element, the
3196 // search scope for the end boundary can be limited to exclude the portion of the element that precedes
3197 // the start boundary
3198 end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false,
3199 startBoundary.nodeInfo).boundaryPosition;
3202 this.setStart(start.node, start.offset);
3203 this.setEnd(end.node, end.offset);
3204 };
3206 WrappedTextRange.prototype.getName = function() {
3207 return "WrappedTextRange";
3208 };
3210 DomRange.copyComparisonConstants(WrappedTextRange);
3212 var rangeToTextRange = function(range) {
3213 if (range.collapsed) {
3214 return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
3215 } else {
3216 var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true);
3217 var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false);
3218 var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange();
3219 textRange.setEndPoint("StartToStart", startRange);
3220 textRange.setEndPoint("EndToEnd", endRange);
3221 return textRange;
3223 };
3225 WrappedTextRange.rangeToTextRange = rangeToTextRange;
3227 WrappedTextRange.prototype.toTextRange = function() {
3228 return rangeToTextRange(this);
3229 };
3231 api.WrappedTextRange = WrappedTextRange;
3233 // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which
3234 // implementation to use by default.
3235 if (!api.features.implementsDomRange || api.config.preferTextRange) {
3236 // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work
3237 var globalObj = (function(f) { return f("return this;")(); })(Function);
3238 if (typeof globalObj.Range == "undefined") {
3239 globalObj.Range = WrappedTextRange;
3242 api.createNativeRange = function(doc) {
3243 doc = getContentDocument(doc, module, "createNativeRange");
3244 return getBody(doc).createTextRange();
3245 };
3247 api.WrappedRange = WrappedTextRange;
3251 api.createRange = function(doc) {
3252 doc = getContentDocument(doc, module, "createRange");
3253 return new api.WrappedRange(api.createNativeRange(doc));
3254 };
3256 api.createRangyRange = function(doc) {
3257 doc = getContentDocument(doc, module, "createRangyRange");
3258 return new DomRange(doc);
3259 };
3261 util.createAliasForDeprecatedMethod(api, "createIframeRange", "createRange");
3262 util.createAliasForDeprecatedMethod(api, "createIframeRangyRange", "createRangyRange");
3264 api.addShimListener(function(win) {
3265 var doc = win.document;
3266 if (typeof doc.createRange == "undefined") {
3267 doc.createRange = function() {
3268 return api.createRange(doc);
3269 };
3271 doc = win = null;
3272 });
3273 });
3275 /*----------------------------------------------------------------------------------------------------------------*/
3277 // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification
3278 // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections)
3279 api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) {
3280 api.config.checkSelectionRanges = true;
3282 var BOOLEAN = "boolean";
3283 var NUMBER = "number";
3284 var dom = api.dom;
3285 var util = api.util;
3286 var isHostMethod = util.isHostMethod;
3287 var DomRange = api.DomRange;
3288 var WrappedRange = api.WrappedRange;
3289 var DOMException = api.DOMException;
3290 var DomPosition = dom.DomPosition;
3291 var getNativeSelection;
3292 var selectionIsCollapsed;
3293 var features = api.features;
3294 var CONTROL = "Control";
3295 var getDocument = dom.getDocument;
3296 var getBody = dom.getBody;
3297 var rangesEqual = DomRange.rangesEqual;
3300 // Utility function to support direction parameters in the API that may be a string ("backward", "backwards",
3301 // "forward" or "forwards") or a Boolean (true for backwards).
3302 function isDirectionBackward(dir) {
3303 return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir;
3306 function getWindow(win, methodName) {
3307 if (!win) {
3308 return window;
3309 } else if (dom.isWindow(win)) {
3310 return win;
3311 } else if (win instanceof WrappedSelection) {
3312 return win.win;
3313 } else {
3314 var doc = dom.getContentDocument(win, module, methodName);
3315 return dom.getWindow(doc);
3319 function getWinSelection(winParam) {
3320 return getWindow(winParam, "getWinSelection").getSelection();
3323 function getDocSelection(winParam) {
3324 return getWindow(winParam, "getDocSelection").document.selection;
3327 function winSelectionIsBackward(sel) {
3328 var backward = false;
3329 if (sel.anchorNode) {
3330 backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1);
3332 return backward;
3335 // Test for the Range/TextRange and Selection features required
3336 // Test for ability to retrieve selection
3337 var implementsWinGetSelection = isHostMethod(window, "getSelection"),
3338 implementsDocSelection = util.isHostObject(document, "selection");
3340 features.implementsWinGetSelection = implementsWinGetSelection;
3341 features.implementsDocSelection = implementsDocSelection;
3343 var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange);
3345 if (useDocumentSelection) {
3346 getNativeSelection = getDocSelection;
3347 api.isSelectionValid = function(winParam) {
3348 var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection;
3350 // Check whether the selection TextRange is actually contained within the correct document
3351 return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc);
3352 };
3353 } else if (implementsWinGetSelection) {
3354 getNativeSelection = getWinSelection;
3355 api.isSelectionValid = function() {
3356 return true;
3357 };
3358 } else {
3359 module.fail("Neither document.selection or window.getSelection() detected.");
3360 return false;
3363 api.getNativeSelection = getNativeSelection;
3365 var testSelection = getNativeSelection();
3367 // In Firefox, the selection is null in an iframe with display: none. See issue #138.
3368 if (!testSelection) {
3369 module.fail("Native selection was null (possibly issue 138?)");
3370 return false;
3373 var testRange = api.createNativeRange(document);
3374 var body = getBody(document);
3376 // Obtaining a range from a selection
3377 var selectionHasAnchorAndFocus = util.areHostProperties(testSelection,
3378 ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]);
3380 features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus;
3382 // Test for existence of native selection extend() method
3383 var selectionHasExtend = isHostMethod(testSelection, "extend");
3384 features.selectionHasExtend = selectionHasExtend;
3386 // Test if rangeCount exists
3387 var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER);
3388 features.selectionHasRangeCount = selectionHasRangeCount;
3390 var selectionSupportsMultipleRanges = false;
3391 var collapsedNonEditableSelectionsSupported = true;
3393 var addRangeBackwardToNative = selectionHasExtend ?
3394 function(nativeSelection, range) {
3395 var doc = DomRange.getRangeDocument(range);
3396 var endRange = api.createRange(doc);
3397 endRange.collapseToPoint(range.endContainer, range.endOffset);
3398 nativeSelection.addRange(getNativeRange(endRange));
3399 nativeSelection.extend(range.startContainer, range.startOffset);
3400 } : null;
3402 if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) &&
3403 typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) {
3405 (function() {
3406 // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are
3407 // performed on the current document's selection. See issue 109.
3409 // Note also that if a selection previously existed, it is wiped and later restored by these tests. This
3410 // will result in the selection direction begin reversed if the original selection was backwards and the
3411 // browser does not support setting backwards selections (Internet Explorer, I'm looking at you).
3412 var sel = window.getSelection();
3413 if (sel) {
3414 // Store the current selection
3415 var originalSelectionRangeCount = sel.rangeCount;
3416 var selectionHasMultipleRanges = (originalSelectionRangeCount > 1);
3417 var originalSelectionRanges = [];
3418 var originalSelectionBackward = winSelectionIsBackward(sel);
3419 for (var i = 0; i < originalSelectionRangeCount; ++i) {
3420 originalSelectionRanges[i] = sel.getRangeAt(i);
3423 // Create some test elements
3424 var testEl = dom.createTestElement(document, "", false);
3425 var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") );
3427 // Test whether the native selection will allow a collapsed selection within a non-editable element
3428 var r1 = document.createRange();
3430 r1.setStart(textNode, 1);
3431 r1.collapse(true);
3432 sel.removeAllRanges();
3433 sel.addRange(r1);
3434 collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1);
3435 sel.removeAllRanges();
3437 // Test whether the native selection is capable of supporting multiple ranges.
3438 if (!selectionHasMultipleRanges) {
3439 // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a
3440 // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's
3441 // nothing we can do about this while retaining the feature test so we have to resort to a browser
3442 // sniff. I'm not happy about it. See
3443 // https://code.google.com/p/chromium/issues/detail?id=399791
3444 var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /);
3445 if (chromeMatch && parseInt(chromeMatch[1]) >= 36) {
3446 selectionSupportsMultipleRanges = false;
3447 } else {
3448 var r2 = r1.cloneRange();
3449 r1.setStart(textNode, 0);
3450 r2.setEnd(textNode, 3);
3451 r2.setStart(textNode, 2);
3452 sel.addRange(r1);
3453 sel.addRange(r2);
3454 selectionSupportsMultipleRanges = (sel.rangeCount == 2);
3458 // Clean up
3459 dom.removeNode(testEl);
3460 sel.removeAllRanges();
3462 for (i = 0; i < originalSelectionRangeCount; ++i) {
3463 if (i == 0 && originalSelectionBackward) {
3464 if (addRangeBackwardToNative) {
3465 addRangeBackwardToNative(sel, originalSelectionRanges[i]);
3466 } else {
3467 api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend");
3468 sel.addRange(originalSelectionRanges[i]);
3470 } else {
3471 sel.addRange(originalSelectionRanges[i]);
3475 })();
3478 features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges;
3479 features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported;
3481 // ControlRanges
3482 var implementsControlRange = false, testControlRange;
3484 if (body && isHostMethod(body, "createControlRange")) {
3485 testControlRange = body.createControlRange();
3486 if (util.areHostProperties(testControlRange, ["item", "add"])) {
3487 implementsControlRange = true;
3490 features.implementsControlRange = implementsControlRange;
3492 // Selection collapsedness
3493 if (selectionHasAnchorAndFocus) {
3494 selectionIsCollapsed = function(sel) {
3495 return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset;
3496 };
3497 } else {
3498 selectionIsCollapsed = function(sel) {
3499 return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false;
3500 };
3503 function updateAnchorAndFocusFromRange(sel, range, backward) {
3504 var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end";
3505 sel.anchorNode = range[anchorPrefix + "Container"];
3506 sel.anchorOffset = range[anchorPrefix + "Offset"];
3507 sel.focusNode = range[focusPrefix + "Container"];
3508 sel.focusOffset = range[focusPrefix + "Offset"];
3511 function updateAnchorAndFocusFromNativeSelection(sel) {
3512 var nativeSel = sel.nativeSelection;
3513 sel.anchorNode = nativeSel.anchorNode;
3514 sel.anchorOffset = nativeSel.anchorOffset;
3515 sel.focusNode = nativeSel.focusNode;
3516 sel.focusOffset = nativeSel.focusOffset;
3519 function updateEmptySelection(sel) {
3520 sel.anchorNode = sel.focusNode = null;
3521 sel.anchorOffset = sel.focusOffset = 0;
3522 sel.rangeCount = 0;
3523 sel.isCollapsed = true;
3524 sel._ranges.length = 0;
3527 function getNativeRange(range) {
3528 var nativeRange;
3529 if (range instanceof DomRange) {
3530 nativeRange = api.createNativeRange(range.getDocument());
3531 nativeRange.setEnd(range.endContainer, range.endOffset);
3532 nativeRange.setStart(range.startContainer, range.startOffset);
3533 } else if (range instanceof WrappedRange) {
3534 nativeRange = range.nativeRange;
3535 } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) {
3536 nativeRange = range;
3538 return nativeRange;
3541 function rangeContainsSingleElement(rangeNodes) {
3542 if (!rangeNodes.length || rangeNodes[0].nodeType != 1) {
3543 return false;
3545 for (var i = 1, len = rangeNodes.length; i < len; ++i) {
3546 if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) {
3547 return false;
3550 return true;
3553 function getSingleElementFromRange(range) {
3554 var nodes = range.getNodes();
3555 if (!rangeContainsSingleElement(nodes)) {
3556 throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element");
3558 return nodes[0];
3561 // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange
3562 function isTextRange(range) {
3563 return !!range && typeof range.text != "undefined";
3566 function updateFromTextRange(sel, range) {
3567 // Create a Range from the selected TextRange
3568 var wrappedRange = new WrappedRange(range);
3569 sel._ranges = [wrappedRange];
3571 updateAnchorAndFocusFromRange(sel, wrappedRange, false);
3572 sel.rangeCount = 1;
3573 sel.isCollapsed = wrappedRange.collapsed;
3576 function updateControlSelection(sel) {
3577 // Update the wrapped selection based on what's now in the native selection
3578 sel._ranges.length = 0;
3579 if (sel.docSelection.type == "None") {
3580 updateEmptySelection(sel);
3581 } else {
3582 var controlRange = sel.docSelection.createRange();
3583 if (isTextRange(controlRange)) {
3584 // This case (where the selection type is "Control" and calling createRange() on the selection returns
3585 // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected
3586 // ControlRange have been removed from the ControlRange and removed from the document.
3587 updateFromTextRange(sel, controlRange);
3588 } else {
3589 sel.rangeCount = controlRange.length;
3590 var range, doc = getDocument(controlRange.item(0));
3591 for (var i = 0; i < sel.rangeCount; ++i) {
3592 range = api.createRange(doc);
3593 range.selectNode(controlRange.item(i));
3594 sel._ranges.push(range);
3596 sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed;
3597 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false);
3602 function addRangeToControlSelection(sel, range) {
3603 var controlRange = sel.docSelection.createRange();
3604 var rangeElement = getSingleElementFromRange(range);
3606 // Create a new ControlRange containing all the elements in the selected ControlRange plus the element
3607 // contained by the supplied range
3608 var doc = getDocument(controlRange.item(0));
3609 var newControlRange = getBody(doc).createControlRange();
3610 for (var i = 0, len = controlRange.length; i < len; ++i) {
3611 newControlRange.add(controlRange.item(i));
3613 try {
3614 newControlRange.add(rangeElement);
3615 } catch (ex) {
3616 throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)");
3618 newControlRange.select();
3620 // Update the wrapped selection based on what's now in the native selection
3621 updateControlSelection(sel);
3624 var getSelectionRangeAt;
3626 if (isHostMethod(testSelection, "getRangeAt")) {
3627 // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation.
3628 // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a
3629 // lesson to us all, especially me.
3630 getSelectionRangeAt = function(sel, index) {
3631 try {
3632 return sel.getRangeAt(index);
3633 } catch (ex) {
3634 return null;
3636 };
3637 } else if (selectionHasAnchorAndFocus) {
3638 getSelectionRangeAt = function(sel) {
3639 var doc = getDocument(sel.anchorNode);
3640 var range = api.createRange(doc);
3641 range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);
3643 // Handle the case when the selection was selected backwards (from the end to the start in the
3644 // document)
3645 if (range.collapsed !== this.isCollapsed) {
3646 range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset);
3649 return range;
3650 };
3653 function WrappedSelection(selection, docSelection, win) {
3654 this.nativeSelection = selection;
3655 this.docSelection = docSelection;
3656 this._ranges = [];
3657 this.win = win;
3658 this.refresh();
3661 WrappedSelection.prototype = api.selectionPrototype;
3663 function deleteProperties(sel) {
3664 sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null;
3665 sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0;
3666 sel.detached = true;
3669 var cachedRangySelections = [];
3671 function actOnCachedSelection(win, action) {
3672 var i = cachedRangySelections.length, cached, sel;
3673 while (i--) {
3674 cached = cachedRangySelections[i];
3675 sel = cached.selection;
3676 if (action == "deleteAll") {
3677 deleteProperties(sel);
3678 } else if (cached.win == win) {
3679 if (action == "delete") {
3680 cachedRangySelections.splice(i, 1);
3681 return true;
3682 } else {
3683 return sel;
3687 if (action == "deleteAll") {
3688 cachedRangySelections.length = 0;
3690 return null;
3693 var getSelection = function(win) {
3694 // Check if the parameter is a Rangy Selection object
3695 if (win && win instanceof WrappedSelection) {
3696 win.refresh();
3697 return win;
3700 win = getWindow(win, "getNativeSelection");
3702 var sel = actOnCachedSelection(win);
3703 var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null;
3704 if (sel) {
3705 sel.nativeSelection = nativeSel;
3706 sel.docSelection = docSel;
3707 sel.refresh();
3708 } else {
3709 sel = new WrappedSelection(nativeSel, docSel, win);
3710 cachedRangySelections.push( { win: win, selection: sel } );
3712 return sel;
3713 };
3715 api.getSelection = getSelection;
3717 util.createAliasForDeprecatedMethod(api, "getIframeSelection", "getSelection");
3719 var selProto = WrappedSelection.prototype;
3721 function createControlSelection(sel, ranges) {
3722 // Ensure that the selection becomes of type "Control"
3723 var doc = getDocument(ranges[0].startContainer);
3724 var controlRange = getBody(doc).createControlRange();
3725 for (var i = 0, el, len = ranges.length; i < len; ++i) {
3726 el = getSingleElementFromRange(ranges[i]);
3727 try {
3728 controlRange.add(el);
3729 } catch (ex) {
3730 throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)");
3733 controlRange.select();
3735 // Update the wrapped selection based on what's now in the native selection
3736 updateControlSelection(sel);
3739 // Selecting a range
3740 if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) {
3741 selProto.removeAllRanges = function() {
3742 this.nativeSelection.removeAllRanges();
3743 updateEmptySelection(this);
3744 };
3746 var addRangeBackward = function(sel, range) {
3747 addRangeBackwardToNative(sel.nativeSelection, range);
3748 sel.refresh();
3749 };
3751 if (selectionHasRangeCount) {
3752 selProto.addRange = function(range, direction) {
3753 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
3754 addRangeToControlSelection(this, range);
3755 } else {
3756 if (isDirectionBackward(direction) && selectionHasExtend) {
3757 addRangeBackward(this, range);
3758 } else {
3759 var previousRangeCount;
3760 if (selectionSupportsMultipleRanges) {
3761 previousRangeCount = this.rangeCount;
3762 } else {
3763 this.removeAllRanges();
3764 previousRangeCount = 0;
3766 // Clone the native range so that changing the selected range does not affect the selection.
3767 // This is contrary to the spec but is the only way to achieve consistency between browsers. See
3768 // issue 80.
3769 var clonedNativeRange = getNativeRange(range).cloneRange();
3770 try {
3771 this.nativeSelection.addRange(clonedNativeRange);
3772 } catch (ex) {
3775 // Check whether adding the range was successful
3776 this.rangeCount = this.nativeSelection.rangeCount;
3778 if (this.rangeCount == previousRangeCount + 1) {
3779 // The range was added successfully
3781 // Check whether the range that we added to the selection is reflected in the last range extracted from
3782 // the selection
3783 if (api.config.checkSelectionRanges) {
3784 var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1);
3785 if (nativeRange && !rangesEqual(nativeRange, range)) {
3786 // Happens in WebKit with, for example, a selection placed at the start of a text node
3787 range = new WrappedRange(nativeRange);
3790 this._ranges[this.rangeCount - 1] = range;
3791 updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection));
3792 this.isCollapsed = selectionIsCollapsed(this);
3793 } else {
3794 // The range was not added successfully. The simplest thing is to refresh
3795 this.refresh();
3799 };
3800 } else {
3801 selProto.addRange = function(range, direction) {
3802 if (isDirectionBackward(direction) && selectionHasExtend) {
3803 addRangeBackward(this, range);
3804 } else {
3805 this.nativeSelection.addRange(getNativeRange(range));
3806 this.refresh();
3808 };
3811 selProto.setRanges = function(ranges) {
3812 if (implementsControlRange && implementsDocSelection && ranges.length > 1) {
3813 createControlSelection(this, ranges);
3814 } else {
3815 this.removeAllRanges();
3816 for (var i = 0, len = ranges.length; i < len; ++i) {
3817 this.addRange(ranges[i]);
3820 };
3821 } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") &&
3822 implementsControlRange && useDocumentSelection) {
3824 selProto.removeAllRanges = function() {
3825 // Added try/catch as fix for issue #21
3826 try {
3827 this.docSelection.empty();
3829 // Check for empty() not working (issue #24)
3830 if (this.docSelection.type != "None") {
3831 // Work around failure to empty a control selection by instead selecting a TextRange and then
3832 // calling empty()
3833 var doc;
3834 if (this.anchorNode) {
3835 doc = getDocument(this.anchorNode);
3836 } else if (this.docSelection.type == CONTROL) {
3837 var controlRange = this.docSelection.createRange();
3838 if (controlRange.length) {
3839 doc = getDocument( controlRange.item(0) );
3842 if (doc) {
3843 var textRange = getBody(doc).createTextRange();
3844 textRange.select();
3845 this.docSelection.empty();
3848 } catch(ex) {}
3849 updateEmptySelection(this);
3850 };
3852 selProto.addRange = function(range) {
3853 if (this.docSelection.type == CONTROL) {
3854 addRangeToControlSelection(this, range);
3855 } else {
3856 api.WrappedTextRange.rangeToTextRange(range).select();
3857 this._ranges[0] = range;
3858 this.rangeCount = 1;
3859 this.isCollapsed = this._ranges[0].collapsed;
3860 updateAnchorAndFocusFromRange(this, range, false);
3862 };
3864 selProto.setRanges = function(ranges) {
3865 this.removeAllRanges();
3866 var rangeCount = ranges.length;
3867 if (rangeCount > 1) {
3868 createControlSelection(this, ranges);
3869 } else if (rangeCount) {
3870 this.addRange(ranges[0]);
3872 };
3873 } else {
3874 module.fail("No means of selecting a Range or TextRange was found");
3875 return false;
3878 selProto.getRangeAt = function(index) {
3879 if (index < 0 || index >= this.rangeCount) {
3880 throw new DOMException("INDEX_SIZE_ERR");
3881 } else {
3882 // Clone the range to preserve selection-range independence. See issue 80.
3883 return this._ranges[index].cloneRange();
3885 };
3887 var refreshSelection;
3889 if (useDocumentSelection) {
3890 refreshSelection = function(sel) {
3891 var range;
3892 if (api.isSelectionValid(sel.win)) {
3893 range = sel.docSelection.createRange();
3894 } else {
3895 range = getBody(sel.win.document).createTextRange();
3896 range.collapse(true);
3899 if (sel.docSelection.type == CONTROL) {
3900 updateControlSelection(sel);
3901 } else if (isTextRange(range)) {
3902 updateFromTextRange(sel, range);
3903 } else {
3904 updateEmptySelection(sel);
3906 };
3907 } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) {
3908 refreshSelection = function(sel) {
3909 if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) {
3910 updateControlSelection(sel);
3911 } else {
3912 sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount;
3913 if (sel.rangeCount) {
3914 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
3915 sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i));
3917 updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection));
3918 sel.isCollapsed = selectionIsCollapsed(sel);
3919 } else {
3920 updateEmptySelection(sel);
3923 };
3924 } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) {
3925 refreshSelection = function(sel) {
3926 var range, nativeSel = sel.nativeSelection;
3927 if (nativeSel.anchorNode) {
3928 range = getSelectionRangeAt(nativeSel, 0);
3929 sel._ranges = [range];
3930 sel.rangeCount = 1;
3931 updateAnchorAndFocusFromNativeSelection(sel);
3932 sel.isCollapsed = selectionIsCollapsed(sel);
3933 } else {
3934 updateEmptySelection(sel);
3936 };
3937 } else {
3938 module.fail("No means of obtaining a Range or TextRange from the user's selection was found");
3939 return false;
3942 selProto.refresh = function(checkForChanges) {
3943 var oldRanges = checkForChanges ? this._ranges.slice(0) : null;
3944 var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset;
3946 refreshSelection(this);
3947 if (checkForChanges) {
3948 // Check the range count first
3949 var i = oldRanges.length;
3950 if (i != this._ranges.length) {
3951 return true;
3954 // Now check the direction. Checking the anchor position is the same is enough since we're checking all the
3955 // ranges after this
3956 if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) {
3957 return true;
3960 // Finally, compare each range in turn
3961 while (i--) {
3962 if (!rangesEqual(oldRanges[i], this._ranges[i])) {
3963 return true;
3966 return false;
3968 };
3970 // Removal of a single range
3971 var removeRangeManually = function(sel, range) {
3972 var ranges = sel.getAllRanges();
3973 sel.removeAllRanges();
3974 for (var i = 0, len = ranges.length; i < len; ++i) {
3975 if (!rangesEqual(range, ranges[i])) {
3976 sel.addRange(ranges[i]);
3979 if (!sel.rangeCount) {
3980 updateEmptySelection(sel);
3982 };
3984 if (implementsControlRange && implementsDocSelection) {
3985 selProto.removeRange = function(range) {
3986 if (this.docSelection.type == CONTROL) {
3987 var controlRange = this.docSelection.createRange();
3988 var rangeElement = getSingleElementFromRange(range);
3990 // Create a new ControlRange containing all the elements in the selected ControlRange minus the
3991 // element contained by the supplied range
3992 var doc = getDocument(controlRange.item(0));
3993 var newControlRange = getBody(doc).createControlRange();
3994 var el, removed = false;
3995 for (var i = 0, len = controlRange.length; i < len; ++i) {
3996 el = controlRange.item(i);
3997 if (el !== rangeElement || removed) {
3998 newControlRange.add(controlRange.item(i));
3999 } else {
4000 removed = true;
4003 newControlRange.select();
4005 // Update the wrapped selection based on what's now in the native selection
4006 updateControlSelection(this);
4007 } else {
4008 removeRangeManually(this, range);
4010 };
4011 } else {
4012 selProto.removeRange = function(range) {
4013 removeRangeManually(this, range);
4014 };
4017 // Detecting if a selection is backward
4018 var selectionIsBackward;
4019 if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) {
4020 selectionIsBackward = winSelectionIsBackward;
4022 selProto.isBackward = function() {
4023 return selectionIsBackward(this);
4024 };
4025 } else {
4026 selectionIsBackward = selProto.isBackward = function() {
4027 return false;
4028 };
4031 // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards"
4032 selProto.isBackwards = selProto.isBackward;
4034 // Selection stringifier
4035 // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation.
4036 // The current spec does not yet define this method.
4037 selProto.toString = function() {
4038 var rangeTexts = [];
4039 for (var i = 0, len = this.rangeCount; i < len; ++i) {
4040 rangeTexts[i] = "" + this._ranges[i];
4042 return rangeTexts.join("");
4043 };
4045 function assertNodeInSameDocument(sel, node) {
4046 if (sel.win.document != getDocument(node)) {
4047 throw new DOMException("WRONG_DOCUMENT_ERR");
4051 // No current browser conforms fully to the spec for this method, so Rangy's own method is always used
4052 selProto.collapse = function(node, offset) {
4053 assertNodeInSameDocument(this, node);
4054 var range = api.createRange(node);
4055 range.collapseToPoint(node, offset);
4056 this.setSingleRange(range);
4057 this.isCollapsed = true;
4058 };
4060 selProto.collapseToStart = function() {
4061 if (this.rangeCount) {
4062 var range = this._ranges[0];
4063 this.collapse(range.startContainer, range.startOffset);
4064 } else {
4065 throw new DOMException("INVALID_STATE_ERR");
4067 };
4069 selProto.collapseToEnd = function() {
4070 if (this.rangeCount) {
4071 var range = this._ranges[this.rangeCount - 1];
4072 this.collapse(range.endContainer, range.endOffset);
4073 } else {
4074 throw new DOMException("INVALID_STATE_ERR");
4076 };
4078 // The spec is very specific on how selectAllChildren should be implemented and not all browsers implement it as
4079 // specified so the native implementation is never used by Rangy.
4080 selProto.selectAllChildren = function(node) {
4081 assertNodeInSameDocument(this, node);
4082 var range = api.createRange(node);
4083 range.selectNodeContents(node);
4084 this.setSingleRange(range);
4085 };
4087 selProto.deleteFromDocument = function() {
4088 // Sepcial behaviour required for IE's control selections
4089 if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) {
4090 var controlRange = this.docSelection.createRange();
4091 var element;
4092 while (controlRange.length) {
4093 element = controlRange.item(0);
4094 controlRange.remove(element);
4095 dom.removeNode(element);
4097 this.refresh();
4098 } else if (this.rangeCount) {
4099 var ranges = this.getAllRanges();
4100 if (ranges.length) {
4101 this.removeAllRanges();
4102 for (var i = 0, len = ranges.length; i < len; ++i) {
4103 ranges[i].deleteContents();
4105 // The spec says nothing about what the selection should contain after calling deleteContents on each
4106 // range. Firefox moves the selection to where the final selected range was, so we emulate that
4107 this.addRange(ranges[len - 1]);
4110 };
4112 // The following are non-standard extensions
4113 selProto.eachRange = function(func, returnValue) {
4114 for (var i = 0, len = this._ranges.length; i < len; ++i) {
4115 if ( func( this.getRangeAt(i) ) ) {
4116 return returnValue;
4119 };
4121 selProto.getAllRanges = function() {
4122 var ranges = [];
4123 this.eachRange(function(range) {
4124 ranges.push(range);
4125 });
4126 return ranges;
4127 };
4129 selProto.setSingleRange = function(range, direction) {
4130 this.removeAllRanges();
4131 this.addRange(range, direction);
4132 };
4134 selProto.callMethodOnEachRange = function(methodName, params) {
4135 var results = [];
4136 this.eachRange( function(range) {
4137 results.push( range[methodName].apply(range, params || []) );
4138 } );
4139 return results;
4140 };
4142 function createStartOrEndSetter(isStart) {
4143 return function(node, offset) {
4144 var range;
4145 if (this.rangeCount) {
4146 range = this.getRangeAt(0);
4147 range["set" + (isStart ? "Start" : "End")](node, offset);
4148 } else {
4149 range = api.createRange(this.win.document);
4150 range.setStartAndEnd(node, offset);
4152 this.setSingleRange(range, this.isBackward());
4153 };
4156 selProto.setStart = createStartOrEndSetter(true);
4157 selProto.setEnd = createStartOrEndSetter(false);
4159 // Add select() method to Range prototype. Any existing selection will be removed.
4160 api.rangePrototype.select = function(direction) {
4161 getSelection( this.getDocument() ).setSingleRange(this, direction);
4162 };
4164 selProto.changeEachRange = function(func) {
4165 var ranges = [];
4166 var backward = this.isBackward();
4168 this.eachRange(function(range) {
4169 func(range);
4170 ranges.push(range);
4171 });
4173 this.removeAllRanges();
4174 if (backward && ranges.length == 1) {
4175 this.addRange(ranges[0], "backward");
4176 } else {
4177 this.setRanges(ranges);
4179 };
4181 selProto.containsNode = function(node, allowPartial) {
4182 return this.eachRange( function(range) {
4183 return range.containsNode(node, allowPartial);
4184 }, true ) || false;
4185 };
4187 selProto.getBookmark = function(containerNode) {
4188 return {
4189 backward: this.isBackward(),
4190 rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode])
4191 };
4192 };
4194 selProto.moveToBookmark = function(bookmark) {
4195 var selRanges = [];
4196 for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) {
4197 range = api.createRange(this.win);
4198 range.moveToBookmark(rangeBookmark);
4199 selRanges.push(range);
4201 if (bookmark.backward) {
4202 this.setSingleRange(selRanges[0], "backward");
4203 } else {
4204 this.setRanges(selRanges);
4206 };
4208 selProto.saveRanges = function() {
4209 return {
4210 backward: this.isBackward(),
4211 ranges: this.callMethodOnEachRange("cloneRange")
4212 };
4213 };
4215 selProto.restoreRanges = function(selRanges) {
4216 this.removeAllRanges();
4217 for (var i = 0, range; range = selRanges.ranges[i]; ++i) {
4218 this.addRange(range, (selRanges.backward && i == 0));
4220 };
4222 selProto.toHtml = function() {
4223 var rangeHtmls = [];
4224 this.eachRange(function(range) {
4225 rangeHtmls.push( DomRange.toHtml(range) );
4226 });
4227 return rangeHtmls.join("");
4228 };
4230 if (features.implementsTextRange) {
4231 selProto.getNativeTextRange = function() {
4232 var sel, textRange;
4233 if ( (sel = this.docSelection) ) {
4234 var range = sel.createRange();
4235 if (isTextRange(range)) {
4236 return range;
4237 } else {
4238 throw module.createError("getNativeTextRange: selection is a control selection");
4240 } else if (this.rangeCount > 0) {
4241 return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) );
4242 } else {
4243 throw module.createError("getNativeTextRange: selection contains no range");
4245 };
4248 function inspect(sel) {
4249 var rangeInspects = [];
4250 var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset);
4251 var focus = new DomPosition(sel.focusNode, sel.focusOffset);
4252 var name = (typeof sel.getName == "function") ? sel.getName() : "Selection";
4254 if (typeof sel.rangeCount != "undefined") {
4255 for (var i = 0, len = sel.rangeCount; i < len; ++i) {
4256 rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i));
4259 return "[" + name + "(Ranges: " + rangeInspects.join(", ") +
4260 ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]";
4263 selProto.getName = function() {
4264 return "WrappedSelection";
4265 };
4267 selProto.inspect = function() {
4268 return inspect(this);
4269 };
4271 selProto.detach = function() {
4272 actOnCachedSelection(this.win, "delete");
4273 deleteProperties(this);
4274 };
4276 WrappedSelection.detachAll = function() {
4277 actOnCachedSelection(null, "deleteAll");
4278 };
4280 WrappedSelection.inspect = inspect;
4281 WrappedSelection.isDirectionBackward = isDirectionBackward;
4283 api.Selection = WrappedSelection;
4285 api.selectionPrototype = selProto;
4287 api.addShimListener(function(win) {
4288 if (typeof win.getSelection == "undefined") {
4289 win.getSelection = function() {
4290 return getSelection(win);
4291 };
4293 win = null;
4294 });
4295 });
4298 /*----------------------------------------------------------------------------------------------------------------*/
4300 // Wait for document to load before initializing
4301 var docReady = false;
4303 var loadHandler = function(e) {
4304 if (!docReady) {
4305 docReady = true;
4306 if (!api.initialized && api.config.autoInitialize) {
4307 init();
4310 };
4312 if (isBrowser) {
4313 // Test whether the document has already been loaded and initialize immediately if so
4314 if (document.readyState == "complete") {
4315 loadHandler();
4316 } else {
4317 if (isHostMethod(document, "addEventListener")) {
4318 document.addEventListener("DOMContentLoaded", loadHandler, false);
4321 // Add a fallback in case the DOMContentLoaded event isn't supported
4322 addListener(window, "load", loadHandler);
4326 rangy = api;
4327 })();
4329 /**
4330 * Selection save and restore module for Rangy.
4331 * Saves and restores user selections using marker invisible elements in the DOM.
4333 * Part of Rangy, a cross-browser JavaScript range and selection library
4334 * https://github.com/timdown/rangy
4336 * Depends on Rangy core.
4338 * Copyright 2015, Tim Down
4339 * Licensed under the MIT license.
4340 * Version: 1.3.1-dev
4341 * Build date: 20 May 2015
4343 * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
4344 */
4345 rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) {
4346 var dom = api.dom;
4347 var removeNode = dom.removeNode;
4348 var isDirectionBackward = api.Selection.isDirectionBackward;
4349 var markerTextChar = "\ufeff";
4351 function gEBI(id, doc) {
4352 return (doc || document).getElementById(id);
4355 function insertRangeBoundaryMarker(range, atStart) {
4356 var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
4357 var markerEl;
4358 var doc = dom.getDocument(range.startContainer);
4360 // Clone the Range and collapse to the appropriate boundary point
4361 var boundaryRange = range.cloneRange();
4362 boundaryRange.collapse(atStart);
4364 // Create the marker element containing a single invisible character using DOM methods and insert it
4365 markerEl = doc.createElement("span");
4366 markerEl.id = markerId;
4367 markerEl.style.lineHeight = "0";
4368 markerEl.style.display = "none";
4369 markerEl.className = "rangySelectionBoundary";
4370 markerEl.appendChild(doc.createTextNode(markerTextChar));
4372 boundaryRange.insertNode(markerEl);
4373 return markerEl;
4376 function setRangeBoundary(doc, range, markerId, atStart) {
4377 var markerEl = gEBI(markerId, doc);
4378 if (markerEl) {
4379 range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
4380 removeNode(markerEl);
4381 } else {
4382 module.warn("Marker element has been removed. Cannot restore selection.");
4386 function compareRanges(r1, r2) {
4387 return r2.compareBoundaryPoints(r1.START_TO_START, r1);
4390 function saveRange(range, direction) {
4391 var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString();
4392 var backward = isDirectionBackward(direction);
4394 if (range.collapsed) {
4395 endEl = insertRangeBoundaryMarker(range, false);
4396 return {
4397 document: doc,
4398 markerId: endEl.id,
4399 collapsed: true
4400 };
4401 } else {
4402 endEl = insertRangeBoundaryMarker(range, false);
4403 startEl = insertRangeBoundaryMarker(range, true);
4405 return {
4406 document: doc,
4407 startMarkerId: startEl.id,
4408 endMarkerId: endEl.id,
4409 collapsed: false,
4410 backward: backward,
4411 toString: function() {
4412 return "original text: '" + text + "', new text: '" + range.toString() + "'";
4414 };
4418 function restoreRange(rangeInfo, normalize) {
4419 var doc = rangeInfo.document;
4420 if (typeof normalize == "undefined") {
4421 normalize = true;
4423 var range = api.createRange(doc);
4424 if (rangeInfo.collapsed) {
4425 var markerEl = gEBI(rangeInfo.markerId, doc);
4426 if (markerEl) {
4427 markerEl.style.display = "inline";
4428 var previousNode = markerEl.previousSibling;
4430 // Workaround for issue 17
4431 if (previousNode && previousNode.nodeType == 3) {
4432 removeNode(markerEl);
4433 range.collapseToPoint(previousNode, previousNode.length);
4434 } else {
4435 range.collapseBefore(markerEl);
4436 removeNode(markerEl);
4438 } else {
4439 module.warn("Marker element has been removed. Cannot restore selection.");
4441 } else {
4442 setRangeBoundary(doc, range, rangeInfo.startMarkerId, true);
4443 setRangeBoundary(doc, range, rangeInfo.endMarkerId, false);
4446 if (normalize) {
4447 range.normalizeBoundaries();
4450 return range;
4453 function saveRanges(ranges, direction) {
4454 var rangeInfos = [], range, doc;
4455 var backward = isDirectionBackward(direction);
4457 // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
4458 ranges = ranges.slice(0);
4459 ranges.sort(compareRanges);
4461 for (var i = 0, len = ranges.length; i < len; ++i) {
4462 rangeInfos[i] = saveRange(ranges[i], backward);
4465 // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
4466 // between its markers
4467 for (i = len - 1; i >= 0; --i) {
4468 range = ranges[i];
4469 doc = api.DomRange.getRangeDocument(range);
4470 if (range.collapsed) {
4471 range.collapseAfter(gEBI(rangeInfos[i].markerId, doc));
4472 } else {
4473 range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
4474 range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
4478 return rangeInfos;
4481 function saveSelection(win) {
4482 if (!api.isSelectionValid(win)) {
4483 module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
4484 return null;
4486 var sel = api.getSelection(win);
4487 var ranges = sel.getAllRanges();
4488 var backward = (ranges.length == 1 && sel.isBackward());
4490 var rangeInfos = saveRanges(ranges, backward);
4492 // Ensure current selection is unaffected
4493 if (backward) {
4494 sel.setSingleRange(ranges[0], backward);
4495 } else {
4496 sel.setRanges(ranges);
4499 return {
4500 win: win,
4501 rangeInfos: rangeInfos,
4502 restored: false
4503 };
4506 function restoreRanges(rangeInfos) {
4507 var ranges = [];
4509 // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
4510 // normalization affecting previously restored ranges.
4511 var rangeCount = rangeInfos.length;
4513 for (var i = rangeCount - 1; i >= 0; i--) {
4514 ranges[i] = restoreRange(rangeInfos[i], true);
4517 return ranges;
4520 function restoreSelection(savedSelection, preserveDirection) {
4521 if (!savedSelection.restored) {
4522 var rangeInfos = savedSelection.rangeInfos;
4523 var sel = api.getSelection(savedSelection.win);
4524 var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length;
4526 if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) {
4527 sel.removeAllRanges();
4528 sel.addRange(ranges[0], true);
4529 } else {
4530 sel.setRanges(ranges);
4533 savedSelection.restored = true;
4537 function removeMarkerElement(doc, markerId) {
4538 var markerEl = gEBI(markerId, doc);
4539 if (markerEl) {
4540 removeNode(markerEl);
4544 function removeMarkers(savedSelection) {
4545 var rangeInfos = savedSelection.rangeInfos;
4546 for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
4547 rangeInfo = rangeInfos[i];
4548 if (rangeInfo.collapsed) {
4549 removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
4550 } else {
4551 removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
4552 removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
4557 api.util.extend(api, {
4558 saveRange: saveRange,
4559 restoreRange: restoreRange,
4560 saveRanges: saveRanges,
4561 restoreRanges: restoreRanges,
4562 saveSelection: saveSelection,
4563 restoreSelection: restoreSelection,
4564 removeMarkerElement: removeMarkerElement,
4565 removeMarkers: removeMarkers
4566 });
4567 });
4569 /**
4570 * Text range module for Rangy.
4571 * Text-based manipulation and searching of ranges and selections.
4573 * Features
4575 * - Ability to move range boundaries by character or word offsets
4576 * - Customizable word tokenizer
4577 * - Ignores text nodes inside <script> or <style> elements or those hidden by CSS display and visibility properties
4578 * - Range findText method to search for text or regex within the page or within a range. Flags for whole words and case
4579 * sensitivity
4580 * - Selection and range save/restore as text offsets within a node
4581 * - Methods to return visible text within a range or selection
4582 * - innerText method for elements
4584 * References
4586 * https://www.w3.org/Bugs/Public/show_bug.cgi?id=13145
4587 * http://aryeh.name/spec/innertext/innertext.html
4588 * http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html
4590 * Part of Rangy, a cross-browser JavaScript range and selection library
4591 * https://github.com/timdown/rangy
4593 * Depends on Rangy core.
4595 * Copyright 2015, Tim Down
4596 * Licensed under the MIT license.
4597 * Version: 1.3.1-dev
4598 * Build date: 20 May 2015
4599 */
4601 /**
4602 * Problem: handling of trailing spaces before line breaks is handled inconsistently between browsers.
4604 * First, a <br>: this is relatively simple. For the following HTML:
4606 * 1 <br>2
4608 * - IE and WebKit render the space, include it in the selection (i.e. when the content is selected and pasted into a
4609 * textarea, the space is present) and allow the caret to be placed after it.
4610 * - Firefox does not acknowledge the space in the selection but it is possible to place the caret after it.
4611 * - Opera does not render the space but has two separate caret positions on either side of the space (left and right
4612 * arrow keys show this) and includes the space in the selection.
4614 * The other case is the line break or breaks implied by block elements. For the following HTML:
4616 * <p>1 </p><p>2<p>
4618 * - WebKit does not acknowledge the space in any way
4619 * - Firefox, IE and Opera as per <br>
4621 * One more case is trailing spaces before line breaks in elements with white-space: pre-line. For the following HTML:
4623 * <p style="white-space: pre-line">1
4624 * 2</p>
4626 * - Firefox and WebKit include the space in caret positions
4627 * - IE does not support pre-line up to and including version 9
4628 * - Opera ignores the space
4629 * - Trailing space only renders if there is a non-collapsed character in the line
4631 * Problem is whether Rangy should ever acknowledge the space and if so, when. Another problem is whether this can be
4632 * feature-tested
4634 * NOTE: UMD wrapper removed manually for bundling (Oliver Pulges)
4635 */
4636 rangy.createModule("TextRange", ["WrappedSelection"], function(api, module) {
4637 var UNDEF = "undefined";
4638 var CHARACTER = "character", WORD = "word";
4639 var dom = api.dom, util = api.util;
4640 var extend = util.extend;
4641 var createOptions = util.createOptions;
4642 var getBody = dom.getBody;
4645 var spacesRegex = /^[ \t\f\r\n]+$/;
4646 var spacesMinusLineBreaksRegex = /^[ \t\f\r]+$/;
4647 var allWhiteSpaceRegex = /^[\t-\r \u0085\u00A0\u1680\u180E\u2000-\u200B\u2028\u2029\u202F\u205F\u3000]+$/;
4648 var nonLineBreakWhiteSpaceRegex = /^[\t \u00A0\u1680\u180E\u2000-\u200B\u202F\u205F\u3000]+$/;
4649 var lineBreakRegex = /^[\n-\r\u0085\u2028\u2029]$/;
4651 var defaultLanguage = "en";
4653 var isDirectionBackward = api.Selection.isDirectionBackward;
4655 // Properties representing whether trailing spaces inside blocks are completely collapsed (as they are in WebKit,
4656 // but not other browsers). Also test whether trailing spaces before <br> elements are collapsed.
4657 var trailingSpaceInBlockCollapses = false;
4658 var trailingSpaceBeforeBrCollapses = false;
4659 var trailingSpaceBeforeBlockCollapses = false;
4660 var trailingSpaceBeforeLineBreakInPreLineCollapses = true;
4662 (function() {
4663 var el = dom.createTestElement(document, "<p>1 </p><p></p>", true);
4664 var p = el.firstChild;
4665 var sel = api.getSelection();
4666 sel.collapse(p.lastChild, 2);
4667 sel.setStart(p.firstChild, 0);
4668 trailingSpaceInBlockCollapses = ("" + sel).length == 1;
4670 el.innerHTML = "1 <br />";
4671 sel.collapse(el, 2);
4672 sel.setStart(el.firstChild, 0);
4673 trailingSpaceBeforeBrCollapses = ("" + sel).length == 1;
4675 el.innerHTML = "1 <p>1</p>";
4676 sel.collapse(el, 2);
4677 sel.setStart(el.firstChild, 0);
4678 trailingSpaceBeforeBlockCollapses = ("" + sel).length == 1;
4680 dom.removeNode(el);
4681 sel.removeAllRanges();
4682 })();
4684 /*----------------------------------------------------------------------------------------------------------------*/
4686 // This function must create word and non-word tokens for the whole of the text supplied to it
4687 function defaultTokenizer(chars, wordOptions) {
4688 var word = chars.join(""), result, tokenRanges = [];
4690 function createTokenRange(start, end, isWord) {
4691 tokenRanges.push( { start: start, end: end, isWord: isWord } );
4694 // Match words and mark characters
4695 var lastWordEnd = 0, wordStart, wordEnd;
4696 while ( (result = wordOptions.wordRegex.exec(word)) ) {
4697 wordStart = result.index;
4698 wordEnd = wordStart + result[0].length;
4700 // Create token for non-word characters preceding this word
4701 if (wordStart > lastWordEnd) {
4702 createTokenRange(lastWordEnd, wordStart, false);
4705 // Get trailing space characters for word
4706 if (wordOptions.includeTrailingSpace) {
4707 while ( nonLineBreakWhiteSpaceRegex.test(chars[wordEnd]) ) {
4708 ++wordEnd;
4711 createTokenRange(wordStart, wordEnd, true);
4712 lastWordEnd = wordEnd;
4715 // Create token for trailing non-word characters, if any exist
4716 if (lastWordEnd < chars.length) {
4717 createTokenRange(lastWordEnd, chars.length, false);
4720 return tokenRanges;
4723 function convertCharRangeToToken(chars, tokenRange) {
4724 var tokenChars = chars.slice(tokenRange.start, tokenRange.end);
4725 var token = {
4726 isWord: tokenRange.isWord,
4727 chars: tokenChars,
4728 toString: function() {
4729 return tokenChars.join("");
4731 };
4732 for (var i = 0, len = tokenChars.length; i < len; ++i) {
4733 tokenChars[i].token = token;
4735 return token;
4738 function tokenize(chars, wordOptions, tokenizer) {
4739 var tokenRanges = tokenizer(chars, wordOptions);
4740 var tokens = [];
4741 for (var i = 0, tokenRange; tokenRange = tokenRanges[i++]; ) {
4742 tokens.push( convertCharRangeToToken(chars, tokenRange) );
4744 return tokens;
4747 var defaultCharacterOptions = {
4748 includeBlockContentTrailingSpace: true,
4749 includeSpaceBeforeBr: true,
4750 includeSpaceBeforeBlock: true,
4751 includePreLineTrailingSpace: true,
4752 ignoreCharacters: ""
4753 };
4755 function normalizeIgnoredCharacters(ignoredCharacters) {
4756 // Check if character is ignored
4757 var ignoredChars = ignoredCharacters || "";
4759 // Normalize ignored characters into a string consisting of characters in ascending order of character code
4760 var ignoredCharsArray = (typeof ignoredChars == "string") ? ignoredChars.split("") : ignoredChars;
4761 ignoredCharsArray.sort(function(char1, char2) {
4762 return char1.charCodeAt(0) - char2.charCodeAt(0);
4763 });
4765 /// Convert back to a string and remove duplicates
4766 return ignoredCharsArray.join("").replace(/(.)\1+/g, "$1");
4769 var defaultCaretCharacterOptions = {
4770 includeBlockContentTrailingSpace: !trailingSpaceBeforeLineBreakInPreLineCollapses,
4771 includeSpaceBeforeBr: !trailingSpaceBeforeBrCollapses,
4772 includeSpaceBeforeBlock: !trailingSpaceBeforeBlockCollapses,
4773 includePreLineTrailingSpace: true
4774 };
4776 var defaultWordOptions = {
4777 "en": {
4778 wordRegex: /[a-z0-9]+('[a-z0-9]+)*/gi,
4779 includeTrailingSpace: false,
4780 tokenizer: defaultTokenizer
4782 };
4784 var defaultFindOptions = {
4785 caseSensitive: false,
4786 withinRange: null,
4787 wholeWordsOnly: false,
4788 wrap: false,
4789 direction: "forward",
4790 wordOptions: null,
4791 characterOptions: null
4792 };
4794 var defaultMoveOptions = {
4795 wordOptions: null,
4796 characterOptions: null
4797 };
4799 var defaultExpandOptions = {
4800 wordOptions: null,
4801 characterOptions: null,
4802 trim: false,
4803 trimStart: true,
4804 trimEnd: true
4805 };
4807 var defaultWordIteratorOptions = {
4808 wordOptions: null,
4809 characterOptions: null,
4810 direction: "forward"
4811 };
4813 function createWordOptions(options) {
4814 var lang, defaults;
4815 if (!options) {
4816 return defaultWordOptions[defaultLanguage];
4817 } else {
4818 lang = options.language || defaultLanguage;
4819 defaults = {};
4820 extend(defaults, defaultWordOptions[lang] || defaultWordOptions[defaultLanguage]);
4821 extend(defaults, options);
4822 return defaults;
4826 function createNestedOptions(optionsParam, defaults) {
4827 var options = createOptions(optionsParam, defaults);
4828 if (defaults.hasOwnProperty("wordOptions")) {
4829 options.wordOptions = createWordOptions(options.wordOptions);
4831 if (defaults.hasOwnProperty("characterOptions")) {
4832 options.characterOptions = createOptions(options.characterOptions, defaultCharacterOptions);
4834 return options;
4837 /*----------------------------------------------------------------------------------------------------------------*/
4839 /* DOM utility functions */
4840 var getComputedStyleProperty = dom.getComputedStyleProperty;
4842 // Create cachable versions of DOM functions
4844 // Test for old IE's incorrect display properties
4845 var tableCssDisplayBlock;
4846 (function() {
4847 var table = document.createElement("table");
4848 var body = getBody(document);
4849 body.appendChild(table);
4850 tableCssDisplayBlock = (getComputedStyleProperty(table, "display") == "block");
4851 body.removeChild(table);
4852 })();
4854 var defaultDisplayValueForTag = {
4855 table: "table",
4856 caption: "table-caption",
4857 colgroup: "table-column-group",
4858 col: "table-column",
4859 thead: "table-header-group",
4860 tbody: "table-row-group",
4861 tfoot: "table-footer-group",
4862 tr: "table-row",
4863 td: "table-cell",
4864 th: "table-cell"
4865 };
4867 // Corrects IE's "block" value for table-related elements
4868 function getComputedDisplay(el, win) {
4869 var display = getComputedStyleProperty(el, "display", win);
4870 var tagName = el.tagName.toLowerCase();
4871 return (display == "block" &&
4872 tableCssDisplayBlock &&
4873 defaultDisplayValueForTag.hasOwnProperty(tagName)) ?
4874 defaultDisplayValueForTag[tagName] : display;
4877 function isHidden(node) {
4878 var ancestors = getAncestorsAndSelf(node);
4879 for (var i = 0, len = ancestors.length; i < len; ++i) {
4880 if (ancestors[i].nodeType == 1 && getComputedDisplay(ancestors[i]) == "none") {
4881 return true;
4885 return false;
4888 function isVisibilityHiddenTextNode(textNode) {
4889 var el;
4890 return textNode.nodeType == 3 &&
4891 (el = textNode.parentNode) &&
4892 getComputedStyleProperty(el, "visibility") == "hidden";
4895 /*----------------------------------------------------------------------------------------------------------------*/
4898 // "A block node is either an Element whose "display" property does not have
4899 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a
4900 // Document, or a DocumentFragment."
4901 function isBlockNode(node) {
4902 return node &&
4903 ((node.nodeType == 1 && !/^(inline(-block|-table)?|none)$/.test(getComputedDisplay(node))) ||
4904 node.nodeType == 9 || node.nodeType == 11);
4907 function getLastDescendantOrSelf(node) {
4908 var lastChild = node.lastChild;
4909 return lastChild ? getLastDescendantOrSelf(lastChild) : node;
4912 function containsPositions(node) {
4913 return dom.isCharacterDataNode(node) ||
4914 !/^(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)$/i.test(node.nodeName);
4917 function getAncestors(node) {
4918 var ancestors = [];
4919 while (node.parentNode) {
4920 ancestors.unshift(node.parentNode);
4921 node = node.parentNode;
4923 return ancestors;
4926 function getAncestorsAndSelf(node) {
4927 return getAncestors(node).concat([node]);
4930 function nextNodeDescendants(node) {
4931 while (node && !node.nextSibling) {
4932 node = node.parentNode;
4934 if (!node) {
4935 return null;
4937 return node.nextSibling;
4940 function nextNode(node, excludeChildren) {
4941 if (!excludeChildren && node.hasChildNodes()) {
4942 return node.firstChild;
4944 return nextNodeDescendants(node);
4947 function previousNode(node) {
4948 var previous = node.previousSibling;
4949 if (previous) {
4950 node = previous;
4951 while (node.hasChildNodes()) {
4952 node = node.lastChild;
4954 return node;
4956 var parent = node.parentNode;
4957 if (parent && parent.nodeType == 1) {
4958 return parent;
4960 return null;
4963 // Adpated from Aryeh's code.
4964 // "A whitespace node is either a Text node whose data is the empty string; or
4965 // a Text node whose data consists only of one or more tabs (0x0009), line
4966 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose
4967 // parent is an Element whose resolved value for "white-space" is "normal" or
4968 // "nowrap"; or a Text node whose data consists only of one or more tabs
4969 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose
4970 // parent is an Element whose resolved value for "white-space" is "pre-line"."
4971 function isWhitespaceNode(node) {
4972 if (!node || node.nodeType != 3) {
4973 return false;
4975 var text = node.data;
4976 if (text === "") {
4977 return true;
4979 var parent = node.parentNode;
4980 if (!parent || parent.nodeType != 1) {
4981 return false;
4983 var computedWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
4985 return (/^[\t\n\r ]+$/.test(text) && /^(normal|nowrap)$/.test(computedWhiteSpace)) ||
4986 (/^[\t\r ]+$/.test(text) && computedWhiteSpace == "pre-line");
4989 // Adpated from Aryeh's code.
4990 // "node is a collapsed whitespace node if the following algorithm returns
4991 // true:"
4992 function isCollapsedWhitespaceNode(node) {
4993 // "If node's data is the empty string, return true."
4994 if (node.data === "") {
4995 return true;
4998 // "If node is not a whitespace node, return false."
4999 if (!isWhitespaceNode(node)) {
5000 return false;
5003 // "Let ancestor be node's parent."
5004 var ancestor = node.parentNode;
5006 // "If ancestor is null, return true."
5007 if (!ancestor) {
5008 return true;
5011 // "If the "display" property of some ancestor of node has resolved value "none", return true."
5012 if (isHidden(node)) {
5013 return true;
5016 return false;
5019 function isCollapsedNode(node) {
5020 var type = node.nodeType;
5021 return type == 7 /* PROCESSING_INSTRUCTION */ ||
5022 type == 8 /* COMMENT */ ||
5023 isHidden(node) ||
5024 /^(script|style)$/i.test(node.nodeName) ||
5025 isVisibilityHiddenTextNode(node) ||
5026 isCollapsedWhitespaceNode(node);
5029 function isIgnoredNode(node, win) {
5030 var type = node.nodeType;
5031 return type == 7 /* PROCESSING_INSTRUCTION */ ||
5032 type == 8 /* COMMENT */ ||
5033 (type == 1 && getComputedDisplay(node, win) == "none");
5036 /*----------------------------------------------------------------------------------------------------------------*/
5038 // Possibly overengineered caching system to prevent repeated DOM calls slowing everything down
5040 function Cache() {
5041 this.store = {};
5044 Cache.prototype = {
5045 get: function(key) {
5046 return this.store.hasOwnProperty(key) ? this.store[key] : null;
5047 },
5049 set: function(key, value) {
5050 return this.store[key] = value;
5052 };
5054 var cachedCount = 0, uncachedCount = 0;
5056 function createCachingGetter(methodName, func, objProperty) {
5057 return function(args) {
5058 var cache = this.cache;
5059 if (cache.hasOwnProperty(methodName)) {
5060 cachedCount++;
5061 return cache[methodName];
5062 } else {
5063 uncachedCount++;
5064 var value = func.call(this, objProperty ? this[objProperty] : this, args);
5065 cache[methodName] = value;
5066 return value;
5068 };
5071 /*----------------------------------------------------------------------------------------------------------------*/
5073 function NodeWrapper(node, session) {
5074 this.node = node;
5075 this.session = session;
5076 this.cache = new Cache();
5077 this.positions = new Cache();
5080 var nodeProto = {
5081 getPosition: function(offset) {
5082 var positions = this.positions;
5083 return positions.get(offset) || positions.set(offset, new Position(this, offset));
5084 },
5086 toString: function() {
5087 return "[NodeWrapper(" + dom.inspectNode(this.node) + ")]";
5089 };
5091 NodeWrapper.prototype = nodeProto;
5093 var EMPTY = "EMPTY",
5094 NON_SPACE = "NON_SPACE",
5095 UNCOLLAPSIBLE_SPACE = "UNCOLLAPSIBLE_SPACE",
5096 COLLAPSIBLE_SPACE = "COLLAPSIBLE_SPACE",
5097 TRAILING_SPACE_BEFORE_BLOCK = "TRAILING_SPACE_BEFORE_BLOCK",
5098 TRAILING_SPACE_IN_BLOCK = "TRAILING_SPACE_IN_BLOCK",
5099 TRAILING_SPACE_BEFORE_BR = "TRAILING_SPACE_BEFORE_BR",
5100 PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK = "PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK",
5101 TRAILING_LINE_BREAK_AFTER_BR = "TRAILING_LINE_BREAK_AFTER_BR",
5102 INCLUDED_TRAILING_LINE_BREAK_AFTER_BR = "INCLUDED_TRAILING_LINE_BREAK_AFTER_BR";
5104 extend(nodeProto, {
5105 isCharacterDataNode: createCachingGetter("isCharacterDataNode", dom.isCharacterDataNode, "node"),
5106 getNodeIndex: createCachingGetter("nodeIndex", dom.getNodeIndex, "node"),
5107 getLength: createCachingGetter("nodeLength", dom.getNodeLength, "node"),
5108 containsPositions: createCachingGetter("containsPositions", containsPositions, "node"),
5109 isWhitespace: createCachingGetter("isWhitespace", isWhitespaceNode, "node"),
5110 isCollapsedWhitespace: createCachingGetter("isCollapsedWhitespace", isCollapsedWhitespaceNode, "node"),
5111 getComputedDisplay: createCachingGetter("computedDisplay", getComputedDisplay, "node"),
5112 isCollapsed: createCachingGetter("collapsed", isCollapsedNode, "node"),
5113 isIgnored: createCachingGetter("ignored", isIgnoredNode, "node"),
5114 next: createCachingGetter("nextPos", nextNode, "node"),
5115 previous: createCachingGetter("previous", previousNode, "node"),
5117 getTextNodeInfo: createCachingGetter("textNodeInfo", function(textNode) {
5118 var spaceRegex = null, collapseSpaces = false;
5119 var cssWhitespace = getComputedStyleProperty(textNode.parentNode, "whiteSpace");
5120 var preLine = (cssWhitespace == "pre-line");
5121 if (preLine) {
5122 spaceRegex = spacesMinusLineBreaksRegex;
5123 collapseSpaces = true;
5124 } else if (cssWhitespace == "normal" || cssWhitespace == "nowrap") {
5125 spaceRegex = spacesRegex;
5126 collapseSpaces = true;
5129 return {
5130 node: textNode,
5131 text: textNode.data,
5132 spaceRegex: spaceRegex,
5133 collapseSpaces: collapseSpaces,
5134 preLine: preLine
5135 };
5136 }, "node"),
5138 hasInnerText: createCachingGetter("hasInnerText", function(el, backward) {
5139 var session = this.session;
5140 var posAfterEl = session.getPosition(el.parentNode, this.getNodeIndex() + 1);
5141 var firstPosInEl = session.getPosition(el, 0);
5143 var pos = backward ? posAfterEl : firstPosInEl;
5144 var endPos = backward ? firstPosInEl : posAfterEl;
5146 /*
5147 <body><p>X </p><p>Y</p></body>
5149 Positions:
5151 body:0:""
5152 p:0:""
5153 text:0:""
5154 text:1:"X"
5155 text:2:TRAILING_SPACE_IN_BLOCK
5156 text:3:COLLAPSED_SPACE
5157 p:1:""
5158 body:1:"\n"
5159 p:0:""
5160 text:0:""
5161 text:1:"Y"
5163 A character is a TRAILING_SPACE_IN_BLOCK iff:
5165 - There is no uncollapsed character after it within the visible containing block element
5167 A character is a TRAILING_SPACE_BEFORE_BR iff:
5169 - There is no uncollapsed character after it preceding a <br> element
5171 An element has inner text iff
5173 - It is not hidden
5174 - It contains an uncollapsed character
5176 All trailing spaces (pre-line, before <br>, end of block) require definite non-empty characters to render.
5177 */
5179 while (pos !== endPos) {
5180 pos.prepopulateChar();
5181 if (pos.isDefinitelyNonEmpty()) {
5182 return true;
5184 pos = backward ? pos.previousVisible() : pos.nextVisible();
5187 return false;
5188 }, "node"),
5190 isRenderedBlock: createCachingGetter("isRenderedBlock", function(el) {
5191 // Ensure that a block element containing a <br> is considered to have inner text
5192 var brs = el.getElementsByTagName("br");
5193 for (var i = 0, len = brs.length; i < len; ++i) {
5194 if (!isCollapsedNode(brs[i])) {
5195 return true;
5198 return this.hasInnerText();
5199 }, "node"),
5201 getTrailingSpace: createCachingGetter("trailingSpace", function(el) {
5202 if (el.tagName.toLowerCase() == "br") {
5203 return "";
5204 } else {
5205 switch (this.getComputedDisplay()) {
5206 case "inline":
5207 var child = el.lastChild;
5208 while (child) {
5209 if (!isIgnoredNode(child)) {
5210 return (child.nodeType == 1) ? this.session.getNodeWrapper(child).getTrailingSpace() : "";
5212 child = child.previousSibling;
5214 break;
5215 case "inline-block":
5216 case "inline-table":
5217 case "none":
5218 case "table-column":
5219 case "table-column-group":
5220 break;
5221 case "table-cell":
5222 return "\t";
5223 default:
5224 return this.isRenderedBlock(true) ? "\n" : "";
5227 return "";
5228 }, "node"),
5230 getLeadingSpace: createCachingGetter("leadingSpace", function(el) {
5231 switch (this.getComputedDisplay()) {
5232 case "inline":
5233 case "inline-block":
5234 case "inline-table":
5235 case "none":
5236 case "table-column":
5237 case "table-column-group":
5238 case "table-cell":
5239 break;
5240 default:
5241 return this.isRenderedBlock(false) ? "\n" : "";
5243 return "";
5244 }, "node")
5245 });
5247 /*----------------------------------------------------------------------------------------------------------------*/
5249 function Position(nodeWrapper, offset) {
5250 this.offset = offset;
5251 this.nodeWrapper = nodeWrapper;
5252 this.node = nodeWrapper.node;
5253 this.session = nodeWrapper.session;
5254 this.cache = new Cache();
5257 function inspectPosition() {
5258 return "[Position(" + dom.inspectNode(this.node) + ":" + this.offset + ")]";
5261 var positionProto = {
5262 character: "",
5263 characterType: EMPTY,
5264 isBr: false,
5266 /*
5267 This method:
5268 - Fully populates positions that have characters that can be determined independently of any other characters.
5269 - Populates most types of space positions with a provisional character. The character is finalized later.
5270 */
5271 prepopulateChar: function() {
5272 var pos = this;
5273 if (!pos.prepopulatedChar) {
5274 var node = pos.node, offset = pos.offset;
5275 var visibleChar = "", charType = EMPTY;
5276 var finalizedChar = false;
5277 if (offset > 0) {
5278 if (node.nodeType == 3) {
5279 var text = node.data;
5280 var textChar = text.charAt(offset - 1);
5282 var nodeInfo = pos.nodeWrapper.getTextNodeInfo();
5283 var spaceRegex = nodeInfo.spaceRegex;
5284 if (nodeInfo.collapseSpaces) {
5285 if (spaceRegex.test(textChar)) {
5286 // "If the character at position is from set, append a single space (U+0020) to newdata and advance
5287 // position until the character at position is not from set."
5289 // We also need to check for the case where we're in a pre-line and we have a space preceding a
5290 // line break, because such spaces are collapsed in some browsers
5291 if (offset > 1 && spaceRegex.test(text.charAt(offset - 2))) {
5292 } else if (nodeInfo.preLine && text.charAt(offset) === "\n") {
5293 visibleChar = " ";
5294 charType = PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK;
5295 } else {
5296 visibleChar = " ";
5297 //pos.checkForFollowingLineBreak = true;
5298 charType = COLLAPSIBLE_SPACE;
5300 } else {
5301 visibleChar = textChar;
5302 charType = NON_SPACE;
5303 finalizedChar = true;
5305 } else {
5306 visibleChar = textChar;
5307 charType = UNCOLLAPSIBLE_SPACE;
5308 finalizedChar = true;
5310 } else {
5311 var nodePassed = node.childNodes[offset - 1];
5312 if (nodePassed && nodePassed.nodeType == 1 && !isCollapsedNode(nodePassed)) {
5313 if (nodePassed.tagName.toLowerCase() == "br") {
5314 visibleChar = "\n";
5315 pos.isBr = true;
5316 charType = COLLAPSIBLE_SPACE;
5317 finalizedChar = false;
5318 } else {
5319 pos.checkForTrailingSpace = true;
5323 // Check the leading space of the next node for the case when a block element follows an inline
5324 // element or text node. In that case, there is an implied line break between the two nodes.
5325 if (!visibleChar) {
5326 var nextNode = node.childNodes[offset];
5327 if (nextNode && nextNode.nodeType == 1 && !isCollapsedNode(nextNode)) {
5328 pos.checkForLeadingSpace = true;
5334 pos.prepopulatedChar = true;
5335 pos.character = visibleChar;
5336 pos.characterType = charType;
5337 pos.isCharInvariant = finalizedChar;
5339 },
5341 isDefinitelyNonEmpty: function() {
5342 var charType = this.characterType;
5343 return charType == NON_SPACE || charType == UNCOLLAPSIBLE_SPACE;
5344 },
5346 // Resolve leading and trailing spaces, which may involve prepopulating other positions
5347 resolveLeadingAndTrailingSpaces: function() {
5348 if (!this.prepopulatedChar) {
5349 this.prepopulateChar();
5351 if (this.checkForTrailingSpace) {
5352 var trailingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset - 1]).getTrailingSpace();
5353 if (trailingSpace) {
5354 this.isTrailingSpace = true;
5355 this.character = trailingSpace;
5356 this.characterType = COLLAPSIBLE_SPACE;
5358 this.checkForTrailingSpace = false;
5360 if (this.checkForLeadingSpace) {
5361 var leadingSpace = this.session.getNodeWrapper(this.node.childNodes[this.offset]).getLeadingSpace();
5362 if (leadingSpace) {
5363 this.isLeadingSpace = true;
5364 this.character = leadingSpace;
5365 this.characterType = COLLAPSIBLE_SPACE;
5367 this.checkForLeadingSpace = false;
5369 },
5371 getPrecedingUncollapsedPosition: function(characterOptions) {
5372 var pos = this, character;
5373 while ( (pos = pos.previousVisible()) ) {
5374 character = pos.getCharacter(characterOptions);
5375 if (character !== "") {
5376 return pos;
5380 return null;
5381 },
5383 getCharacter: function(characterOptions) {
5384 this.resolveLeadingAndTrailingSpaces();
5386 var thisChar = this.character, returnChar;
5388 // Check if character is ignored
5389 var ignoredChars = normalizeIgnoredCharacters(characterOptions.ignoreCharacters);
5390 var isIgnoredCharacter = (thisChar !== "" && ignoredChars.indexOf(thisChar) > -1);
5392 // Check if this position's character is invariant (i.e. not dependent on character options) and return it
5393 // if so
5394 if (this.isCharInvariant) {
5395 returnChar = isIgnoredCharacter ? "" : thisChar;
5396 return returnChar;
5399 var cacheKey = ["character", characterOptions.includeSpaceBeforeBr, characterOptions.includeBlockContentTrailingSpace, characterOptions.includePreLineTrailingSpace, ignoredChars].join("_");
5400 var cachedChar = this.cache.get(cacheKey);
5401 if (cachedChar !== null) {
5402 return cachedChar;
5405 // We need to actually get the character now
5406 var character = "";
5407 var collapsible = (this.characterType == COLLAPSIBLE_SPACE);
5409 var nextPos, previousPos;
5410 var gotPreviousPos = false;
5411 var pos = this;
5413 function getPreviousPos() {
5414 if (!gotPreviousPos) {
5415 previousPos = pos.getPrecedingUncollapsedPosition(characterOptions);
5416 gotPreviousPos = true;
5418 return previousPos;
5421 // Disallow a collapsible space that is followed by a line break or is the last character
5422 if (collapsible) {
5423 // Allow a trailing space that we've previously determined should be included
5424 if (this.type == INCLUDED_TRAILING_LINE_BREAK_AFTER_BR) {
5425 character = "\n";
5427 // Disallow a collapsible space that follows a trailing space or line break, or is the first character,
5428 // or follows a collapsible included space
5429 else if (thisChar == " " &&
5430 (!getPreviousPos() || previousPos.isTrailingSpace || previousPos.character == "\n" || (previousPos.character == " " && previousPos.characterType == COLLAPSIBLE_SPACE))) {
5432 // Allow a leading line break unless it follows a line break
5433 else if (thisChar == "\n" && this.isLeadingSpace) {
5434 if (getPreviousPos() && previousPos.character != "\n") {
5435 character = "\n";
5436 } else {
5438 } else {
5439 nextPos = this.nextUncollapsed();
5440 if (nextPos) {
5441 if (nextPos.isBr) {
5442 this.type = TRAILING_SPACE_BEFORE_BR;
5443 } else if (nextPos.isTrailingSpace && nextPos.character == "\n") {
5444 this.type = TRAILING_SPACE_IN_BLOCK;
5445 } else if (nextPos.isLeadingSpace && nextPos.character == "\n") {
5446 this.type = TRAILING_SPACE_BEFORE_BLOCK;
5449 if (nextPos.character == "\n") {
5450 if (this.type == TRAILING_SPACE_BEFORE_BR && !characterOptions.includeSpaceBeforeBr) {
5451 } else if (this.type == TRAILING_SPACE_BEFORE_BLOCK && !characterOptions.includeSpaceBeforeBlock) {
5452 } else if (this.type == TRAILING_SPACE_IN_BLOCK && nextPos.isTrailingSpace && !characterOptions.includeBlockContentTrailingSpace) {
5453 } else if (this.type == PRE_LINE_TRAILING_SPACE_BEFORE_LINE_BREAK && nextPos.type == NON_SPACE && !characterOptions.includePreLineTrailingSpace) {
5454 } else if (thisChar == "\n") {
5455 if (nextPos.isTrailingSpace) {
5456 if (this.isTrailingSpace) {
5457 } else if (this.isBr) {
5458 nextPos.type = TRAILING_LINE_BREAK_AFTER_BR;
5460 if (getPreviousPos() && previousPos.isLeadingSpace && !previousPos.isTrailingSpace && previousPos.character == "\n") {
5461 nextPos.character = "";
5462 } else {
5463 nextPos.type = INCLUDED_TRAILING_LINE_BREAK_AFTER_BR;
5466 } else {
5467 character = "\n";
5469 } else if (thisChar == " ") {
5470 character = " ";
5471 } else {
5473 } else {
5474 character = thisChar;
5476 } else {
5481 if (ignoredChars.indexOf(character) > -1) {
5482 character = "";
5486 this.cache.set(cacheKey, character);
5488 return character;
5489 },
5491 equals: function(pos) {
5492 return !!pos && this.node === pos.node && this.offset === pos.offset;
5493 },
5495 inspect: inspectPosition,
5497 toString: function() {
5498 return this.character;
5500 };
5502 Position.prototype = positionProto;
5504 extend(positionProto, {
5505 next: createCachingGetter("nextPos", function(pos) {
5506 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
5507 if (!node) {
5508 return null;
5510 var nextNode, nextOffset, child;
5511 if (offset == nodeWrapper.getLength()) {
5512 // Move onto the next node
5513 nextNode = node.parentNode;
5514 nextOffset = nextNode ? nodeWrapper.getNodeIndex() + 1 : 0;
5515 } else {
5516 if (nodeWrapper.isCharacterDataNode()) {
5517 nextNode = node;
5518 nextOffset = offset + 1;
5519 } else {
5520 child = node.childNodes[offset];
5521 // Go into the children next, if children there are
5522 if (session.getNodeWrapper(child).containsPositions()) {
5523 nextNode = child;
5524 nextOffset = 0;
5525 } else {
5526 nextNode = node;
5527 nextOffset = offset + 1;
5532 return nextNode ? session.getPosition(nextNode, nextOffset) : null;
5533 }),
5535 previous: createCachingGetter("previous", function(pos) {
5536 var nodeWrapper = pos.nodeWrapper, node = pos.node, offset = pos.offset, session = nodeWrapper.session;
5537 var previousNode, previousOffset, child;
5538 if (offset == 0) {
5539 previousNode = node.parentNode;
5540 previousOffset = previousNode ? nodeWrapper.getNodeIndex() : 0;
5541 } else {
5542 if (nodeWrapper.isCharacterDataNode()) {
5543 previousNode = node;
5544 previousOffset = offset - 1;
5545 } else {
5546 child = node.childNodes[offset - 1];
5547 // Go into the children next, if children there are
5548 if (session.getNodeWrapper(child).containsPositions()) {
5549 previousNode = child;
5550 previousOffset = dom.getNodeLength(child);
5551 } else {
5552 previousNode = node;
5553 previousOffset = offset - 1;
5557 return previousNode ? session.getPosition(previousNode, previousOffset) : null;
5558 }),
5560 /*
5561 Next and previous position moving functions that filter out
5563 - Hidden (CSS visibility/display) elements
5564 - Script and style elements
5565 */
5566 nextVisible: createCachingGetter("nextVisible", function(pos) {
5567 var next = pos.next();
5568 if (!next) {
5569 return null;
5571 var nodeWrapper = next.nodeWrapper, node = next.node;
5572 var newPos = next;
5573 if (nodeWrapper.isCollapsed()) {
5574 // We're skipping this node and all its descendants
5575 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex() + 1);
5577 return newPos;
5578 }),
5580 nextUncollapsed: createCachingGetter("nextUncollapsed", function(pos) {
5581 var nextPos = pos;
5582 while ( (nextPos = nextPos.nextVisible()) ) {
5583 nextPos.resolveLeadingAndTrailingSpaces();
5584 if (nextPos.character !== "") {
5585 return nextPos;
5588 return null;
5589 }),
5591 previousVisible: createCachingGetter("previousVisible", function(pos) {
5592 var previous = pos.previous();
5593 if (!previous) {
5594 return null;
5596 var nodeWrapper = previous.nodeWrapper, node = previous.node;
5597 var newPos = previous;
5598 if (nodeWrapper.isCollapsed()) {
5599 // We're skipping this node and all its descendants
5600 newPos = nodeWrapper.session.getPosition(node.parentNode, nodeWrapper.getNodeIndex());
5602 return newPos;
5603 })
5604 });
5606 /*----------------------------------------------------------------------------------------------------------------*/
5608 var currentSession = null;
5610 var Session = (function() {
5611 function createWrapperCache(nodeProperty) {
5612 var cache = new Cache();
5614 return {
5615 get: function(node) {
5616 var wrappersByProperty = cache.get(node[nodeProperty]);
5617 if (wrappersByProperty) {
5618 for (var i = 0, wrapper; wrapper = wrappersByProperty[i++]; ) {
5619 if (wrapper.node === node) {
5620 return wrapper;
5624 return null;
5625 },
5627 set: function(nodeWrapper) {
5628 var property = nodeWrapper.node[nodeProperty];
5629 var wrappersByProperty = cache.get(property) || cache.set(property, []);
5630 wrappersByProperty.push(nodeWrapper);
5632 };
5635 var uniqueIDSupported = util.isHostProperty(document.documentElement, "uniqueID");
5637 function Session() {
5638 this.initCaches();
5641 Session.prototype = {
5642 initCaches: function() {
5643 this.elementCache = uniqueIDSupported ? (function() {
5644 var elementsCache = new Cache();
5646 return {
5647 get: function(el) {
5648 return elementsCache.get(el.uniqueID);
5649 },
5651 set: function(elWrapper) {
5652 elementsCache.set(elWrapper.node.uniqueID, elWrapper);
5654 };
5655 })() : createWrapperCache("tagName");
5657 // Store text nodes keyed by data, although we may need to truncate this
5658 this.textNodeCache = createWrapperCache("data");
5659 this.otherNodeCache = createWrapperCache("nodeName");
5660 },
5662 getNodeWrapper: function(node) {
5663 var wrapperCache;
5664 switch (node.nodeType) {
5665 case 1:
5666 wrapperCache = this.elementCache;
5667 break;
5668 case 3:
5669 wrapperCache = this.textNodeCache;
5670 break;
5671 default:
5672 wrapperCache = this.otherNodeCache;
5673 break;
5676 var wrapper = wrapperCache.get(node);
5677 if (!wrapper) {
5678 wrapper = new NodeWrapper(node, this);
5679 wrapperCache.set(wrapper);
5681 return wrapper;
5682 },
5684 getPosition: function(node, offset) {
5685 return this.getNodeWrapper(node).getPosition(offset);
5686 },
5688 getRangeBoundaryPosition: function(range, isStart) {
5689 var prefix = isStart ? "start" : "end";
5690 return this.getPosition(range[prefix + "Container"], range[prefix + "Offset"]);
5691 },
5693 detach: function() {
5694 this.elementCache = this.textNodeCache = this.otherNodeCache = null;
5696 };
5698 return Session;
5699 })();
5701 /*----------------------------------------------------------------------------------------------------------------*/
5703 function startSession() {
5704 endSession();
5705 return (currentSession = new Session());
5708 function getSession() {
5709 return currentSession || startSession();
5712 function endSession() {
5713 if (currentSession) {
5714 currentSession.detach();
5716 currentSession = null;
5719 /*----------------------------------------------------------------------------------------------------------------*/
5721 // Extensions to the rangy.dom utility object
5723 extend(dom, {
5724 nextNode: nextNode,
5725 previousNode: previousNode
5726 });
5728 /*----------------------------------------------------------------------------------------------------------------*/
5730 function createCharacterIterator(startPos, backward, endPos, characterOptions) {
5732 // Adjust the end position to ensure that it is actually reached
5733 if (endPos) {
5734 if (backward) {
5735 if (isCollapsedNode(endPos.node)) {
5736 endPos = startPos.previousVisible();
5738 } else {
5739 if (isCollapsedNode(endPos.node)) {
5740 endPos = endPos.nextVisible();
5745 var pos = startPos, finished = false;
5747 function next() {
5748 var charPos = null;
5749 if (backward) {
5750 charPos = pos;
5751 if (!finished) {
5752 pos = pos.previousVisible();
5753 finished = !pos || (endPos && pos.equals(endPos));
5755 } else {
5756 if (!finished) {
5757 charPos = pos = pos.nextVisible();
5758 finished = !pos || (endPos && pos.equals(endPos));
5761 if (finished) {
5762 pos = null;
5764 return charPos;
5767 var previousTextPos, returnPreviousTextPos = false;
5769 return {
5770 next: function() {
5771 if (returnPreviousTextPos) {
5772 returnPreviousTextPos = false;
5773 return previousTextPos;
5774 } else {
5775 var pos, character;
5776 while ( (pos = next()) ) {
5777 character = pos.getCharacter(characterOptions);
5778 if (character) {
5779 previousTextPos = pos;
5780 return pos;
5783 return null;
5785 },
5787 rewind: function() {
5788 if (previousTextPos) {
5789 returnPreviousTextPos = true;
5790 } else {
5791 throw module.createError("createCharacterIterator: cannot rewind. Only one position can be rewound.");
5793 },
5795 dispose: function() {
5796 startPos = endPos = null;
5798 };
5801 var arrayIndexOf = Array.prototype.indexOf ?
5802 function(arr, val) {
5803 return arr.indexOf(val);
5804 } :
5805 function(arr, val) {
5806 for (var i = 0, len = arr.length; i < len; ++i) {
5807 if (arr[i] === val) {
5808 return i;
5811 return -1;
5812 };
5814 // Provides a pair of iterators over text positions, tokenized. Transparently requests more text when next()
5815 // is called and there is no more tokenized text
5816 function createTokenizedTextProvider(pos, characterOptions, wordOptions) {
5817 var forwardIterator = createCharacterIterator(pos, false, null, characterOptions);
5818 var backwardIterator = createCharacterIterator(pos, true, null, characterOptions);
5819 var tokenizer = wordOptions.tokenizer;
5821 // Consumes a word and the whitespace beyond it
5822 function consumeWord(forward) {
5823 var pos, textChar;
5824 var newChars = [], it = forward ? forwardIterator : backwardIterator;
5826 var passedWordBoundary = false, insideWord = false;
5828 while ( (pos = it.next()) ) {
5829 textChar = pos.character;
5832 if (allWhiteSpaceRegex.test(textChar)) {
5833 if (insideWord) {
5834 insideWord = false;
5835 passedWordBoundary = true;
5837 } else {
5838 if (passedWordBoundary) {
5839 it.rewind();
5840 break;
5841 } else {
5842 insideWord = true;
5845 newChars.push(pos);
5849 return newChars;
5852 // Get initial word surrounding initial position and tokenize it
5853 var forwardChars = consumeWord(true);
5854 var backwardChars = consumeWord(false).reverse();
5855 var tokens = tokenize(backwardChars.concat(forwardChars), wordOptions, tokenizer);
5857 // Create initial token buffers
5858 var forwardTokensBuffer = forwardChars.length ?
5859 tokens.slice(arrayIndexOf(tokens, forwardChars[0].token)) : [];
5861 var backwardTokensBuffer = backwardChars.length ?
5862 tokens.slice(0, arrayIndexOf(tokens, backwardChars.pop().token) + 1) : [];
5864 function inspectBuffer(buffer) {
5865 var textPositions = ["[" + buffer.length + "]"];
5866 for (var i = 0; i < buffer.length; ++i) {
5867 textPositions.push("(word: " + buffer[i] + ", is word: " + buffer[i].isWord + ")");
5869 return textPositions;
5873 return {
5874 nextEndToken: function() {
5875 var lastToken, forwardChars;
5877 // If we're down to the last token, consume character chunks until we have a word or run out of
5878 // characters to consume
5879 while ( forwardTokensBuffer.length == 1 &&
5880 !(lastToken = forwardTokensBuffer[0]).isWord &&
5881 (forwardChars = consumeWord(true)).length > 0) {
5883 // Merge trailing non-word into next word and tokenize
5884 forwardTokensBuffer = tokenize(lastToken.chars.concat(forwardChars), wordOptions, tokenizer);
5887 return forwardTokensBuffer.shift();
5888 },
5890 previousStartToken: function() {
5891 var lastToken, backwardChars;
5893 // If we're down to the last token, consume character chunks until we have a word or run out of
5894 // characters to consume
5895 while ( backwardTokensBuffer.length == 1 &&
5896 !(lastToken = backwardTokensBuffer[0]).isWord &&
5897 (backwardChars = consumeWord(false)).length > 0) {
5899 // Merge leading non-word into next word and tokenize
5900 backwardTokensBuffer = tokenize(backwardChars.reverse().concat(lastToken.chars), wordOptions, tokenizer);
5903 return backwardTokensBuffer.pop();
5904 },
5906 dispose: function() {
5907 forwardIterator.dispose();
5908 backwardIterator.dispose();
5909 forwardTokensBuffer = backwardTokensBuffer = null;
5911 };
5914 function movePositionBy(pos, unit, count, characterOptions, wordOptions) {
5915 var unitsMoved = 0, currentPos, newPos = pos, charIterator, nextPos, absCount = Math.abs(count), token;
5916 if (count !== 0) {
5917 var backward = (count < 0);
5919 switch (unit) {
5920 case CHARACTER:
5921 charIterator = createCharacterIterator(pos, backward, null, characterOptions);
5922 while ( (currentPos = charIterator.next()) && unitsMoved < absCount ) {
5923 ++unitsMoved;
5924 newPos = currentPos;
5926 nextPos = currentPos;
5927 charIterator.dispose();
5928 break;
5929 case WORD:
5930 var tokenizedTextProvider = createTokenizedTextProvider(pos, characterOptions, wordOptions);
5931 var next = backward ? tokenizedTextProvider.previousStartToken : tokenizedTextProvider.nextEndToken;
5933 while ( (token = next()) && unitsMoved < absCount ) {
5934 if (token.isWord) {
5935 ++unitsMoved;
5936 newPos = backward ? token.chars[0] : token.chars[token.chars.length - 1];
5939 break;
5940 default:
5941 throw new Error("movePositionBy: unit '" + unit + "' not implemented");
5944 // Perform any necessary position tweaks
5945 if (backward) {
5946 newPos = newPos.previousVisible();
5947 unitsMoved = -unitsMoved;
5948 } else if (newPos && newPos.isLeadingSpace && !newPos.isTrailingSpace) {
5949 // Tweak the position for the case of a leading space. The problem is that an uncollapsed leading space
5950 // before a block element (for example, the line break between "1" and "2" in the following HTML:
5951 // "1<p>2</p>") is considered to be attached to the position immediately before the block element, which
5952 // corresponds with a different selection position in most browsers from the one we want (i.e. at the
5953 // start of the contents of the block element). We get round this by advancing the position returned to
5954 // the last possible equivalent visible position.
5955 if (unit == WORD) {
5956 charIterator = createCharacterIterator(pos, false, null, characterOptions);
5957 nextPos = charIterator.next();
5958 charIterator.dispose();
5960 if (nextPos) {
5961 newPos = nextPos.previousVisible();
5967 return {
5968 position: newPos,
5969 unitsMoved: unitsMoved
5970 };
5973 function createRangeCharacterIterator(session, range, characterOptions, backward) {
5974 var rangeStart = session.getRangeBoundaryPosition(range, true);
5975 var rangeEnd = session.getRangeBoundaryPosition(range, false);
5976 var itStart = backward ? rangeEnd : rangeStart;
5977 var itEnd = backward ? rangeStart : rangeEnd;
5979 return createCharacterIterator(itStart, !!backward, itEnd, characterOptions);
5982 function getRangeCharacters(session, range, characterOptions) {
5984 var chars = [], it = createRangeCharacterIterator(session, range, characterOptions), pos;
5985 while ( (pos = it.next()) ) {
5986 chars.push(pos);
5989 it.dispose();
5990 return chars;
5993 function isWholeWord(startPos, endPos, wordOptions) {
5994 var range = api.createRange(startPos.node);
5995 range.setStartAndEnd(startPos.node, startPos.offset, endPos.node, endPos.offset);
5996 return !range.expand("word", { wordOptions: wordOptions });
5999 function findTextFromPosition(initialPos, searchTerm, isRegex, searchScopeRange, findOptions) {
6000 var backward = isDirectionBackward(findOptions.direction);
6001 var it = createCharacterIterator(
6002 initialPos,
6003 backward,
6004 initialPos.session.getRangeBoundaryPosition(searchScopeRange, backward),
6005 findOptions.characterOptions
6006 );
6007 var text = "", chars = [], pos, currentChar, matchStartIndex, matchEndIndex;
6008 var result, insideRegexMatch;
6009 var returnValue = null;
6011 function handleMatch(startIndex, endIndex) {
6012 var startPos = chars[startIndex].previousVisible();
6013 var endPos = chars[endIndex - 1];
6014 var valid = (!findOptions.wholeWordsOnly || isWholeWord(startPos, endPos, findOptions.wordOptions));
6016 return {
6017 startPos: startPos,
6018 endPos: endPos,
6019 valid: valid
6020 };
6023 while ( (pos = it.next()) ) {
6024 currentChar = pos.character;
6025 if (!isRegex && !findOptions.caseSensitive) {
6026 currentChar = currentChar.toLowerCase();
6029 if (backward) {
6030 chars.unshift(pos);
6031 text = currentChar + text;
6032 } else {
6033 chars.push(pos);
6034 text += currentChar;
6037 if (isRegex) {
6038 result = searchTerm.exec(text);
6039 if (result) {
6040 matchStartIndex = result.index;
6041 matchEndIndex = matchStartIndex + result[0].length;
6042 if (insideRegexMatch) {
6043 // Check whether the match is now over
6044 if ((!backward && matchEndIndex < text.length) || (backward && matchStartIndex > 0)) {
6045 returnValue = handleMatch(matchStartIndex, matchEndIndex);
6046 break;
6048 } else {
6049 insideRegexMatch = true;
6052 } else if ( (matchStartIndex = text.indexOf(searchTerm)) != -1 ) {
6053 returnValue = handleMatch(matchStartIndex, matchStartIndex + searchTerm.length);
6054 break;
6058 // Check whether regex match extends to the end of the range
6059 if (insideRegexMatch) {
6060 returnValue = handleMatch(matchStartIndex, matchEndIndex);
6062 it.dispose();
6064 return returnValue;
6067 function createEntryPointFunction(func) {
6068 return function() {
6069 var sessionRunning = !!currentSession;
6070 var session = getSession();
6071 var args = [session].concat( util.toArray(arguments) );
6072 var returnValue = func.apply(this, args);
6073 if (!sessionRunning) {
6074 endSession();
6076 return returnValue;
6077 };
6080 /*----------------------------------------------------------------------------------------------------------------*/
6082 // Extensions to the Rangy Range object
6084 function createRangeBoundaryMover(isStart, collapse) {
6085 /*
6086 Unit can be "character" or "word"
6087 Options:
6089 - includeTrailingSpace
6090 - wordRegex
6091 - tokenizer
6092 - collapseSpaceBeforeLineBreak
6093 */
6094 return createEntryPointFunction(
6095 function(session, unit, count, moveOptions) {
6096 if (typeof count == UNDEF) {
6097 count = unit;
6098 unit = CHARACTER;
6100 moveOptions = createNestedOptions(moveOptions, defaultMoveOptions);
6102 var boundaryIsStart = isStart;
6103 if (collapse) {
6104 boundaryIsStart = (count >= 0);
6105 this.collapse(!boundaryIsStart);
6107 var moveResult = movePositionBy(session.getRangeBoundaryPosition(this, boundaryIsStart), unit, count, moveOptions.characterOptions, moveOptions.wordOptions);
6108 var newPos = moveResult.position;
6109 this[boundaryIsStart ? "setStart" : "setEnd"](newPos.node, newPos.offset);
6110 return moveResult.unitsMoved;
6112 );
6115 function createRangeTrimmer(isStart) {
6116 return createEntryPointFunction(
6117 function(session, characterOptions) {
6118 characterOptions = createOptions(characterOptions, defaultCharacterOptions);
6119 var pos;
6120 var it = createRangeCharacterIterator(session, this, characterOptions, !isStart);
6121 var trimCharCount = 0;
6122 while ( (pos = it.next()) && allWhiteSpaceRegex.test(pos.character) ) {
6123 ++trimCharCount;
6125 it.dispose();
6126 var trimmed = (trimCharCount > 0);
6127 if (trimmed) {
6128 this[isStart ? "moveStart" : "moveEnd"](
6129 "character",
6130 isStart ? trimCharCount : -trimCharCount,
6131 { characterOptions: characterOptions }
6132 );
6134 return trimmed;
6136 );
6139 extend(api.rangePrototype, {
6140 moveStart: createRangeBoundaryMover(true, false),
6142 moveEnd: createRangeBoundaryMover(false, false),
6144 move: createRangeBoundaryMover(true, true),
6146 trimStart: createRangeTrimmer(true),
6148 trimEnd: createRangeTrimmer(false),
6150 trim: createEntryPointFunction(
6151 function(session, characterOptions) {
6152 var startTrimmed = this.trimStart(characterOptions), endTrimmed = this.trimEnd(characterOptions);
6153 return startTrimmed || endTrimmed;
6155 ),
6157 expand: createEntryPointFunction(
6158 function(session, unit, expandOptions) {
6159 var moved = false;
6160 expandOptions = createNestedOptions(expandOptions, defaultExpandOptions);
6161 var characterOptions = expandOptions.characterOptions;
6162 if (!unit) {
6163 unit = CHARACTER;
6165 if (unit == WORD) {
6166 var wordOptions = expandOptions.wordOptions;
6167 var startPos = session.getRangeBoundaryPosition(this, true);
6168 var endPos = session.getRangeBoundaryPosition(this, false);
6170 var startTokenizedTextProvider = createTokenizedTextProvider(startPos, characterOptions, wordOptions);
6171 var startToken = startTokenizedTextProvider.nextEndToken();
6172 var newStartPos = startToken.chars[0].previousVisible();
6173 var endToken, newEndPos;
6175 if (this.collapsed) {
6176 endToken = startToken;
6177 } else {
6178 var endTokenizedTextProvider = createTokenizedTextProvider(endPos, characterOptions, wordOptions);
6179 endToken = endTokenizedTextProvider.previousStartToken();
6181 newEndPos = endToken.chars[endToken.chars.length - 1];
6183 if (!newStartPos.equals(startPos)) {
6184 this.setStart(newStartPos.node, newStartPos.offset);
6185 moved = true;
6187 if (newEndPos && !newEndPos.equals(endPos)) {
6188 this.setEnd(newEndPos.node, newEndPos.offset);
6189 moved = true;
6192 if (expandOptions.trim) {
6193 if (expandOptions.trimStart) {
6194 moved = this.trimStart(characterOptions) || moved;
6196 if (expandOptions.trimEnd) {
6197 moved = this.trimEnd(characterOptions) || moved;
6201 return moved;
6202 } else {
6203 return this.moveEnd(CHARACTER, 1, expandOptions);
6206 ),
6208 text: createEntryPointFunction(
6209 function(session, characterOptions) {
6210 return this.collapsed ?
6211 "" : getRangeCharacters(session, this, createOptions(characterOptions, defaultCharacterOptions)).join("");
6213 ),
6215 selectCharacters: createEntryPointFunction(
6216 function(session, containerNode, startIndex, endIndex, characterOptions) {
6217 var moveOptions = { characterOptions: characterOptions };
6218 if (!containerNode) {
6219 containerNode = getBody( this.getDocument() );
6221 this.selectNodeContents(containerNode);
6222 this.collapse(true);
6223 this.moveStart("character", startIndex, moveOptions);
6224 this.collapse(true);
6225 this.moveEnd("character", endIndex - startIndex, moveOptions);
6227 ),
6229 // Character indexes are relative to the start of node
6230 toCharacterRange: createEntryPointFunction(
6231 function(session, containerNode, characterOptions) {
6232 if (!containerNode) {
6233 containerNode = getBody( this.getDocument() );
6235 var parent = containerNode.parentNode, nodeIndex = dom.getNodeIndex(containerNode);
6236 var rangeStartsBeforeNode = (dom.comparePoints(this.startContainer, this.endContainer, parent, nodeIndex) == -1);
6237 var rangeBetween = this.cloneRange();
6238 var startIndex, endIndex;
6239 if (rangeStartsBeforeNode) {
6240 rangeBetween.setStartAndEnd(this.startContainer, this.startOffset, parent, nodeIndex);
6241 startIndex = -rangeBetween.text(characterOptions).length;
6242 } else {
6243 rangeBetween.setStartAndEnd(parent, nodeIndex, this.startContainer, this.startOffset);
6244 startIndex = rangeBetween.text(characterOptions).length;
6246 endIndex = startIndex + this.text(characterOptions).length;
6248 return {
6249 start: startIndex,
6250 end: endIndex
6251 };
6253 ),
6255 findText: createEntryPointFunction(
6256 function(session, searchTermParam, findOptions) {
6257 // Set up options
6258 findOptions = createNestedOptions(findOptions, defaultFindOptions);
6260 // Create word options if we're matching whole words only
6261 if (findOptions.wholeWordsOnly) {
6262 // We don't ever want trailing spaces for search results
6263 findOptions.wordOptions.includeTrailingSpace = false;
6266 var backward = isDirectionBackward(findOptions.direction);
6268 // Create a range representing the search scope if none was provided
6269 var searchScopeRange = findOptions.withinRange;
6270 if (!searchScopeRange) {
6271 searchScopeRange = api.createRange();
6272 searchScopeRange.selectNodeContents(this.getDocument());
6275 // Examine and prepare the search term
6276 var searchTerm = searchTermParam, isRegex = false;
6277 if (typeof searchTerm == "string") {
6278 if (!findOptions.caseSensitive) {
6279 searchTerm = searchTerm.toLowerCase();
6281 } else {
6282 isRegex = true;
6285 var initialPos = session.getRangeBoundaryPosition(this, !backward);
6287 // Adjust initial position if it lies outside the search scope
6288 var comparison = searchScopeRange.comparePoint(initialPos.node, initialPos.offset);
6290 if (comparison === -1) {
6291 initialPos = session.getRangeBoundaryPosition(searchScopeRange, true);
6292 } else if (comparison === 1) {
6293 initialPos = session.getRangeBoundaryPosition(searchScopeRange, false);
6296 var pos = initialPos;
6297 var wrappedAround = false;
6299 // Try to find a match and ignore invalid ones
6300 var findResult;
6301 while (true) {
6302 findResult = findTextFromPosition(pos, searchTerm, isRegex, searchScopeRange, findOptions);
6304 if (findResult) {
6305 if (findResult.valid) {
6306 this.setStartAndEnd(findResult.startPos.node, findResult.startPos.offset, findResult.endPos.node, findResult.endPos.offset);
6307 return true;
6308 } else {
6309 // We've found a match that is not a whole word, so we carry on searching from the point immediately
6310 // after the match
6311 pos = backward ? findResult.startPos : findResult.endPos;
6313 } else if (findOptions.wrap && !wrappedAround) {
6314 // No result found but we're wrapping around and limiting the scope to the unsearched part of the range
6315 searchScopeRange = searchScopeRange.cloneRange();
6316 pos = session.getRangeBoundaryPosition(searchScopeRange, !backward);
6317 searchScopeRange.setBoundary(initialPos.node, initialPos.offset, backward);
6318 wrappedAround = true;
6319 } else {
6320 // Nothing found and we can't wrap around, so we're done
6321 return false;
6325 ),
6327 pasteHtml: function(html) {
6328 this.deleteContents();
6329 if (html) {
6330 var frag = this.createContextualFragment(html);
6331 var lastChild = frag.lastChild;
6332 this.insertNode(frag);
6333 this.collapseAfter(lastChild);
6336 });
6338 /*----------------------------------------------------------------------------------------------------------------*/
6340 // Extensions to the Rangy Selection object
6342 function createSelectionTrimmer(methodName) {
6343 return createEntryPointFunction(
6344 function(session, characterOptions) {
6345 var trimmed = false;
6346 this.changeEachRange(function(range) {
6347 trimmed = range[methodName](characterOptions) || trimmed;
6348 });
6349 return trimmed;
6351 );
6354 extend(api.selectionPrototype, {
6355 expand: createEntryPointFunction(
6356 function(session, unit, expandOptions) {
6357 this.changeEachRange(function(range) {
6358 range.expand(unit, expandOptions);
6359 });
6361 ),
6363 move: createEntryPointFunction(
6364 function(session, unit, count, options) {
6365 var unitsMoved = 0;
6366 if (this.focusNode) {
6367 this.collapse(this.focusNode, this.focusOffset);
6368 var range = this.getRangeAt(0);
6369 if (!options) {
6370 options = {};
6372 options.characterOptions = createOptions(options.characterOptions, defaultCaretCharacterOptions);
6373 unitsMoved = range.move(unit, count, options);
6374 this.setSingleRange(range);
6376 return unitsMoved;
6378 ),
6380 trimStart: createSelectionTrimmer("trimStart"),
6381 trimEnd: createSelectionTrimmer("trimEnd"),
6382 trim: createSelectionTrimmer("trim"),
6384 selectCharacters: createEntryPointFunction(
6385 function(session, containerNode, startIndex, endIndex, direction, characterOptions) {
6386 var range = api.createRange(containerNode);
6387 range.selectCharacters(containerNode, startIndex, endIndex, characterOptions);
6388 this.setSingleRange(range, direction);
6390 ),
6392 saveCharacterRanges: createEntryPointFunction(
6393 function(session, containerNode, characterOptions) {
6394 var ranges = this.getAllRanges(), rangeCount = ranges.length;
6395 var rangeInfos = [];
6397 var backward = rangeCount == 1 && this.isBackward();
6399 for (var i = 0, len = ranges.length; i < len; ++i) {
6400 rangeInfos[i] = {
6401 characterRange: ranges[i].toCharacterRange(containerNode, characterOptions),
6402 backward: backward,
6403 characterOptions: characterOptions
6404 };
6407 return rangeInfos;
6409 ),
6411 restoreCharacterRanges: createEntryPointFunction(
6412 function(session, containerNode, saved) {
6413 this.removeAllRanges();
6414 for (var i = 0, len = saved.length, range, rangeInfo, characterRange; i < len; ++i) {
6415 rangeInfo = saved[i];
6416 characterRange = rangeInfo.characterRange;
6417 range = api.createRange(containerNode);
6418 range.selectCharacters(containerNode, characterRange.start, characterRange.end, rangeInfo.characterOptions);
6419 this.addRange(range, rangeInfo.backward);
6422 ),
6424 text: createEntryPointFunction(
6425 function(session, characterOptions) {
6426 var rangeTexts = [];
6427 for (var i = 0, len = this.rangeCount; i < len; ++i) {
6428 rangeTexts[i] = this.getRangeAt(i).text(characterOptions);
6430 return rangeTexts.join("");
6433 });
6435 /*----------------------------------------------------------------------------------------------------------------*/
6437 // Extensions to the core rangy object
6439 api.innerText = function(el, characterOptions) {
6440 var range = api.createRange(el);
6441 range.selectNodeContents(el);
6442 var text = range.text(characterOptions);
6443 return text;
6444 };
6446 api.createWordIterator = function(startNode, startOffset, iteratorOptions) {
6447 var session = getSession();
6448 iteratorOptions = createNestedOptions(iteratorOptions, defaultWordIteratorOptions);
6449 var startPos = session.getPosition(startNode, startOffset);
6450 var tokenizedTextProvider = createTokenizedTextProvider(startPos, iteratorOptions.characterOptions, iteratorOptions.wordOptions);
6451 var backward = isDirectionBackward(iteratorOptions.direction);
6453 return {
6454 next: function() {
6455 return backward ? tokenizedTextProvider.previousStartToken() : tokenizedTextProvider.nextEndToken();
6456 },
6458 dispose: function() {
6459 tokenizedTextProvider.dispose();
6460 this.next = function() {};
6462 };
6463 };
6465 /*----------------------------------------------------------------------------------------------------------------*/
6467 api.noMutation = function(func) {
6468 var session = getSession();
6469 func(session);
6470 endSession();
6471 };
6473 api.noMutation.createEntryPointFunction = createEntryPointFunction;
6475 api.textRange = {
6476 isBlockNode: isBlockNode,
6477 isCollapsedWhitespaceNode: isCollapsedWhitespaceNode,
6479 createPosition: createEntryPointFunction(
6480 function(session, node, offset) {
6481 return session.getPosition(node, offset);
6484 };
6485 });
6487 /**
6488 * Detect browser support for specific features
6489 */
6490 wysihtml.browser = (function() {
6491 var userAgent = navigator.userAgent,
6492 testElement = document.createElement("div"),
6493 // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect
6494 // We need to be extra careful about Microsoft as it shows increasing tendency of tainting its userAgent strings with false feathers
6495 isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1 && !isIE(),
6496 isWebKit = userAgent.indexOf("AppleWebKit/") !== -1 && !isIE(),
6497 isChrome = userAgent.indexOf("Chrome/") !== -1 && !isIE(),
6498 isOpera = userAgent.indexOf("Opera/") !== -1 && !isIE();
6500 function iosVersion(userAgent) {
6501 return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1];
6504 function androidVersion(userAgent) {
6505 return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1];
6508 function isIE(version, equation) {
6509 var rv = -1,
6510 re;
6512 if (navigator.appName == 'Microsoft Internet Explorer') {
6513 re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
6514 } else if (navigator.appName == 'Netscape') {
6515 if (navigator.userAgent.indexOf("Trident") > -1) {
6516 re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})");
6517 } else if ((/Edge\/(\d+)./i).test(navigator.userAgent)) {
6518 re = /Edge\/(\d+)./i;
6522 if (re && re.exec(navigator.userAgent) != null) {
6523 rv = parseFloat(RegExp.$1);
6526 if (rv === -1) { return false; }
6527 if (!version) { return true; }
6528 if (!equation) { return version === rv; }
6529 if (equation === "<") { return version < rv; }
6530 if (equation === ">") { return version > rv; }
6531 if (equation === "<=") { return version <= rv; }
6532 if (equation === ">=") { return version >= rv; }
6535 return {
6536 // Static variable needed, publicly accessible, to be able override it in unit tests
6537 USER_AGENT: userAgent,
6539 /**
6540 * Exclude browsers that are not capable of displaying and handling
6541 * contentEditable as desired:
6542 * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable
6543 * - IE < 8 create invalid markup and crash randomly from time to time
6545 * @return {Boolean}
6546 */
6547 supported: function() {
6548 var userAgent = this.USER_AGENT.toLowerCase(),
6549 // Essential for making html elements editable
6550 hasContentEditableSupport = "contentEditable" in testElement,
6551 // Following methods are needed in order to interact with the contentEditable area
6552 hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState,
6553 // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+
6554 hasQuerySelectorSupport = document.querySelector && document.querySelectorAll,
6555 // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05)
6556 isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || (this.isAndroid() && androidVersion(userAgent) < 4) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1;
6557 return hasContentEditableSupport
6558 && hasEditingApiSupport
6559 && hasQuerySelectorSupport
6560 && !isIncompatibleMobileBrowser;
6561 },
6563 isTouchDevice: function() {
6564 return this.supportsEvent("touchmove");
6565 },
6567 isIos: function() {
6568 return (/ipad|iphone|ipod/i).test(this.USER_AGENT);
6569 },
6571 isAndroid: function() {
6572 return this.USER_AGENT.indexOf("Android") !== -1;
6573 },
6575 /**
6576 * Whether the browser supports sandboxed iframes
6577 * Currently only IE 6+ offers such feature <iframe security="restricted">
6579 * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx
6580 * http://blogs.msdn.com/b/ie/archive/2008/01/18/using-frames-more-securely.aspx
6582 * HTML5 sandboxed iframes are still buggy and their DOM is not reachable from the outside (except when using postMessage)
6583 */
6584 supportsSandboxedIframes: function() {
6585 return isIE();
6586 },
6588 /**
6589 * IE6+7 throw a mixed content warning when the src of an iframe
6590 * is empty/unset or about:blank
6591 * window.querySelector is implemented as of IE8
6592 */
6593 throwsMixedContentWarningWhenIframeSrcIsEmpty: function() {
6594 return !("querySelector" in document);
6595 },
6597 /**
6598 * Whether the caret is correctly displayed in contentEditable elements
6599 * Firefox sometimes shows a huge caret in the beginning after focusing
6600 */
6601 displaysCaretInEmptyContentEditableCorrectly: function() {
6602 return isIE(12, ">");
6603 },
6605 /**
6606 * Opera and IE are the only browsers who offer the css value
6607 * in the original unit, thx to the currentStyle object
6608 * All other browsers provide the computed style in px via window.getComputedStyle
6609 */
6610 hasCurrentStyleProperty: function() {
6611 return "currentStyle" in testElement;
6612 },
6614 /**
6615 * Whether the browser inserts a <br> when pressing enter in a contentEditable element
6616 */
6617 insertsLineBreaksOnReturn: function() {
6618 return isGecko;
6619 },
6621 supportsPlaceholderAttributeOn: function(element) {
6622 return "placeholder" in element;
6623 },
6625 supportsEvent: function(eventName) {
6626 return "on" + eventName in testElement || (function() {
6627 testElement.setAttribute("on" + eventName, "return;");
6628 return typeof(testElement["on" + eventName]) === "function";
6629 })();
6630 },
6632 /**
6633 * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe
6634 */
6635 supportsEventsInIframeCorrectly: function() {
6636 return !isOpera;
6637 },
6639 /**
6640 * Everything below IE9 doesn't know how to treat HTML5 tags
6642 * @param {Object} context The document object on which to check HTML5 support
6644 * @example
6645 * wysihtml.browser.supportsHTML5Tags(document);
6646 */
6647 supportsHTML5Tags: function(context) {
6648 var element = context.createElement("div"),
6649 html5 = "<article>foo</article>";
6650 element.innerHTML = html5;
6651 return element.innerHTML.toLowerCase() === html5;
6652 },
6654 /**
6655 * Checks whether a document supports a certain queryCommand
6656 * In particular, Opera needs a reference to a document that has a contentEditable in it's dom tree
6657 * in oder to report correct results
6659 * @param {Object} doc Document object on which to check for a query command
6660 * @param {String} command The query command to check for
6661 * @return {Boolean}
6663 * @example
6664 * wysihtml.browser.supportsCommand(document, "bold");
6665 */
6666 supportsCommand: (function() {
6667 // Following commands are supported but contain bugs in some browsers
6668 // TODO: investigate if some of these bugs can be tested without altering selection on page, instead of targeting browsers and versions directly
6669 var buggyCommands = {
6670 // formatBlock fails with some tags (eg. <blockquote>)
6671 "formatBlock": isIE(10, "<="),
6672 // When inserting unordered or ordered lists in Firefox, Chrome or Safari, the current selection or line gets
6673 // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>)
6674 // IE and Opera act a bit different here as they convert the entire content of the current block element into a list
6675 "insertUnorderedList": isIE(),
6676 "insertOrderedList": isIE()
6677 };
6679 // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands
6680 var supported = {
6681 "insertHTML": isGecko
6682 };
6684 return function(doc, command) {
6685 var isBuggy = buggyCommands[command];
6686 if (!isBuggy) {
6687 // Firefox throws errors when invoking queryCommandSupported or queryCommandEnabled
6688 try {
6689 return doc.queryCommandSupported(command);
6690 } catch(e1) {}
6692 try {
6693 return doc.queryCommandEnabled(command);
6694 } catch(e2) {
6695 return !!supported[command];
6698 return false;
6699 };
6700 })(),
6702 /**
6703 * IE: URLs starting with:
6704 * www., http://, https://, ftp://, gopher://, mailto:, new:, snews:, telnet:, wasis:, file://,
6705 * nntp://, newsrc:, ldap://, ldaps://, outlook:, mic:// and url:
6706 * will automatically be auto-linked when either the user inserts them via copy&paste or presses the
6707 * space bar when the caret is directly after such an url.
6708 * This behavior cannot easily be avoided in IE < 9 since the logic is hardcoded in the mshtml.dll
6709 * (related blog post on msdn
6710 * http://blogs.msdn.com/b/ieinternals/archive/2009/09/17/prevent-automatic-hyperlinking-in-contenteditable-html.aspx).
6711 */
6712 doesAutoLinkingInContentEditable: function() {
6713 return isIE();
6714 },
6716 /**
6717 * As stated above, IE auto links urls typed into contentEditable elements
6718 * Since IE9 it's possible to prevent this behavior
6719 */
6720 canDisableAutoLinking: function() {
6721 return this.supportsCommand(document, "AutoUrlDetect");
6722 },
6724 /**
6725 * IE leaves an empty paragraph in the contentEditable element after clearing it
6726 * Chrome/Safari sometimes an empty <div>
6727 */
6728 clearsContentEditableCorrectly: function() {
6729 return isGecko || isOpera || isWebKit;
6730 },
6732 /**
6733 * IE gives wrong results for getAttribute
6734 */
6735 supportsGetAttributeCorrectly: function() {
6736 var td = document.createElement("td");
6737 return td.getAttribute("rowspan") != "1";
6738 },
6740 /**
6741 * When clicking on images in IE, Opera and Firefox, they are selected, which makes it easy to interact with them.
6742 * Chrome and Safari both don't support this
6743 */
6744 canSelectImagesInContentEditable: function() {
6745 return isGecko || isIE() || isOpera;
6746 },
6748 /**
6749 * All browsers except Safari and Chrome automatically scroll the range/caret position into view
6750 */
6751 autoScrollsToCaret: function() {
6752 return !isWebKit;
6753 },
6755 /**
6756 * Check whether the browser automatically closes tags that don't need to be opened
6757 */
6758 autoClosesUnclosedTags: function() {
6759 var clonedTestElement = testElement.cloneNode(false),
6760 returnValue,
6761 innerHTML;
6763 clonedTestElement.innerHTML = "<p><div></div>";
6764 innerHTML = clonedTestElement.innerHTML.toLowerCase();
6765 returnValue = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>";
6767 // Cache result by overwriting current function
6768 this.autoClosesUnclosedTags = function() { return returnValue; };
6770 return returnValue;
6771 },
6773 /**
6774 * Whether the browser supports the native document.getElementsByClassName which returns live NodeLists
6775 */
6776 supportsNativeGetElementsByClassName: function() {
6777 return String(document.getElementsByClassName).indexOf("[native code]") !== -1;
6778 },
6780 /**
6781 * As of now (19.04.2011) only supported by Firefox 4 and Chrome
6782 * See https://developer.mozilla.org/en/DOM/Selection/modify
6783 */
6784 supportsSelectionModify: function() {
6785 return "getSelection" in window && "modify" in window.getSelection();
6786 },
6788 /**
6789 * Opera needs a white space after a <br> in order to position the caret correctly
6790 */
6791 needsSpaceAfterLineBreak: function() {
6792 return isOpera;
6793 },
6795 /**
6796 * Whether the browser supports the speech api on the given element
6797 * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/
6799 * @example
6800 * var input = document.createElement("input");
6801 * if (wysihtml.browser.supportsSpeechApiOn(input)) {
6802 * // ...
6803 * }
6804 */
6805 supportsSpeechApiOn: function(input) {
6806 var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [undefined, 0];
6807 return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input);
6808 },
6810 /**
6811 * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest
6812 * See https://connect.microsoft.com/ie/feedback/details/650112
6813 * or try the POC http://tifftiff.de/ie9_crash/
6814 */
6815 crashesWhenDefineProperty: function(property) {
6816 return isIE(9) && (property === "XMLHttpRequest" || property === "XDomainRequest");
6817 },
6819 /**
6820 * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element
6821 */
6822 doesAsyncFocus: function() {
6823 return isIE(12, ">");
6824 },
6826 /**
6827 * 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
6828 */
6829 hasProblemsSettingCaretAfterImg: function() {
6830 return isIE();
6831 },
6833 /* In IE when deleting with caret at the begining of LI, List get broken into half instead of merging the LI with previous */
6834 hasLiDeletingProblem: function() {
6835 return isIE();
6836 },
6838 hasUndoInContextMenu: function() {
6839 return isGecko || isChrome || isOpera;
6840 },
6842 /**
6843 * Opera sometimes doesn't insert the node at the right position when range.insertNode(someNode)
6844 * is used (regardless if rangy or native)
6845 * This especially happens when the caret is positioned right after a <br> because then
6846 * insertNode() will insert the node right before the <br>
6847 */
6848 hasInsertNodeIssue: function() {
6849 return isOpera;
6850 },
6852 /**
6853 * 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>)
6854 */
6855 hasIframeFocusIssue: function() {
6856 return isIE();
6857 },
6859 /**
6860 * Chrome + Safari create invalid nested markup after paste
6862 * <p>
6863 * foo
6864 * <p>bar</p> <!-- BOO! -->
6865 * </p>
6866 */
6867 createsNestedInvalidMarkupAfterPaste: function() {
6868 return isWebKit;
6869 },
6871 // In all webkit browsers there are some places where caret can not be placed at the end of blocks and directly before block level element
6872 // when startContainer is element.
6873 hasCaretBlockElementIssue: function() {
6874 return isWebKit;
6875 },
6877 supportsMutationEvents: function() {
6878 return ("MutationEvent" in window);
6879 },
6881 /**
6882 IE (at least up to 11) does not support clipboardData on event.
6883 It is on window but cannot return text/html
6884 Should actually check for clipboardData on paste event, but cannot in firefox
6885 */
6886 supportsModernPaste: function () {
6887 return !isIE();
6888 },
6890 // Unifies the property names of element.style by returning the suitable property name for current browser
6891 // Input property key must be the standard
6892 fixStyleKey: function(key) {
6893 if (key === "cssFloat") {
6894 return ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat";
6896 return key;
6897 },
6899 usesControlRanges: function() {
6900 return document.body && "createControlRange" in document.body;
6901 },
6903 // Webkit browsers have an issue that when caret is at the end of link it is moved outside of link while inserting new characters,
6904 // so all inserted content will be after link. Selection before inserion is reported to be in link though.
6905 // This makes changing link texts from problematic to impossible (if link is just 1 characer long) for the user.
6906 // TODO: needs to be tested better than just browser as it some day might get fixed
6907 hasCaretAtLinkEndInsertionProblems: function() {
6908 return isWebKit;
6910 };
6911 })();
6913 wysihtml.lang.array = function(arr) {
6914 return {
6915 /**
6916 * Check whether a given object exists in an array
6918 * @example
6919 * wysihtml.lang.array([1, 2]).contains(1);
6920 * // => true
6922 * Can be used to match array with array. If intersection is found true is returned
6923 */
6924 contains: function(needle) {
6925 if (Array.isArray(needle)) {
6926 for (var i = needle.length; i--;) {
6927 if (wysihtml.lang.array(arr).indexOf(needle[i]) !== -1) {
6928 return true;
6931 return false;
6932 } else {
6933 return wysihtml.lang.array(arr).indexOf(needle) !== -1;
6935 },
6937 /**
6938 * Check whether a given object exists in an array and return index
6939 * If no elelemt found returns -1
6941 * @example
6942 * wysihtml.lang.array([1, 2]).indexOf(2);
6943 * // => 1
6944 */
6945 indexOf: function(needle) {
6946 if (arr.indexOf) {
6947 return arr.indexOf(needle);
6948 } else {
6949 for (var i=0, length=arr.length; i<length; i++) {
6950 if (arr[i] === needle) { return i; }
6952 return -1;
6954 },
6956 /**
6957 * Substract one array from another
6959 * @example
6960 * wysihtml.lang.array([1, 2, 3, 4]).without([3, 4]);
6961 * // => [1, 2]
6962 */
6963 without: function(arrayToSubstract) {
6964 arrayToSubstract = wysihtml.lang.array(arrayToSubstract);
6965 var newArr = [],
6966 i = 0,
6967 length = arr.length;
6968 for (; i<length; i++) {
6969 if (!arrayToSubstract.contains(arr[i])) {
6970 newArr.push(arr[i]);
6973 return newArr;
6974 },
6976 /**
6977 * Return a clean native array
6979 * Following will convert a Live NodeList to a proper Array
6980 * @example
6981 * var childNodes = wysihtml.lang.array(document.body.childNodes).get();
6982 */
6983 get: function() {
6984 var i = 0,
6985 length = arr.length,
6986 newArray = [];
6987 for (; i<length; i++) {
6988 newArray.push(arr[i]);
6990 return newArray;
6991 },
6993 /**
6994 * Creates a new array with the results of calling a provided function on every element in this array.
6995 * optionally this can be provided as second argument
6997 * @example
6998 * var childNodes = wysihtml.lang.array([1,2,3,4]).map(function (value, index, array) {
6999 return value * 2;
7000 * });
7001 * // => [2,4,6,8]
7002 */
7003 map: function(callback, thisArg) {
7004 if (Array.prototype.map) {
7005 return arr.map(callback, thisArg);
7006 } else {
7007 var len = arr.length >>> 0,
7008 A = new Array(len),
7009 i = 0;
7010 for (; i < len; i++) {
7011 A[i] = callback.call(thisArg, arr[i], i, arr);
7013 return A;
7015 },
7017 /* ReturnS new array without duplicate entries
7019 * @example
7020 * var uniq = wysihtml.lang.array([1,2,3,2,1,4]).unique();
7021 * // => [1,2,3,4]
7022 */
7023 unique: function() {
7024 var vals = [],
7025 max = arr.length,
7026 idx = 0;
7028 while (idx < max) {
7029 if (!wysihtml.lang.array(vals).contains(arr[idx])) {
7030 vals.push(arr[idx]);
7032 idx++;
7034 return vals;
7037 };
7038 };
7040 wysihtml.lang.Dispatcher = Base.extend(
7041 /** @scope wysihtml.lang.Dialog.prototype */ {
7042 on: function(eventName, handler) {
7043 this.events = this.events || {};
7044 this.events[eventName] = this.events[eventName] || [];
7045 this.events[eventName].push(handler);
7046 return this;
7047 },
7049 off: function(eventName, handler) {
7050 this.events = this.events || {};
7051 var i = 0,
7052 handlers,
7053 newHandlers;
7054 if (eventName) {
7055 handlers = this.events[eventName] || [],
7056 newHandlers = [];
7057 for (; i<handlers.length; i++) {
7058 if (handlers[i] !== handler && handler) {
7059 newHandlers.push(handlers[i]);
7062 this.events[eventName] = newHandlers;
7063 } else {
7064 // Clean up all events
7065 this.events = {};
7067 return this;
7068 },
7070 fire: function(eventName, payload) {
7071 this.events = this.events || {};
7072 var handlers = this.events[eventName] || [],
7073 i = 0;
7074 for (; i<handlers.length; i++) {
7075 handlers[i].call(this, payload);
7077 return this;
7078 },
7080 // deprecated, use .on()
7081 observe: function() {
7082 return this.on.apply(this, arguments);
7083 },
7085 // deprecated, use .off()
7086 stopObserving: function() {
7087 return this.off.apply(this, arguments);
7089 });
7091 wysihtml.lang.object = function(obj) {
7092 return {
7093 /**
7094 * @example
7095 * wysihtml.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get();
7096 * // => { foo: 1, bar: 2, baz: 3 }
7097 */
7098 merge: function(otherObj, deep) {
7099 for (var i in otherObj) {
7100 if (deep && wysihtml.lang.object(otherObj[i]).isPlainObject() && (typeof obj[i] === "undefined" || wysihtml.lang.object(obj[i]).isPlainObject())) {
7101 if (typeof obj[i] === "undefined") {
7102 obj[i] = wysihtml.lang.object(otherObj[i]).clone(true);
7103 } else {
7104 wysihtml.lang.object(obj[i]).merge(wysihtml.lang.object(otherObj[i]).clone(true));
7106 } else {
7107 obj[i] = wysihtml.lang.object(otherObj[i]).isPlainObject() ? wysihtml.lang.object(otherObj[i]).clone(true) : otherObj[i];
7110 return this;
7111 },
7113 difference: function (otherObj) {
7114 var diffObj = {};
7116 // Get old values not in comparing object
7117 for (var i in obj) {
7118 if (obj.hasOwnProperty(i)) {
7119 if (!otherObj.hasOwnProperty(i)) {
7120 diffObj[i] = obj[i];
7125 // Get new and different values in comparing object
7126 for (var o in otherObj) {
7127 if (otherObj.hasOwnProperty(o)) {
7128 if (!obj.hasOwnProperty(o) || obj[o] !== otherObj[o]) {
7129 diffObj[0] = obj[0];
7133 return diffObj;
7134 },
7136 get: function() {
7137 return obj;
7138 },
7140 /**
7141 * @example
7142 * wysihtml.lang.object({ foo: 1 }).clone();
7143 * // => { foo: 1 }
7145 * v0.4.14 adds options for deep clone : wysihtml.lang.object({ foo: 1 }).clone(true);
7146 */
7147 clone: function(deep) {
7148 var newObj = {},
7149 i;
7151 if (obj === null || !wysihtml.lang.object(obj).isPlainObject()) {
7152 return obj;
7155 for (i in obj) {
7156 if(obj.hasOwnProperty(i)) {
7157 if (deep) {
7158 newObj[i] = wysihtml.lang.object(obj[i]).clone(deep);
7159 } else {
7160 newObj[i] = obj[i];
7164 return newObj;
7165 },
7167 /**
7168 * @example
7169 * wysihtml.lang.object([]).isArray();
7170 * // => true
7171 */
7172 isArray: function() {
7173 return Object.prototype.toString.call(obj) === "[object Array]";
7174 },
7176 /**
7177 * @example
7178 * wysihtml.lang.object(function() {}).isFunction();
7179 * // => true
7180 */
7181 isFunction: function() {
7182 return Object.prototype.toString.call(obj) === '[object Function]';
7183 },
7185 isPlainObject: function () {
7186 return obj && Object.prototype.toString.call(obj) === '[object Object]' && !(("Node" in window) ? obj instanceof Node : obj instanceof Element || obj instanceof Text);
7187 },
7189 /**
7190 * @example
7191 * wysihtml.lang.object({}).isEmpty();
7192 * // => true
7193 */
7194 isEmpty: function() {
7195 for (var i in obj) {
7196 if (obj.hasOwnProperty(i)) {
7197 return false;
7200 return true;
7202 };
7203 };
7205 (function() {
7206 var WHITE_SPACE_START = /^\s+/,
7207 WHITE_SPACE_END = /\s+$/,
7208 ENTITY_REG_EXP = /[&<>\t"]/g,
7209 ENTITY_MAP = {
7210 '&': '&amp;',
7211 '<': '&lt;',
7212 '>': '&gt;',
7213 '"': "&quot;",
7214 '\t':"&nbsp; "
7215 };
7216 wysihtml.lang.string = function(str) {
7217 str = String(str);
7218 return {
7219 /**
7220 * @example
7221 * wysihtml.lang.string(" foo ").trim();
7222 * // => "foo"
7223 */
7224 trim: function() {
7225 return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, "");
7226 },
7228 /**
7229 * @example
7230 * wysihtml.lang.string("Hello #{name}").interpolate({ name: "Christopher" });
7231 * // => "Hello Christopher"
7232 */
7233 interpolate: function(vars) {
7234 for (var i in vars) {
7235 str = this.replace("#{" + i + "}").by(vars[i]);
7237 return str;
7238 },
7240 /**
7241 * @example
7242 * wysihtml.lang.string("Hello Tom").replace("Tom").with("Hans");
7243 * // => "Hello Hans"
7244 */
7245 replace: function(search) {
7246 return {
7247 by: function(replace) {
7248 return str.split(search).join(replace);
7250 };
7251 },
7253 /**
7254 * @example
7255 * wysihtml.lang.string("hello<br>").escapeHTML();
7256 * // => "hello&lt;br&gt;"
7257 */
7258 escapeHTML: function(linebreaks, convertSpaces) {
7259 var html = str.replace(ENTITY_REG_EXP, function(c) { return ENTITY_MAP[c]; });
7260 if (linebreaks) {
7261 html = html.replace(/(?:\r\n|\r|\n)/g, '<br />');
7263 if (convertSpaces) {
7264 html = html.replace(/ /gi, "&nbsp; ");
7266 return html;
7268 };
7269 };
7270 })();
7272 /**
7273 * Find urls in descendant text nodes of an element and auto-links them
7274 * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/
7276 * @param {Element} element Container element in which to search for urls
7278 * @example
7279 * <div id="text-container">Please click here: www.google.com</div>
7280 * <script>wysihtml.dom.autoLink(document.getElementById("text-container"));</script>
7281 */
7282 (function(wysihtml) {
7283 var /**
7284 * Don't auto-link urls that are contained in the following elements:
7285 */
7286 IGNORE_URLS_IN = wysihtml.lang.array(["CODE", "PRE", "A", "SCRIPT", "HEAD", "TITLE", "STYLE"]),
7287 /**
7288 * revision 1:
7289 * /(\S+\.{1}[^\s\,\.\!]+)/g
7291 * revision 2:
7292 * /(\b(((https?|ftp):\/\/)|(www\.))[-A-Z0-9+&@#\/%?=~_|!:,.;\[\]]*[-A-Z0-9+&@#\/%=~_|])/gim
7294 * put this in the beginning if you don't wan't to match within a word
7295 * (^|[\>\(\{\[\s\>])
7296 */
7297 URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi,
7298 TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i,
7299 MAX_DISPLAY_LENGTH = 100,
7300 BRACKETS = { ")": "(", "]": "[", "}": "{" };
7302 function autoLink(element, ignoreInClasses) {
7303 if (_hasParentThatShouldBeIgnored(element, ignoreInClasses)) {
7304 return element;
7307 if (element === element.ownerDocument.documentElement) {
7308 element = element.ownerDocument.body;
7311 return _parseNode(element, ignoreInClasses);
7314 /**
7315 * This is basically a rebuild of
7316 * the rails auto_link_urls text helper
7317 */
7318 function _convertUrlsToLinks(str) {
7319 return str.replace(URL_REG_EXP, function(match, url) {
7320 var punctuation = (url.match(TRAILING_CHAR_REG_EXP) || [])[1] || "",
7321 opening = BRACKETS[punctuation];
7322 url = url.replace(TRAILING_CHAR_REG_EXP, "");
7324 if (url.split(opening).length > url.split(punctuation).length) {
7325 url = url + punctuation;
7326 punctuation = "";
7328 var realUrl = url,
7329 displayUrl = url;
7330 if (url.length > MAX_DISPLAY_LENGTH) {
7331 displayUrl = displayUrl.substr(0, MAX_DISPLAY_LENGTH) + "...";
7333 // Add http prefix if necessary
7334 if (realUrl.substr(0, 4) === "www.") {
7335 realUrl = "http://" + realUrl;
7338 return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation;
7339 });
7342 /**
7343 * Creates or (if already cached) returns a temp element
7344 * for the given document object
7345 */
7346 function _getTempElement(context) {
7347 var tempElement = context._wysihtml_tempElement;
7348 if (!tempElement) {
7349 tempElement = context._wysihtml_tempElement = context.createElement("div");
7351 return tempElement;
7354 /**
7355 * Replaces the original text nodes with the newly auto-linked dom tree
7356 */
7357 function _wrapMatchesInNode(textNode) {
7358 var parentNode = textNode.parentNode,
7359 nodeValue = wysihtml.lang.string(textNode.data).escapeHTML(),
7360 tempElement = _getTempElement(parentNode.ownerDocument);
7362 // We need to insert an empty/temporary <span /> to fix IE quirks
7363 // Elsewise IE would strip white space in the beginning
7364 tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(nodeValue);
7365 tempElement.removeChild(tempElement.firstChild);
7367 while (tempElement.firstChild) {
7368 // inserts tempElement.firstChild before textNode
7369 parentNode.insertBefore(tempElement.firstChild, textNode);
7371 parentNode.removeChild(textNode);
7374 function _hasParentThatShouldBeIgnored(node, ignoreInClasses) {
7375 var nodeName;
7376 while (node.parentNode) {
7377 node = node.parentNode;
7378 nodeName = node.nodeName;
7379 if (node.className && wysihtml.lang.array(node.className.split(' ')).contains(ignoreInClasses)) {
7380 return true;
7382 if (IGNORE_URLS_IN.contains(nodeName)) {
7383 return true;
7384 } else if (nodeName === "body") {
7385 return false;
7388 return false;
7391 function _parseNode(element, ignoreInClasses) {
7392 if (IGNORE_URLS_IN.contains(element.nodeName)) {
7393 return;
7396 if (element.className && wysihtml.lang.array(element.className.split(' ')).contains(ignoreInClasses)) {
7397 return;
7400 if (element.nodeType === wysihtml.TEXT_NODE && element.data.match(URL_REG_EXP)) {
7401 _wrapMatchesInNode(element);
7402 return;
7405 var childNodes = wysihtml.lang.array(element.childNodes).get(),
7406 childNodesLength = childNodes.length,
7407 i = 0;
7409 for (; i<childNodesLength; i++) {
7410 _parseNode(childNodes[i], ignoreInClasses);
7413 return element;
7416 wysihtml.dom.autoLink = autoLink;
7418 // Reveal url reg exp to the outside
7419 wysihtml.dom.autoLink.URL_REG_EXP = URL_REG_EXP;
7420 })(wysihtml);
7422 (function(wysihtml) {
7423 var api = wysihtml.dom;
7425 api.addClass = function(element, className) {
7426 var classList = element.classList;
7427 if (classList) {
7428 return classList.add(className);
7430 if (api.hasClass(element, className)) {
7431 return;
7433 element.className += " " + className;
7434 };
7436 api.removeClass = function(element, className) {
7437 var classList = element.classList;
7438 if (classList) {
7439 return classList.remove(className);
7442 element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " ");
7443 };
7445 api.hasClass = function(element, className) {
7446 var classList = element.classList;
7447 if (classList) {
7448 return classList.contains(className);
7451 var elementClassName = element.className;
7452 return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
7453 };
7454 })(wysihtml);
7456 wysihtml.dom.compareDocumentPosition = (function() {
7457 var documentElement = document.documentElement;
7458 if (documentElement.compareDocumentPosition) {
7459 return function(container, element) {
7460 return container.compareDocumentPosition(element);
7461 };
7462 } else {
7463 return function( container, element ) {
7464 // implementation borrowed from https://github.com/tmpvar/jsdom/blob/681a8524b663281a0f58348c6129c8c184efc62c/lib/jsdom/level3/core.js // MIT license
7465 var thisOwner, otherOwner;
7467 if( container.nodeType === 9) // Node.DOCUMENT_NODE
7468 thisOwner = container;
7469 else
7470 thisOwner = container.ownerDocument;
7472 if( element.nodeType === 9) // Node.DOCUMENT_NODE
7473 otherOwner = element;
7474 else
7475 otherOwner = element.ownerDocument;
7477 if( container === element ) return 0;
7478 if( container === element.ownerDocument ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
7479 if( container.ownerDocument === element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
7480 if( thisOwner !== otherOwner ) return 1; // Node.DOCUMENT_POSITION_DISCONNECTED;
7482 // Text nodes for attributes does not have a _parentNode. So we need to find them as attribute child.
7483 if( container.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && container.childNodes && wysihtml.lang.array(container.childNodes).indexOf( element ) !== -1)
7484 return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
7486 if( element.nodeType === 2 /*Node.ATTRIBUTE_NODE*/ && element.childNodes && wysihtml.lang.array(element.childNodes).indexOf( container ) !== -1)
7487 return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
7489 var point = container;
7490 var parents = [ ];
7491 var previous = null;
7492 while( point ) {
7493 if( point == element ) return 2 + 8; //Node.DOCUMENT_POSITION_PRECEDING + Node.DOCUMENT_POSITION_CONTAINS;
7494 parents.push( point );
7495 point = point.parentNode;
7497 point = element;
7498 previous = null;
7499 while( point ) {
7500 if( point == container ) return 4 + 16; //Node.DOCUMENT_POSITION_FOLLOWING + Node.DOCUMENT_POSITION_CONTAINED_BY;
7501 var location_index = wysihtml.lang.array(parents).indexOf( point );
7502 if( location_index !== -1) {
7503 var smallest_common_ancestor = parents[ location_index ];
7504 var this_index = wysihtml.lang.array(smallest_common_ancestor.childNodes).indexOf( parents[location_index - 1]);//smallest_common_ancestor.childNodes.toArray().indexOf( parents[location_index - 1] );
7505 var other_index = wysihtml.lang.array(smallest_common_ancestor.childNodes).indexOf( previous ); //smallest_common_ancestor.childNodes.toArray().indexOf( previous );
7506 if( this_index > other_index ) {
7507 return 2; //Node.DOCUMENT_POSITION_PRECEDING;
7509 else {
7510 return 4; //Node.DOCUMENT_POSITION_FOLLOWING;
7513 previous = point;
7514 point = point.parentNode;
7516 return 1; //Node.DOCUMENT_POSITION_DISCONNECTED;
7517 };
7519 })();
7521 wysihtml.dom.contains = (function() {
7522 var documentElement = document.documentElement;
7523 if (documentElement.contains) {
7524 return function(container, element) {
7525 if (element.nodeType !== wysihtml.ELEMENT_NODE) {
7526 if (element.parentNode === container) {
7527 return true;
7529 element = element.parentNode;
7531 return container !== element && container.contains(element);
7532 };
7533 } else if (documentElement.compareDocumentPosition) {
7534 return function(container, element) {
7535 // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition
7536 return !!(container.compareDocumentPosition(element) & 16);
7537 };
7539 })();
7541 (function(wysihtml) {
7542 var doc = document;
7543 wysihtml.dom.ContentEditableArea = Base.extend({
7544 getContentEditable: function() {
7545 return this.element;
7546 },
7548 getWindow: function() {
7549 return this.element.ownerDocument.defaultView || this.element.ownerDocument.parentWindow;
7550 },
7552 getDocument: function() {
7553 return this.element.ownerDocument;
7554 },
7556 constructor: function(readyCallback, config, contentEditable) {
7557 this.callback = readyCallback || wysihtml.EMPTY_FUNCTION;
7558 this.config = wysihtml.lang.object({}).merge(config).get();
7559 if (!this.config.className) {
7560 this.config.className = "wysihtml-sandbox";
7562 if (contentEditable) {
7563 this.element = this._bindElement(contentEditable);
7564 } else {
7565 this.element = this._createElement();
7567 },
7569 destroy: function() {
7571 },
7573 // creates a new contenteditable and initiates it
7574 _createElement: function() {
7575 var element = doc.createElement("div");
7576 element.className = this.config.className;
7577 this._loadElement(element);
7578 return element;
7579 },
7581 // initiates an allready existent contenteditable
7582 _bindElement: function(contentEditable) {
7583 contentEditable.className = contentEditable.className ? contentEditable.className + " wysihtml-sandbox" : "wysihtml-sandbox";
7584 this._loadElement(contentEditable, true);
7585 return contentEditable;
7586 },
7588 _loadElement: function(element, contentExists) {
7589 var that = this;
7591 if (!contentExists) {
7592 var innerHtml = this._getHtml();
7593 element.innerHTML = innerHtml;
7596 this.loaded = true;
7597 // Trigger the callback
7598 setTimeout(function() { that.callback(that); }, 0);
7599 },
7601 _getHtml: function(templateVars) {
7602 return '';
7605 });
7606 })(wysihtml);
7608 /**
7609 * Converts an HTML fragment/element into a unordered/ordered list
7611 * @param {Element} element The element which should be turned into a list
7612 * @param {String} listType The list type in which to convert the tree (either "ul" or "ol")
7613 * @return {Element} The created list
7615 * @example
7616 * <!-- Assume the following dom: -->
7617 * <span id="pseudo-list">
7618 * eminem<br>
7619 * dr. dre
7620 * <div>50 Cent</div>
7621 * </span>
7623 * <script>
7624 * wysihtml.dom.convertToList(document.getElementById("pseudo-list"), "ul");
7625 * </script>
7627 * <!-- Will result in: -->
7628 * <ul>
7629 * <li>eminem</li>
7630 * <li>dr. dre</li>
7631 * <li>50 Cent</li>
7632 * </ul>
7633 */
7634 wysihtml.dom.convertToList = (function() {
7635 function _createListItem(doc, list) {
7636 var listItem = doc.createElement("li");
7637 list.appendChild(listItem);
7638 return listItem;
7641 function _createList(doc, type) {
7642 return doc.createElement(type);
7645 function convertToList(element, listType, uneditableClass) {
7646 if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") {
7647 // Already a list
7648 return element;
7651 var doc = element.ownerDocument,
7652 list = _createList(doc, listType),
7653 lineBreaks = element.querySelectorAll("br"),
7654 lineBreaksLength = lineBreaks.length,
7655 childNodes,
7656 childNodesLength,
7657 childNode,
7658 lineBreak,
7659 parentNode,
7660 isBlockElement,
7661 isLineBreak,
7662 currentListItem,
7663 i;
7665 // First find <br> at the end of inline elements and move them behind them
7666 for (i=0; i<lineBreaksLength; i++) {
7667 lineBreak = lineBreaks[i];
7668 while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) {
7669 if (wysihtml.dom.getStyle("display").from(parentNode) === "block") {
7670 parentNode.removeChild(lineBreak);
7671 break;
7673 wysihtml.dom.insert(lineBreak).after(lineBreak.parentNode);
7677 childNodes = wysihtml.lang.array(element.childNodes).get();
7678 childNodesLength = childNodes.length;
7680 for (i=0; i<childNodesLength; i++) {
7681 currentListItem = currentListItem || _createListItem(doc, list);
7682 childNode = childNodes[i];
7683 isBlockElement = wysihtml.dom.getStyle("display").from(childNode) === "block";
7684 isLineBreak = childNode.nodeName === "BR";
7686 // consider uneditable as an inline element
7687 if (isBlockElement && (!uneditableClass || !wysihtml.dom.hasClass(childNode, uneditableClass))) {
7688 // Append blockElement to current <li> if empty, otherwise create a new one
7689 currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem;
7690 currentListItem.appendChild(childNode);
7691 currentListItem = null;
7692 continue;
7695 if (isLineBreak) {
7696 // Only create a new list item in the next iteration when the current one has already content
7697 currentListItem = currentListItem.firstChild ? null : currentListItem;
7698 continue;
7701 currentListItem.appendChild(childNode);
7704 if (childNodes.length === 0) {
7705 _createListItem(doc, list);
7708 element.parentNode.replaceChild(list, element);
7709 return list;
7712 return convertToList;
7713 })();
7715 /**
7716 * Copy a set of attributes from one element to another
7718 * @param {Array} attributesToCopy List of attributes which should be copied
7719 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
7720 * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked
7721 * with the element where to copy the attributes to (see example)
7723 * @example
7724 * var textarea = document.querySelector("textarea"),
7725 * div = document.querySelector("div[contenteditable=true]"),
7726 * anotherDiv = document.querySelector("div.preview");
7727 * wysihtml.dom.copyAttributes(["spellcheck", "value", "placeholder"]).from(textarea).to(div).andTo(anotherDiv);
7729 */
7730 wysihtml.dom.copyAttributes = function(attributesToCopy) {
7731 return {
7732 from: function(elementToCopyFrom) {
7733 return {
7734 to: function pasteElementAttributesTo(elementToCopyTo) {
7735 var attribute,
7736 i = 0,
7737 length = attributesToCopy.length;
7738 for (; i<length; i++) {
7739 attribute = attributesToCopy[i];
7740 if (typeof(elementToCopyFrom[attribute]) !== "undefined" && elementToCopyFrom[attribute] !== "") {
7741 elementToCopyTo[attribute] = elementToCopyFrom[attribute];
7744 return { andTo: pasteElementAttributesTo };
7746 };
7748 };
7749 };
7751 /**
7752 * Copy a set of styles from one element to another
7753 * Please note that this only works properly across browsers when the element from which to copy the styles
7754 * is in the dom
7756 * Interesting article on how to copy styles
7758 * @param {Array} stylesToCopy List of styles which should be copied
7759 * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to
7760 * copy the styles from., this again returns an object which provides a method named "to" which can be invoked
7761 * with the element where to copy the styles to (see example)
7763 * @example
7764 * var textarea = document.querySelector("textarea"),
7765 * div = document.querySelector("div[contenteditable=true]"),
7766 * anotherDiv = document.querySelector("div.preview");
7767 * wysihtml.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv);
7769 */
7770 (function(dom) {
7772 /**
7773 * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set
7774 * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then
7775 * its computed css width will be 198px
7777 * See https://bugzilla.mozilla.org/show_bug.cgi?id=520992
7778 */
7779 var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"];
7781 var shouldIgnoreBoxSizingBorderBox = function(element) {
7782 if (hasBoxSizingBorderBox(element)) {
7783 return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth;
7785 return false;
7786 };
7788 var hasBoxSizingBorderBox = function(element) {
7789 var i = 0,
7790 length = BOX_SIZING_PROPERTIES.length;
7791 for (; i<length; i++) {
7792 if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") {
7793 return BOX_SIZING_PROPERTIES[i];
7796 };
7798 dom.copyStyles = function(stylesToCopy) {
7799 return {
7800 from: function(element) {
7801 if (shouldIgnoreBoxSizingBorderBox(element)) {
7802 stylesToCopy = wysihtml.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES);
7805 var cssText = "",
7806 length = stylesToCopy.length,
7807 i = 0,
7808 property;
7809 for (; i<length; i++) {
7810 property = stylesToCopy[i];
7811 cssText += property + ":" + dom.getStyle(property).from(element) + ";";
7814 return {
7815 to: function pasteStylesTo(element) {
7816 dom.setStyles(cssText).on(element);
7817 return { andTo: pasteStylesTo };
7819 };
7821 };
7822 };
7823 })(wysihtml.dom);
7825 /**
7826 * Event Delegation
7828 * @example
7829 * wysihtml.dom.delegate(document.body, "a", "click", function() {
7830 * // foo
7831 * });
7832 */
7833 (function(wysihtml) {
7834 wysihtml.dom.delegate = function(container, selector, eventName, handler) {
7835 var callback = function(event) {
7836 var target = event.target,
7837 element = (target.nodeType === 3) ? target.parentNode : target, // IE has .contains only seeing elements not textnodes
7838 matches = container.querySelectorAll(selector);
7840 for (var i = 0, max = matches.length; i < max; i++) {
7841 if (matches[i].contains(element)) {
7842 handler.call(matches[i], event);
7845 };
7847 container.addEventListener(eventName, callback, false);
7848 return {
7849 stop: function() {
7850 container.removeEventListener(eventName, callback, false);
7852 };
7853 };
7854 })(wysihtml);
7856 // TODO: Refactor dom tree traversing here
7857 (function(wysihtml) {
7859 // Finds parents of a node, returning the outermost node first in Array
7860 // if contain node is given parents search is stopped at the container
7861 function parents(node, container) {
7862 var nodes = [node], n = node;
7864 // iterate parents while parent exists and it is not container element
7865 while((container && n && n !== container) || (!container && n)) {
7866 nodes.unshift(n);
7867 n = n.parentNode;
7869 return nodes;
7872 wysihtml.dom.domNode = function(node) {
7873 var defaultNodeTypes = [wysihtml.ELEMENT_NODE, wysihtml.TEXT_NODE];
7875 return {
7877 is: {
7878 emptyTextNode: function(ignoreWhitespace) {
7879 var regx = ignoreWhitespace ? (/^\s*$/g) : (/^[\r\n]*$/g);
7880 return node && node.nodeType === wysihtml.TEXT_NODE && (regx).test(node.data);
7881 },
7883 // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
7884 rangyBookmark: function() {
7885 return node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary');
7886 },
7888 visible: function() {
7889 var isVisible = !(/^\s*$/g).test(wysihtml.dom.getTextContent(node));
7891 if (!isVisible) {
7892 if (node.nodeType === 1 && node.querySelector('img, br, hr, object, embed, canvas, input, textarea')) {
7893 isVisible = true;
7896 return isVisible;
7897 },
7898 lineBreak: function() {
7899 return node && node.nodeType === 1 && node.nodeName === "BR";
7900 },
7901 block: function() {
7902 return node && node.nodeType === 1 && node.ownerDocument.defaultView.getComputedStyle(node).display === "block";
7903 },
7904 // Void elements are elemens that can not have content
7905 // In most cases browsers should solve the cases for you when you try to insert content into those,
7906 // but IE does not and it is not nice to do so anyway.
7907 voidElement: function() {
7908 return wysihtml.dom.domNode(node).test({
7909 query: wysihtml.VOID_ELEMENTS
7910 });
7912 },
7914 // var node = wysihtml.dom.domNode(element).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
7915 prev: function(options) {
7916 var prevNode = node.previousSibling,
7917 types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes;
7919 if (!prevNode) {
7920 return null;
7923 if (
7924 wysihtml.dom.domNode(prevNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass)
7925 (!wysihtml.lang.array(types).contains(prevNode.nodeType)) || // nodeTypes check.
7926 (options && options.ignoreBlankTexts && wysihtml.dom.domNode(prevNode).is.emptyTextNode(true)) // Blank text nodes bypassed if set
7927 ) {
7928 return wysihtml.dom.domNode(prevNode).prev(options);
7931 return prevNode;
7932 },
7934 // var node = wysihtml.dom.domNode(element).next({nodeTypes: [1,3], ignoreBlankTexts: true});
7935 next: function(options) {
7936 var nextNode = node.nextSibling,
7937 types = (options && options.nodeTypes) ? options.nodeTypes : defaultNodeTypes;
7939 if (!nextNode) {
7940 return null;
7943 if (
7944 wysihtml.dom.domNode(nextNode).is.rangyBookmark() || // is Rangy temporary boomark element (bypass)
7945 (!wysihtml.lang.array(types).contains(nextNode.nodeType)) || // nodeTypes check.
7946 (options && options.ignoreBlankTexts && wysihtml.dom.domNode(nextNode).is.emptyTextNode(true)) // blank text nodes bypassed if set
7947 ) {
7948 return wysihtml.dom.domNode(nextNode).next(options);
7951 return nextNode;
7952 },
7954 // Finds the common acnestor container of two nodes
7955 // If container given stops search at the container
7956 // If no common ancestor found returns null
7957 // var node = wysihtml.dom.domNode(element).commonAncestor(node2, container);
7958 commonAncestor: function(node2, container) {
7959 var parents1 = parents(node, container),
7960 parents2 = parents(node2, container);
7962 // Ensure we have found a common ancestor, which will be the first one if anything
7963 if (parents1[0] != parents2[0]) {
7964 return null;
7967 // Traverse up the hierarchy of parents until we reach where they're no longer
7968 // the same. Then return previous which was the common ancestor.
7969 for (var i = 0; i < parents1.length; i++) {
7970 if (parents1[i] != parents2[i]) {
7971 return parents1[i - 1];
7975 return null;
7976 },
7978 // Traverses a node for last children and their chidren (including itself), and finds the last node that has no children.
7979 // Array of classes for forced last-leaves (ex: uneditable-container) can be defined (options = {leafClasses: [...]})
7980 // Useful for finding the actually visible element before cursor
7981 lastLeafNode: function(options) {
7982 var lastChild;
7984 // Returns non-element nodes
7985 if (node.nodeType !== 1) {
7986 return node;
7989 // Returns if element is leaf
7990 lastChild = node.lastChild;
7991 if (!lastChild) {
7992 return node;
7995 // Returns if element is of of options.leafClasses leaf
7996 if (options && options.leafClasses) {
7997 for (var i = options.leafClasses.length; i--;) {
7998 if (wysihtml.dom.hasClass(node, options.leafClasses[i])) {
7999 return node;
8004 return wysihtml.dom.domNode(lastChild).lastLeafNode(options);
8005 },
8007 // Splits element at childnode and extracts the childNode out of the element context
8008 // Example:
8009 // var node = wysihtml.dom.domNode(node).escapeParent(parentNode);
8010 escapeParent: function(element, newWrapper) {
8011 var parent, split2, nodeWrap,
8012 curNode = node;
8014 // Stop if node is not a descendant of element
8015 if (!wysihtml.dom.contains(element, node)) {
8016 throw new Error("Child is not a descendant of node.");
8019 // Climb up the node tree untill node is reached
8020 do {
8021 // Get current parent of node
8022 parent = curNode.parentNode;
8024 // Move after nodes to new clone wrapper
8025 split2 = parent.cloneNode(false);
8026 while (parent.lastChild && parent.lastChild !== curNode) {
8027 split2.insertBefore(parent.lastChild, split2.firstChild);
8030 // Move node up a level. If parent is not yet the container to escape, clone the parent around node, so inner nodes are escaped out too
8031 if (parent !== element) {
8032 nodeWrap = parent.cloneNode(false);
8033 nodeWrap.appendChild(curNode);
8034 curNode = nodeWrap;
8036 parent.parentNode.insertBefore(curNode, parent.nextSibling);
8038 // Add after nodes (unless empty)
8039 if (split2.innerHTML !== '') {
8040 // if contents are empty insert without wrap
8041 if ((/^\s+$/).test(split2.innerHTML)) {
8042 while (split2.lastChild) {
8043 parent.parentNode.insertBefore(split2.lastChild, curNode.nextSibling);
8045 } else {
8046 parent.parentNode.insertBefore(split2, curNode.nextSibling);
8050 // If the node left behind before the split (parent) is now empty then remove
8051 if (parent.innerHTML === '') {
8052 parent.parentNode.removeChild(parent);
8053 } else if ((/^\s+$/).test(parent.innerHTML)) {
8054 while (parent.firstChild) {
8055 parent.parentNode.insertBefore(parent.firstChild, parent);
8057 parent.parentNode.removeChild(parent);
8060 } while (parent && parent !== element);
8062 if (newWrapper && curNode) {
8063 curNode.parentNode.insertBefore(newWrapper, curNode);
8064 newWrapper.appendChild(curNode);
8066 },
8068 transferContentTo: function(targetNode, removeOldWrapper) {
8069 if (node.nodeType === 1) {
8070 if (wysihtml.dom.domNode(targetNode).is.voidElement() || targetNode.nodeType === 3) {
8071 while (node.lastChild) {
8072 targetNode.parentNode.insertBefore(node.lastChild, targetNode.nextSibling);
8074 } else {
8075 while (node.firstChild) {
8076 targetNode.appendChild(node.firstChild);
8079 if (removeOldWrapper) {
8080 node.parentNode.removeChild(node);
8082 } else if (node.nodeType === 3 || node.nodeType === 8){
8083 if (wysihtml.dom.domNode(targetNode).is.voidElement()) {
8084 targetNode.parentNode.insertBefore(node, targetNode.nextSibling);
8085 } else {
8086 targetNode.appendChild(node);
8089 },
8091 /*
8092 Tests a node against properties, and returns true if matches.
8093 Tests on principle that all properties defined must have at least one match.
8094 styleValue parameter works in context of styleProperty and has no effect otherwise.
8095 Returns true if element matches and false if it does not.
8097 Properties for filtering element:
8099 query: selector string,
8100 nodeName: string (uppercase),
8101 className: string,
8102 classRegExp: regex,
8103 styleProperty: string or [],
8104 styleValue: string, [] or regex
8107 Example:
8108 var node = wysihtml.dom.domNode(element).test({})
8109 */
8110 test: function(properties) {
8111 var prop;
8113 // return false if properties object is not defined
8114 if (!properties) {
8115 return false;
8118 // Only element nodes can be tested for these properties
8119 if (node.nodeType !== 1) {
8120 return false;
8123 if (properties.query) {
8124 if (!node.matches(properties.query)) {
8125 return false;
8129 if (properties.nodeName && node.nodeName.toLowerCase() !== properties.nodeName.toLowerCase()) {
8130 return false;
8133 if (properties.className && !node.classList.contains(properties.className)) {
8134 return false;
8137 // classRegExp check (useful for classname begins with logic)
8138 if (properties.classRegExp) {
8139 var matches = (node.className || "").match(properties.classRegExp) || [];
8140 if (matches.length === 0) {
8141 return false;
8145 // styleProperty check
8146 if (properties.styleProperty && properties.styleProperty.length > 0) {
8147 var hasOneStyle = false,
8148 styles = (Array.isArray(properties.styleProperty)) ? properties.styleProperty : [properties.styleProperty];
8149 for (var j = 0, maxStyleP = styles.length; j < maxStyleP; j++) {
8150 // Some old IE-s have different property name for cssFloat
8151 prop = wysihtml.browser.fixStyleKey(styles[j]);
8152 if (node.style[prop]) {
8153 if (properties.styleValue) {
8154 // Style value as additional parameter
8155 if (properties.styleValue instanceof RegExp) {
8156 // style value as Regexp
8157 if (node.style[prop].trim().match(properties.styleValue).length > 0) {
8158 hasOneStyle = true;
8159 break;
8161 } else if (Array.isArray(properties.styleValue)) {
8162 // style value as array
8163 if (properties.styleValue.indexOf(node.style[prop].trim())) {
8164 hasOneStyle = true;
8165 break;
8167 } else {
8168 // style value as string
8169 if (properties.styleValue === node.style[prop].trim().replace(/, /g, ",")) {
8170 hasOneStyle = true;
8171 break;
8174 } else {
8175 hasOneStyle = true;
8176 break;
8179 if (!hasOneStyle) {
8180 return false;
8185 if (properties.attribute) {
8186 var attr = wysihtml.dom.getAttributes(node),
8187 attrList = [],
8188 hasOneAttribute = false;
8190 if (Array.isArray(properties.attribute)) {
8191 attrList = properties.attribute;
8192 } else {
8193 attrList[properties.attribute] = properties.attributeValue;
8196 for (var a in attrList) {
8197 if (attrList.hasOwnProperty(a)) {
8198 if (typeof attrList[a] === "undefined") {
8199 if (typeof attr[a] !== "undefined") {
8200 hasOneAttribute = true;
8201 break;
8203 } else if (attr[a] === attrList[a]) {
8204 hasOneAttribute = true;
8205 break;
8210 if (!hasOneAttribute) {
8211 return false;
8216 return true;
8219 };
8220 };
8221 })(wysihtml);
8223 /**
8224 * Returns the given html wrapped in a div element
8226 * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly
8227 * when inserted via innerHTML
8229 * @param {String} html The html which should be wrapped in a dom element
8230 * @param {Obejct} [context] Document object of the context the html belongs to
8232 * @example
8233 * wysihtml.dom.getAsDom("<article>foo</article>");
8234 */
8235 wysihtml.dom.getAsDom = (function() {
8237 var _innerHTMLShiv = function(html, context) {
8238 var tempElement = context.createElement("div");
8239 tempElement.style.display = "none";
8240 context.body.appendChild(tempElement);
8241 // IE throws an exception when trying to insert <frameset></frameset> via innerHTML
8242 try { tempElement.innerHTML = html; } catch(e) {}
8243 context.body.removeChild(tempElement);
8244 return tempElement;
8245 };
8247 /**
8248 * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element
8249 */
8250 var _ensureHTML5Compatibility = function(context) {
8251 if (context._wysihtml_supportsHTML5Tags) {
8252 return;
8254 for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) {
8255 context.createElement(HTML5_ELEMENTS[i]);
8257 context._wysihtml_supportsHTML5Tags = true;
8258 };
8261 /**
8262 * List of html5 tags
8263 * taken from http://simon.html5.org/html5-elements
8264 */
8265 var HTML5_ELEMENTS = [
8266 "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption",
8267 "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress",
8268 "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr"
8269 ];
8271 return function(html, context) {
8272 context = context || document;
8273 var tempElement;
8274 if (typeof(html) === "object" && html.nodeType) {
8275 tempElement = context.createElement("div");
8276 tempElement.appendChild(html);
8277 } else if (wysihtml.browser.supportsHTML5Tags(context)) {
8278 tempElement = context.createElement("div");
8279 tempElement.innerHTML = html;
8280 } else {
8281 _ensureHTML5Compatibility(context);
8282 tempElement = _innerHTMLShiv(html, context);
8284 return tempElement;
8285 };
8286 })();
8288 /**
8289 * Get a set of attribute from one element
8291 * IE gives wrong results for hasAttribute/getAttribute, for example:
8292 * var td = document.createElement("td");
8293 * td.getAttribute("rowspan"); // => "1" in IE
8295 * Therefore we have to check the element's outerHTML for the attribute
8296 */
8298 wysihtml.dom.getAttribute = function(node, attributeName) {
8299 var HAS_GET_ATTRIBUTE_BUG = !wysihtml.browser.supportsGetAttributeCorrectly();
8300 attributeName = attributeName.toLowerCase();
8301 var nodeName = node.nodeName;
8302 if (nodeName == "IMG" && attributeName == "src" && wysihtml.dom.isLoadedImage(node) === true) {
8303 // Get 'src' attribute value via object property since this will always contain the
8304 // full absolute url (http://...)
8305 // this fixes a very annoying bug in firefox (ver 3.6 & 4) and IE 8 where images copied from the same host
8306 // will have relative paths, which the sanitizer strips out (see attributeCheckMethods.url)
8307 return node.src;
8308 } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) {
8309 // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML
8310 var outerHTML = node.outerHTML.toLowerCase(),
8311 // TODO: This might not work for attributes without value: <input disabled>
8312 hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1;
8314 return hasAttribute ? node.getAttribute(attributeName) : null;
8315 } else{
8316 return node.getAttribute(attributeName);
8318 };
8320 /**
8321 * Get all attributes of an element
8323 * IE gives wrong results for hasAttribute/getAttribute, for example:
8324 * var td = document.createElement("td");
8325 * td.getAttribute("rowspan"); // => "1" in IE
8327 * Therefore we have to check the element's outerHTML for the attribute
8328 */
8330 wysihtml.dom.getAttributes = function(node) {
8331 var HAS_GET_ATTRIBUTE_BUG = !wysihtml.browser.supportsGetAttributeCorrectly(),
8332 nodeName = node.nodeName,
8333 attributes = [],
8334 attr;
8336 for (attr in node.attributes) {
8337 if ((node.attributes.hasOwnProperty && node.attributes.hasOwnProperty(attr)) || (!node.attributes.hasOwnProperty && Object.prototype.hasOwnProperty.call(node.attributes, attr))) {
8338 if (node.attributes[attr].specified) {
8339 if (nodeName == "IMG" && node.attributes[attr].name.toLowerCase() == "src" && wysihtml.dom.isLoadedImage(node) === true) {
8340 attributes['src'] = node.src;
8341 } else if (wysihtml.lang.array(['rowspan', 'colspan']).contains(node.attributes[attr].name.toLowerCase()) && HAS_GET_ATTRIBUTE_BUG) {
8342 if (node.attributes[attr].value !== 1) {
8343 attributes[node.attributes[attr].name] = node.attributes[attr].value;
8345 } else {
8346 attributes[node.attributes[attr].name] = node.attributes[attr].value;
8351 return attributes;
8352 };
8354 /**
8355 * Walks the dom tree from the given node up until it finds a match
8357 * @param {Element} node The from which to check the parent nodes
8358 * @param {Object} matchingSet Object to match against, Properties for filtering element:
8359 * {
8360 * query: selector string,
8361 * classRegExp: regex,
8362 * styleProperty: string or [],
8363 * styleValue: string, [] or regex
8364 * }
8365 * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50)
8366 * @param {Element} Optional, defines the container that limits the search
8368 * @return {null|Element} Returns the first element that matched the desiredNodeName(s)
8369 */
8371 wysihtml.dom.getParentElement = (function() {
8373 return function(node, properties, levels, container) {
8374 levels = levels || 50;
8375 while (levels-- && node && node.nodeName !== "BODY" && (!container || node !== container)) {
8376 if (wysihtml.dom.domNode(node).test(properties)) {
8377 return node;
8379 node = node.parentNode;
8381 return null;
8382 };
8384 })();
8386 /*
8387 * Methods for fetching pasted html before it gets inserted into content
8388 **/
8390 /* Modern event.clipboardData driven approach.
8391 * Advantage is that it does not have to loose selection or modify dom to catch the data.
8392 * IE does not support though.
8393 **/
8394 wysihtml.dom.getPastedHtml = function(event) {
8395 var html;
8396 if (wysihtml.browser.supportsModernPaste() && event.clipboardData) {
8397 if (wysihtml.lang.array(event.clipboardData.types).contains('text/html')) {
8398 html = event.clipboardData.getData('text/html');
8399 } else if (wysihtml.lang.array(event.clipboardData.types).contains('text/plain')) {
8400 html = wysihtml.lang.string(event.clipboardData.getData('text/plain')).escapeHTML(true, true);
8403 return html;
8404 };
8406 /* Older temprorary contenteditable as paste source catcher method for fallbacks */
8407 wysihtml.dom.getPastedHtmlWithDiv = function (composer, f) {
8408 var selBookmark = composer.selection.getBookmark(),
8409 doc = composer.element.ownerDocument,
8410 cleanerDiv = doc.createElement('DIV'),
8411 scrollPos = composer.getScrollPos();
8413 doc.body.appendChild(cleanerDiv);
8415 cleanerDiv.style.width = "1px";
8416 cleanerDiv.style.height = "1px";
8417 cleanerDiv.style.overflow = "hidden";
8418 cleanerDiv.style.position = "absolute";
8419 cleanerDiv.style.top = scrollPos.y + "px";
8420 cleanerDiv.style.left = scrollPos.x + "px";
8422 cleanerDiv.setAttribute('contenteditable', 'true');
8423 cleanerDiv.focus();
8425 setTimeout(function () {
8426 var html;
8428 composer.selection.setBookmark(selBookmark);
8429 html = cleanerDiv.innerHTML;
8430 if (html && (/^<br\/?>$/i).test(html.trim())) {
8431 html = false;
8433 f(html);
8434 cleanerDiv.parentNode.removeChild(cleanerDiv);
8435 }, 0);
8436 };
8438 /**
8439 * Get element's style for a specific css property
8441 * @param {Element} element The element on which to retrieve the style
8442 * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...)
8444 * @example
8445 * wysihtml.dom.getStyle("display").from(document.body);
8446 * // => "block"
8447 */
8448 wysihtml.dom.getStyle = (function() {
8449 var stylePropertyMapping = {
8450 "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"
8451 },
8452 REG_EXP_CAMELIZE = /\-[a-z]/g;
8454 function camelize(str) {
8455 return str.replace(REG_EXP_CAMELIZE, function(match) {
8456 return match.charAt(1).toUpperCase();
8457 });
8460 return function(property) {
8461 return {
8462 from: function(element) {
8463 if (element.nodeType !== wysihtml.ELEMENT_NODE) {
8464 return;
8467 var doc = element.ownerDocument,
8468 camelizedProperty = stylePropertyMapping[property] || camelize(property),
8469 style = element.style,
8470 currentStyle = element.currentStyle,
8471 styleValue = style[camelizedProperty];
8472 if (styleValue) {
8473 return styleValue;
8476 // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant
8477 // window.getComputedStyle, since it returns css property values in their original unit:
8478 // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle
8479 // gives you the original "50%".
8480 // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio
8481 if (currentStyle) {
8482 try {
8483 return currentStyle[camelizedProperty];
8484 } catch(e) {
8485 //ie will occasionally fail for unknown reasons. swallowing exception
8489 var win = doc.defaultView || doc.parentWindow,
8490 needsOverflowReset = (property === "height" || property === "width") && element.nodeName === "TEXTAREA",
8491 originalOverflow,
8492 returnValue;
8494 if (win.getComputedStyle) {
8495 // Chrome and Safari both calculate a wrong width and height for textareas when they have scroll bars
8496 // therfore we remove and restore the scrollbar and calculate the value in between
8497 if (needsOverflowReset) {
8498 originalOverflow = style.overflow;
8499 style.overflow = "hidden";
8501 returnValue = win.getComputedStyle(element, null).getPropertyValue(property);
8502 if (needsOverflowReset) {
8503 style.overflow = originalOverflow || "";
8505 return returnValue;
8508 };
8509 };
8510 })();
8512 wysihtml.dom.getTextNodes = function(node, ingoreEmpty){
8513 var all = [];
8514 for (node=node.firstChild;node;node=node.nextSibling){
8515 if (node.nodeType == 3) {
8516 if (!ingoreEmpty || !(/^\s*$/).test(node.innerText || node.textContent)) {
8517 all.push(node);
8519 } else {
8520 all = all.concat(wysihtml.dom.getTextNodes(node, ingoreEmpty));
8523 return all;
8524 };
8526 /**
8527 * High performant way to check whether an element with a specific class name is in the given document
8528 * Optimized for being heavily executed
8529 * Unleashes the power of live node lists
8531 * @param {Object} doc The document object of the context where to check
8532 * @param {String} tagName Upper cased tag name
8533 * @example
8534 * wysihtml.dom.hasElementWithClassName(document, "foobar");
8535 */
8536 (function(wysihtml) {
8537 var LIVE_CACHE = {},
8538 DOCUMENT_IDENTIFIER = 1;
8540 function _getDocumentIdentifier(doc) {
8541 return doc._wysihtml_identifier || (doc._wysihtml_identifier = DOCUMENT_IDENTIFIER++);
8544 wysihtml.dom.hasElementWithClassName = function(doc, className) {
8545 // getElementsByClassName is not supported by IE<9
8546 // but is sometimes mocked via library code (which then doesn't return live node lists)
8547 if (!wysihtml.browser.supportsNativeGetElementsByClassName()) {
8548 return !!doc.querySelector("." + className);
8551 var key = _getDocumentIdentifier(doc) + ":" + className,
8552 cacheEntry = LIVE_CACHE[key];
8553 if (!cacheEntry) {
8554 cacheEntry = LIVE_CACHE[key] = doc.getElementsByClassName(className);
8557 return cacheEntry.length > 0;
8558 };
8559 })(wysihtml);
8561 /**
8562 * High performant way to check whether an element with a specific tag name is in the given document
8563 * Optimized for being heavily executed
8564 * Unleashes the power of live node lists
8566 * @param {Object} doc The document object of the context where to check
8567 * @param {String} tagName Upper cased tag name
8568 * @example
8569 * wysihtml.dom.hasElementWithTagName(document, "IMG");
8570 */
8571 wysihtml.dom.hasElementWithTagName = (function() {
8572 var LIVE_CACHE = {},
8573 DOCUMENT_IDENTIFIER = 1;
8575 function _getDocumentIdentifier(doc) {
8576 return doc._wysihtml_identifier || (doc._wysihtml_identifier = DOCUMENT_IDENTIFIER++);
8579 return function(doc, tagName) {
8580 var key = _getDocumentIdentifier(doc) + ":" + tagName,
8581 cacheEntry = LIVE_CACHE[key];
8582 if (!cacheEntry) {
8583 cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName);
8586 return cacheEntry.length > 0;
8587 };
8588 })();
8590 wysihtml.dom.insert = function(elementToInsert) {
8591 return {
8592 after: function(element) {
8593 element.parentNode.insertBefore(elementToInsert, element.nextSibling);
8594 },
8596 before: function(element) {
8597 element.parentNode.insertBefore(elementToInsert, element);
8598 },
8600 into: function(element) {
8601 element.appendChild(elementToInsert);
8603 };
8604 };
8606 wysihtml.dom.insertCSS = function(rules) {
8607 rules = rules.join("\n");
8609 return {
8610 into: function(doc) {
8611 var styleElement = doc.createElement("style");
8612 styleElement.type = "text/css";
8614 if (styleElement.styleSheet) {
8615 styleElement.styleSheet.cssText = rules;
8616 } else {
8617 styleElement.appendChild(doc.createTextNode(rules));
8620 var link = doc.querySelector("head link");
8621 if (link) {
8622 link.parentNode.insertBefore(styleElement, link);
8623 return;
8624 } else {
8625 var head = doc.querySelector("head");
8626 if (head) {
8627 head.appendChild(styleElement);
8631 };
8632 };
8634 /**
8635 * Check whether the given node is a proper loaded image
8636 * FIXME: Returns undefined when unknown (Chrome, Safari)
8637 */
8639 wysihtml.dom.isLoadedImage = function (node) {
8640 try {
8641 return node.complete && !node.mozMatchesSelector(":-moz-broken");
8642 } catch(e) {
8643 if (node.complete && node.readyState === "complete") {
8644 return true;
8647 };
8649 // TODO: Refactor dom tree traversing here
8650 (function(wysihtml) {
8651 wysihtml.dom.lineBreaks = function(node) {
8653 function _isLineBreak(n) {
8654 return n.nodeName === "BR";
8657 /**
8658 * Checks whether the elment causes a visual line break
8659 * (<br> or block elements)
8660 */
8661 function _isLineBreakOrBlockElement(element) {
8662 if (_isLineBreak(element)) {
8663 return true;
8666 if (wysihtml.dom.getStyle("display").from(element) === "block") {
8667 return true;
8670 return false;
8673 return {
8675 /* wysihtml.dom.lineBreaks(element).add();
8677 * Adds line breaks before and after the given node if the previous and next siblings
8678 * aren't already causing a visual line break (block element or <br>)
8679 */
8680 add: function(options) {
8681 var doc = node.ownerDocument,
8682 nextSibling = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}),
8683 previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true});
8685 if (nextSibling && !_isLineBreakOrBlockElement(nextSibling)) {
8686 wysihtml.dom.insert(doc.createElement("br")).after(node);
8688 if (previousSibling && !_isLineBreakOrBlockElement(previousSibling)) {
8689 wysihtml.dom.insert(doc.createElement("br")).before(node);
8691 },
8693 /* wysihtml.dom.lineBreaks(element).remove();
8695 * Removes line breaks before and after the given node
8696 */
8697 remove: function(options) {
8698 var nextSibling = wysihtml.dom.domNode(node).next({ignoreBlankTexts: true}),
8699 previousSibling = wysihtml.dom.domNode(node).prev({ignoreBlankTexts: true});
8701 if (nextSibling && _isLineBreak(nextSibling)) {
8702 nextSibling.parentNode.removeChild(nextSibling);
8704 if (previousSibling && _isLineBreak(previousSibling)) {
8705 previousSibling.parentNode.removeChild(previousSibling);
8708 };
8709 };
8710 })(wysihtml);
8711 /**
8712 * Method to set dom events
8714 * @example
8715 * wysihtml.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... });
8716 */
8717 wysihtml.dom.observe = function(element, eventNames, handler) {
8718 eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames;
8720 var handlerWrapper,
8721 eventName,
8722 i = 0,
8723 length = eventNames.length;
8725 for (; i<length; i++) {
8726 eventName = eventNames[i];
8727 if (element.addEventListener) {
8728 element.addEventListener(eventName, handler, false);
8729 } else {
8730 handlerWrapper = function(event) {
8731 if (!("target" in event)) {
8732 event.target = event.srcElement;
8734 event.preventDefault = event.preventDefault || function() {
8735 this.returnValue = false;
8736 };
8737 event.stopPropagation = event.stopPropagation || function() {
8738 this.cancelBubble = true;
8739 };
8740 handler.call(element, event);
8741 };
8742 element.attachEvent("on" + eventName, handlerWrapper);
8746 return {
8747 stop: function() {
8748 var eventName,
8749 i = 0,
8750 length = eventNames.length;
8751 for (; i<length; i++) {
8752 eventName = eventNames[i];
8753 if (element.removeEventListener) {
8754 element.removeEventListener(eventName, handler, false);
8755 } else {
8756 element.detachEvent("on" + eventName, handlerWrapper);
8760 };
8761 };
8763 /**
8764 * HTML Sanitizer
8765 * Rewrites the HTML based on given rules
8767 * @param {Element|String} elementOrHtml HTML String to be sanitized OR element whose content should be sanitized
8768 * @param {Object} [rules] List of rules for rewriting the HTML, if there's no rule for an element it will
8769 * be converted to a "span". Each rule is a key/value pair where key is the tag to convert, and value the
8770 * desired substitution.
8771 * @param {Object} context Document object in which to parse the html, needed to sandbox the parsing
8773 * @return {Element|String} Depends on the elementOrHtml parameter. When html then the sanitized html as string elsewise the element.
8775 * @example
8776 * var userHTML = '<div id="foo" onclick="alert(1);"><p><font color="red">foo</font><script>alert(1);</script></p></div>';
8777 * wysihtml.dom.parse(userHTML, {
8778 * tags {
8779 * p: "div", // Rename p tags to div tags
8780 * font: "span" // Rename font tags to span tags
8781 * div: true, // Keep them, also possible (same result when passing: "div" or true)
8782 * script: undefined // Remove script elements
8783 * }
8784 * });
8785 * // => <div><div><span>foo bar</span></div></div>
8787 * var userHTML = '<table><tbody><tr><td>I'm a table!</td></tr></tbody></table>';
8788 * wysihtml.dom.parse(userHTML);
8789 * // => '<span><span><span><span>I'm a table!</span></span></span></span>'
8791 * var userHTML = '<div>foobar<br>foobar</div>';
8792 * wysihtml.dom.parse(userHTML, {
8793 * tags: {
8794 * div: undefined,
8795 * br: true
8796 * }
8797 * });
8798 * // => ''
8800 * var userHTML = '<div class="red">foo</div><div class="pink">bar</div>';
8801 * wysihtml.dom.parse(userHTML, {
8802 * classes: {
8803 * red: 1,
8804 * green: 1
8805 * },
8806 * tags: {
8807 * div: {
8808 * rename_tag: "p"
8809 * }
8810 * }
8811 * });
8812 * // => '<p class="red">foo</p><p>bar</p>'
8813 */
8815 wysihtml.dom.parse = function(elementOrHtml_current, config_current) {
8816 /* TODO: Currently escaped module pattern as otherwise folloowing default swill be shared among multiple editors.
8817 * Refactor whole code as this method while workind is kind of awkward too */
8819 /**
8820 * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML
8821 * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the
8822 * node isn't closed
8824 * Therefore we've to use the browser's ordinary HTML parser invoked by setting innerHTML.
8825 */
8826 var NODE_TYPE_MAPPING = {
8827 "1": _handleElement,
8828 "3": _handleText,
8829 "8": _handleComment
8830 },
8831 // Rename unknown tags to this
8832 DEFAULT_NODE_NAME = "span",
8833 WHITE_SPACE_REG_EXP = /\s+/,
8834 defaultRules = { tags: {}, classes: {} },
8835 currentRules = {},
8836 blockElements = ["ADDRESS" ,"BLOCKQUOTE" ,"CENTER" ,"DIR" ,"DIV" ,"DL" ,"FIELDSET" ,
8837 "FORM", "H1" ,"H2" ,"H3" ,"H4" ,"H5" ,"H6" ,"ISINDEX" ,"MENU",
8838 "NOFRAMES", "NOSCRIPT" ,"OL" ,"P" ,"PRE","TABLE", "UL"];
8840 /**
8841 * Iterates over all childs of the element, recreates them, appends them into a document fragment
8842 * which later replaces the entire body content
8843 */
8844 function parse(elementOrHtml, config) {
8845 wysihtml.lang.object(currentRules).merge(defaultRules).merge(config.rules).get();
8847 var context = config.context || elementOrHtml.ownerDocument || document,
8848 fragment = context.createDocumentFragment(),
8849 isString = typeof(elementOrHtml) === "string",
8850 clearInternals = false,
8851 element,
8852 newNode,
8853 firstChild;
8855 if (config.clearInternals === true) {
8856 clearInternals = true;
8859 if (isString) {
8860 element = wysihtml.dom.getAsDom(elementOrHtml, context);
8861 } else {
8862 element = elementOrHtml;
8865 if (currentRules.selectors) {
8866 _applySelectorRules(element, currentRules.selectors);
8869 while (element.firstChild) {
8870 firstChild = element.firstChild;
8871 newNode = _convert(firstChild, config.cleanUp, clearInternals, config.uneditableClass);
8872 if (newNode) {
8873 fragment.appendChild(newNode);
8875 if (firstChild !== newNode) {
8876 element.removeChild(firstChild);
8880 if (config.unjoinNbsps) {
8881 // replace joined non-breakable spaces with unjoined
8882 var txtnodes = wysihtml.dom.getTextNodes(fragment);
8883 for (var n = txtnodes.length; n--;) {
8884 txtnodes[n].nodeValue = txtnodes[n].nodeValue.replace(/([\S\u00A0])\u00A0/gi, "$1 ");
8888 // Clear element contents
8889 element.innerHTML = "";
8891 // Insert new DOM tree
8892 element.appendChild(fragment);
8894 return isString ? wysihtml.quirks.getCorrectInnerHTML(element) : element;
8897 function _convert(oldNode, cleanUp, clearInternals, uneditableClass) {
8898 var oldNodeType = oldNode.nodeType,
8899 oldChilds = oldNode.childNodes,
8900 oldChildsLength = oldChilds.length,
8901 method = NODE_TYPE_MAPPING[oldNodeType],
8902 i = 0,
8903 fragment,
8904 newNode,
8905 newChild,
8906 nodeDisplay;
8908 // Passes directly elemets with uneditable class
8909 if (uneditableClass && oldNodeType === 1 && wysihtml.dom.hasClass(oldNode, uneditableClass)) {
8910 return oldNode;
8913 newNode = method && method(oldNode, clearInternals);
8915 // Remove or unwrap node in case of return value null or false
8916 if (!newNode) {
8917 if (newNode === false) {
8918 // false defines that tag should be removed but contents should remain (unwrap)
8919 fragment = oldNode.ownerDocument.createDocumentFragment();
8921 for (i = oldChildsLength; i--;) {
8922 if (oldChilds[i]) {
8923 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
8924 if (newChild) {
8925 if (oldChilds[i] === newChild) {
8926 i--;
8928 fragment.insertBefore(newChild, fragment.firstChild);
8933 nodeDisplay = wysihtml.dom.getStyle("display").from(oldNode);
8935 if (nodeDisplay === '') {
8936 // Handle display style when element not in dom
8937 nodeDisplay = wysihtml.lang.array(blockElements).contains(oldNode.tagName) ? "block" : "";
8939 if (wysihtml.lang.array(["block", "flex", "table"]).contains(nodeDisplay)) {
8940 fragment.appendChild(oldNode.ownerDocument.createElement("br"));
8943 // TODO: try to minimize surplus spaces
8944 if (wysihtml.lang.array([
8945 "div", "pre", "p",
8946 "table", "td", "th",
8947 "ul", "ol", "li",
8948 "dd", "dl",
8949 "footer", "header", "section",
8950 "h1", "h2", "h3", "h4", "h5", "h6"
8951 ]).contains(oldNode.nodeName.toLowerCase()) && oldNode.parentNode.lastChild !== oldNode) {
8952 // add space at first when unwraping non-textflow elements
8953 if (!oldNode.nextSibling || oldNode.nextSibling.nodeType !== 3 || !(/^\s/).test(oldNode.nextSibling.nodeValue)) {
8954 fragment.appendChild(oldNode.ownerDocument.createTextNode(" "));
8958 if (fragment.normalize) {
8959 fragment.normalize();
8961 return fragment;
8962 } else {
8963 // Remove
8964 return null;
8968 // Converts all childnodes
8969 for (i=0; i<oldChildsLength; i++) {
8970 if (oldChilds[i]) {
8971 newChild = _convert(oldChilds[i], cleanUp, clearInternals, uneditableClass);
8972 if (newChild) {
8973 if (oldChilds[i] === newChild) {
8974 i--;
8976 newNode.appendChild(newChild);
8981 // Cleanup senseless <span> elements
8982 if (cleanUp &&
8983 newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME &&
8984 (!newNode.childNodes.length ||
8985 ((/^\s*$/gi).test(newNode.innerHTML) && (clearInternals || (oldNode.className !== "_wysihtml-temp-placeholder" && oldNode.className !== "rangySelectionBoundary"))) ||
8986 !newNode.attributes.length)
8987 ) {
8988 fragment = newNode.ownerDocument.createDocumentFragment();
8989 while (newNode.firstChild) {
8990 fragment.appendChild(newNode.firstChild);
8992 if (fragment.normalize) {
8993 fragment.normalize();
8995 return fragment;
8998 if (newNode.normalize) {
8999 newNode.normalize();
9001 return newNode;
9004 function _applySelectorRules (element, selectorRules) {
9005 var sel, method, els;
9007 for (sel in selectorRules) {
9008 if (selectorRules.hasOwnProperty(sel)) {
9009 if (wysihtml.lang.object(selectorRules[sel]).isFunction()) {
9010 method = selectorRules[sel];
9011 } else if (typeof(selectorRules[sel]) === "string" && elementHandlingMethods[selectorRules[sel]]) {
9012 method = elementHandlingMethods[selectorRules[sel]];
9014 els = element.querySelectorAll(sel);
9015 for (var i = els.length; i--;) {
9016 method(els[i]);
9022 function _handleElement(oldNode, clearInternals) {
9023 var rule,
9024 newNode,
9025 tagRules = currentRules.tags,
9026 nodeName = oldNode.nodeName.toLowerCase(),
9027 scopeName = oldNode.scopeName,
9028 renameTag;
9030 /**
9031 * We already parsed that element
9032 * ignore it! (yes, this sometimes happens in IE8 when the html is invalid)
9033 */
9034 if (oldNode._wysihtml) {
9035 return null;
9037 oldNode._wysihtml = 1;
9039 if (oldNode.className === "wysihtml-temp") {
9040 return null;
9043 /**
9044 * IE is the only browser who doesn't include the namespace in the
9045 * nodeName, that's why we have to prepend it by ourselves
9046 * scopeName is a proprietary IE feature
9047 * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx
9048 */
9049 if (scopeName && scopeName != "HTML") {
9050 nodeName = scopeName + ":" + nodeName;
9052 /**
9053 * Repair node
9054 * IE is a bit bitchy when it comes to invalid nested markup which includes unclosed tags
9055 * 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
9056 */
9057 if ("outerHTML" in oldNode) {
9058 if (!wysihtml.browser.autoClosesUnclosedTags() &&
9059 oldNode.nodeName === "P" &&
9060 oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") {
9061 nodeName = "div";
9065 if (nodeName in tagRules) {
9066 rule = tagRules[nodeName];
9067 if (!rule || rule.remove) {
9068 return null;
9069 } else if (rule.unwrap) {
9070 return false;
9072 rule = typeof(rule) === "string" ? { rename_tag: rule } : rule;
9073 } else if (oldNode.firstChild) {
9074 rule = { rename_tag: DEFAULT_NODE_NAME };
9075 } else {
9076 // Remove empty unknown elements
9077 return null;
9080 // tests if type condition is met or node should be removed/unwrapped/renamed
9081 if (rule.one_of_type && !_testTypes(oldNode, currentRules, rule.one_of_type, clearInternals)) {
9082 if (rule.remove_action) {
9083 if (rule.remove_action === "unwrap") {
9084 return false;
9085 } else if (rule.remove_action === "rename") {
9086 renameTag = rule.remove_action_rename_to || DEFAULT_NODE_NAME;
9087 } else {
9088 return null;
9090 } else {
9091 return null;
9095 newNode = oldNode.ownerDocument.createElement(renameTag || rule.rename_tag || nodeName);
9096 _handleAttributes(oldNode, newNode, rule, clearInternals);
9097 _handleStyles(oldNode, newNode, rule);
9099 oldNode = null;
9101 if (newNode.normalize) { newNode.normalize(); }
9102 return newNode;
9105 function _testTypes(oldNode, rules, types, clearInternals) {
9106 var definition, type;
9108 // do not interfere with placeholder span or pasting caret position is not maintained
9109 if (oldNode.nodeName === "SPAN" && !clearInternals && (oldNode.className === "_wysihtml-temp-placeholder" || oldNode.className === "rangySelectionBoundary")) {
9110 return true;
9113 for (type in types) {
9114 if (types.hasOwnProperty(type) && rules.type_definitions && rules.type_definitions[type]) {
9115 definition = rules.type_definitions[type];
9116 if (_testType(oldNode, definition)) {
9117 return true;
9121 return false;
9124 function array_contains(a, obj) {
9125 var i = a.length;
9126 while (i--) {
9127 if (a[i] === obj) {
9128 return true;
9131 return false;
9134 function _testType(oldNode, definition) {
9136 var nodeClasses = oldNode.getAttribute("class"),
9137 nodeStyles = oldNode.getAttribute("style"),
9138 classesLength, s, s_corrected, a, attr, currentClass, styleProp;
9140 // test for methods
9141 if (definition.methods) {
9142 for (var m in definition.methods) {
9143 if (definition.methods.hasOwnProperty(m) && typeCeckMethods[m]) {
9145 if (typeCeckMethods[m](oldNode)) {
9146 return true;
9152 // test for classes, if one found return true
9153 if (nodeClasses && definition.classes) {
9154 nodeClasses = nodeClasses.replace(/^\s+/g, '').replace(/\s+$/g, '').split(WHITE_SPACE_REG_EXP);
9155 classesLength = nodeClasses.length;
9156 for (var i = 0; i < classesLength; i++) {
9157 if (definition.classes[nodeClasses[i]]) {
9158 return true;
9163 // test for styles, if one found return true
9164 if (nodeStyles && definition.styles) {
9166 nodeStyles = nodeStyles.split(';');
9167 for (s in definition.styles) {
9168 if (definition.styles.hasOwnProperty(s)) {
9169 for (var sp = nodeStyles.length; sp--;) {
9170 styleProp = nodeStyles[sp].split(':');
9172 if (styleProp[0].replace(/\s/g, '').toLowerCase() === s) {
9173 if (definition.styles[s] === true || definition.styles[s] === 1 || wysihtml.lang.array(definition.styles[s]).contains(styleProp[1].replace(/\s/g, '').toLowerCase()) ) {
9174 return true;
9182 // test for attributes in general against regex match
9183 if (definition.attrs) {
9184 for (a in definition.attrs) {
9185 if (definition.attrs.hasOwnProperty(a)) {
9186 attr = wysihtml.dom.getAttribute(oldNode, a);
9187 if (typeof(attr) === "string") {
9188 if (attr.search(definition.attrs[a]) > -1) {
9189 return true;
9195 return false;
9198 function _handleStyles(oldNode, newNode, rule) {
9199 var s, v;
9200 if(rule && rule.keep_styles) {
9201 for (s in rule.keep_styles) {
9202 if (rule.keep_styles.hasOwnProperty(s)) {
9203 v = (s === "float") ? oldNode.style.styleFloat || oldNode.style.cssFloat : oldNode.style[s];
9204 // value can be regex and if so should match or style skipped
9205 if (rule.keep_styles[s] instanceof RegExp && !(rule.keep_styles[s].test(v))) {
9206 continue;
9208 if (s === "float") {
9209 // IE compability
9210 newNode.style[(oldNode.style.styleFloat) ? 'styleFloat': 'cssFloat'] = v;
9211 } else if (oldNode.style[s]) {
9212 newNode.style[s] = v;
9217 };
9219 function _getAttributesBeginningWith(beginning, attributes) {
9220 var returnAttributes = [];
9221 for (var attr in attributes) {
9222 if (attributes.hasOwnProperty(attr) && attr.indexOf(beginning) === 0) {
9223 returnAttributes.push(attr);
9226 return returnAttributes;
9229 function _checkAttribute(attributeName, attributeValue, methodName, nodeName) {
9230 var method = wysihtml.lang.object(methodName).isFunction() ? methodName : attributeCheckMethods[methodName],
9231 newAttributeValue;
9233 if (method) {
9234 newAttributeValue = method(attributeValue, nodeName);
9235 if (typeof(newAttributeValue) === "string") {
9236 return newAttributeValue;
9240 return false;
9243 function _checkAttributes(oldNode, local_attributes) {
9244 var globalAttributes = wysihtml.lang.object(currentRules.attributes || {}).clone(), // global values for check/convert values of attributes
9245 checkAttributes = wysihtml.lang.object(globalAttributes).merge( wysihtml.lang.object(local_attributes || {}).clone()).get(),
9246 attributes = {},
9247 oldAttributes = wysihtml.dom.getAttributes(oldNode),
9248 attributeName, newValue, matchingAttributes;
9250 for (attributeName in checkAttributes) {
9251 if ((/\*$/).test(attributeName)) {
9253 matchingAttributes = _getAttributesBeginningWith(attributeName.slice(0,-1), oldAttributes);
9254 for (var i = 0, imax = matchingAttributes.length; i < imax; i++) {
9256 newValue = _checkAttribute(matchingAttributes[i], oldAttributes[matchingAttributes[i]], checkAttributes[attributeName], oldNode.nodeName);
9257 if (newValue !== false) {
9258 attributes[matchingAttributes[i]] = newValue;
9261 } else {
9262 newValue = _checkAttribute(attributeName, oldAttributes[attributeName], checkAttributes[attributeName], oldNode.nodeName);
9263 if (newValue !== false) {
9264 attributes[attributeName] = newValue;
9269 return attributes;
9272 // TODO: refactor. Too long to read
9273 function _handleAttributes(oldNode, newNode, rule, clearInternals) {
9274 var attributes = {}, // fresh new set of attributes to set on newNode
9275 setClass = rule.set_class, // classes to set
9276 addClass = rule.add_class, // add classes based on existing attributes
9277 addStyle = rule.add_style, // add styles based on existing attributes
9278 setAttributes = rule.set_attributes, // attributes to set on the current node
9279 allowedClasses = currentRules.classes,
9280 i = 0,
9281 classes = [],
9282 styles = [],
9283 newClasses = [],
9284 oldClasses = [],
9285 classesLength,
9286 newClassesLength,
9287 currentClass,
9288 newClass,
9289 attributeName,
9290 method;
9292 if (setAttributes) {
9293 attributes = wysihtml.lang.object(setAttributes).clone();
9296 // check/convert values of attributes
9297 attributes = wysihtml.lang.object(attributes).merge(_checkAttributes(oldNode, rule.check_attributes)).get();
9299 if (setClass) {
9300 classes.push(setClass);
9303 if (addClass) {
9304 for (attributeName in addClass) {
9305 method = addClassMethods[addClass[attributeName]];
9306 if (!method) {
9307 continue;
9309 newClass = method(wysihtml.dom.getAttribute(oldNode, attributeName));
9310 if (typeof(newClass) === "string") {
9311 classes.push(newClass);
9316 if (addStyle) {
9317 for (attributeName in addStyle) {
9318 method = addStyleMethods[addStyle[attributeName]];
9319 if (!method) {
9320 continue;
9323 newStyle = method(wysihtml.dom.getAttribute(oldNode, attributeName));
9324 if (typeof(newStyle) === "string") {
9325 styles.push(newStyle);
9331 if (typeof(allowedClasses) === "string" && allowedClasses === "any") {
9332 if (oldNode.getAttribute("class")) {
9333 if (currentRules.classes_blacklist) {
9334 oldClasses = oldNode.getAttribute("class");
9335 if (oldClasses) {
9336 classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
9339 classesLength = classes.length;
9340 for (; i<classesLength; i++) {
9341 currentClass = classes[i];
9342 if (!currentRules.classes_blacklist[currentClass]) {
9343 newClasses.push(currentClass);
9347 if (newClasses.length) {
9348 attributes["class"] = wysihtml.lang.array(newClasses).unique().join(" ");
9351 } else {
9352 attributes["class"] = oldNode.getAttribute("class");
9354 } else {
9355 if(classes && classes.length > 0) {
9356 attributes["class"] = wysihtml.lang.array(classes).unique().join(" ");
9359 } else {
9360 // make sure that wysihtml temp class doesn't get stripped out
9361 if (!clearInternals) {
9362 allowedClasses["_wysihtml-temp-placeholder"] = 1;
9363 allowedClasses["_rangySelectionBoundary"] = 1;
9364 allowedClasses["wysiwyg-tmp-selected-cell"] = 1;
9367 // add old classes last
9368 oldClasses = oldNode.getAttribute("class");
9369 if (oldClasses) {
9370 classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP));
9372 classesLength = classes.length;
9373 for (; i<classesLength; i++) {
9374 currentClass = classes[i];
9375 if (allowedClasses[currentClass]) {
9376 newClasses.push(currentClass);
9380 if (newClasses.length) {
9381 attributes["class"] = wysihtml.lang.array(newClasses).unique().join(" ");
9385 // remove table selection class if present
9386 if (attributes["class"] && clearInternals) {
9387 attributes["class"] = attributes["class"].replace("wysiwyg-tmp-selected-cell", "");
9388 if ((/^\s*$/g).test(attributes["class"])) {
9389 delete attributes["class"];
9393 if (styles.length) {
9394 attributes["style"] = wysihtml.lang.array(styles).unique().join(" ");
9397 // set attributes on newNode
9398 for (attributeName in attributes) {
9399 // Setting attributes can cause a js error in IE under certain circumstances
9400 // eg. on a <img> under https when it's new attribute value is non-https
9401 // TODO: Investigate this further and check for smarter handling
9402 try {
9403 newNode.setAttribute(attributeName, attributes[attributeName]);
9404 } catch(e) {}
9407 // IE8 sometimes loses the width/height attributes when those are set before the "src"
9408 // so we make sure to set them again
9409 if (attributes.src) {
9410 if (typeof(attributes.width) !== "undefined") {
9411 newNode.setAttribute("width", attributes.width);
9413 if (typeof(attributes.height) !== "undefined") {
9414 newNode.setAttribute("height", attributes.height);
9419 function _handleText(oldNode) {
9420 var nextSibling = oldNode.nextSibling;
9421 if (nextSibling && nextSibling.nodeType === wysihtml.TEXT_NODE) {
9422 // Concatenate text nodes
9423 nextSibling.data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "") + nextSibling.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
9424 } else {
9425 // \uFEFF = wysihtml.INVISIBLE_SPACE (used as a hack in certain rich text editing situations)
9426 var data = oldNode.data.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
9427 return oldNode.ownerDocument.createTextNode(data);
9431 function _handleComment(oldNode) {
9432 if (currentRules.comments) {
9433 return oldNode.ownerDocument.createComment(oldNode.nodeValue);
9437 // ------------ attribute checks ------------ \\
9438 var attributeCheckMethods = {
9439 url: (function() {
9440 var REG_EXP = /^https?:\/\//i;
9441 return function(attributeValue) {
9442 if (!attributeValue || !attributeValue.match(REG_EXP)) {
9443 return null;
9445 return attributeValue.replace(REG_EXP, function(match) {
9446 return match.toLowerCase();
9447 });
9448 };
9449 })(),
9451 src: (function() {
9452 var REG_EXP = /^(\/|https?:\/\/)/i;
9453 return function(attributeValue) {
9454 if (!attributeValue || !attributeValue.match(REG_EXP)) {
9455 return null;
9457 return attributeValue.replace(REG_EXP, function(match) {
9458 return match.toLowerCase();
9459 });
9460 };
9461 })(),
9463 href: (function() {
9464 var REG_EXP = /^(#|\/|https?:\/\/|mailto:|tel:)/i;
9465 return function(attributeValue) {
9466 if (!attributeValue || !attributeValue.match(REG_EXP)) {
9467 return null;
9469 return attributeValue.replace(REG_EXP, function(match) {
9470 return match.toLowerCase();
9471 });
9472 };
9473 })(),
9475 alt: (function() {
9476 var REG_EXP = /[^ a-z0-9_\-]/gi;
9477 return function(attributeValue, nodeName) {
9478 if (!attributeValue) {
9479 if (nodeName === "IMG") {
9480 return "";
9481 } else {
9482 return null;
9485 return attributeValue.replace(REG_EXP, "");
9486 };
9487 })(),
9489 // Integers. Does not work with floating point numbers and units
9490 numbers: (function() {
9491 var REG_EXP = /\D/g;
9492 return function(attributeValue) {
9493 attributeValue = (attributeValue || "").replace(REG_EXP, "");
9494 return attributeValue || null;
9495 };
9496 })(),
9498 // Useful for with/height attributes where floating points and percentages are allowed
9499 dimension: (function() {
9500 var REG_EXP = /\D*(\d+)(\.\d+)?\s?(%)?\D*/;
9501 return function(attributeValue) {
9502 attributeValue = (attributeValue || "").replace(REG_EXP, "$1$2$3");
9503 return attributeValue || null;
9504 };
9505 })(),
9507 any: (function() {
9508 return function(attributeValue) {
9509 if (!attributeValue) {
9510 return null;
9512 return attributeValue;
9513 };
9514 })()
9515 };
9517 // ------------ style converter (converts an html attribute to a style) ------------ \\
9518 var addStyleMethods = {
9519 align_text: (function() {
9520 var mapping = {
9521 left: "text-align: left;",
9522 right: "text-align: right;",
9523 center: "text-align: center;"
9524 };
9525 return function(attributeValue) {
9526 return mapping[String(attributeValue).toLowerCase()];
9527 };
9528 })(),
9529 };
9531 // ------------ class converter (converts an html attribute to a class name) ------------ \\
9532 var addClassMethods = {
9533 align_img: (function() {
9534 var mapping = {
9535 left: "wysiwyg-float-left",
9536 right: "wysiwyg-float-right"
9537 };
9538 return function(attributeValue) {
9539 return mapping[String(attributeValue).toLowerCase()];
9540 };
9541 })(),
9543 align_text: (function() {
9544 var mapping = {
9545 left: "wysiwyg-text-align-left",
9546 right: "wysiwyg-text-align-right",
9547 center: "wysiwyg-text-align-center",
9548 justify: "wysiwyg-text-align-justify"
9549 };
9550 return function(attributeValue) {
9551 return mapping[String(attributeValue).toLowerCase()];
9552 };
9553 })(),
9555 clear_br: (function() {
9556 var mapping = {
9557 left: "wysiwyg-clear-left",
9558 right: "wysiwyg-clear-right",
9559 both: "wysiwyg-clear-both",
9560 all: "wysiwyg-clear-both"
9561 };
9562 return function(attributeValue) {
9563 return mapping[String(attributeValue).toLowerCase()];
9564 };
9565 })(),
9567 size_font: (function() {
9568 var mapping = {
9569 "1": "wysiwyg-font-size-xx-small",
9570 "2": "wysiwyg-font-size-small",
9571 "3": "wysiwyg-font-size-medium",
9572 "4": "wysiwyg-font-size-large",
9573 "5": "wysiwyg-font-size-x-large",
9574 "6": "wysiwyg-font-size-xx-large",
9575 "7": "wysiwyg-font-size-xx-large",
9576 "-": "wysiwyg-font-size-smaller",
9577 "+": "wysiwyg-font-size-larger"
9578 };
9579 return function(attributeValue) {
9580 return mapping[String(attributeValue).charAt(0)];
9581 };
9582 })()
9583 };
9585 // checks if element is possibly visible
9586 var typeCeckMethods = {
9587 has_visible_contet: (function() {
9588 var txt,
9589 isVisible = false,
9590 visibleElements = ['img', 'video', 'picture', 'br', 'script', 'noscript',
9591 'style', 'table', 'iframe', 'object', 'embed', 'audio',
9592 'svg', 'input', 'button', 'select','textarea', 'canvas'];
9594 return function(el) {
9596 // has visible innertext. so is visible
9597 txt = (el.innerText || el.textContent).replace(/\s/g, '');
9598 if (txt && txt.length > 0) {
9599 return true;
9602 // matches list of visible dimensioned elements
9603 for (var i = visibleElements.length; i--;) {
9604 if (el.querySelector(visibleElements[i])) {
9605 return true;
9609 // try to measure dimesions in last resort. (can find only of elements in dom)
9610 if (el.offsetWidth && el.offsetWidth > 0 && el.offsetHeight && el.offsetHeight > 0) {
9611 return true;
9614 return false;
9615 };
9616 })()
9617 };
9619 var elementHandlingMethods = {
9620 unwrap: function (element) {
9621 wysihtml.dom.unwrap(element);
9622 },
9624 remove: function (element) {
9625 element.parentNode.removeChild(element);
9627 };
9629 return parse(elementOrHtml_current, config_current);
9630 };
9632 // does a selector query on element or array of elements
9633 wysihtml.dom.query = function(elements, query) {
9634 var ret = [],
9635 q;
9637 if (elements.nodeType) {
9638 elements = [elements];
9641 for (var e = 0, len = elements.length; e < len; e++) {
9642 q = elements[e].querySelectorAll(query);
9643 if (q) {
9644 for(var i = q.length; i--; ret.unshift(q[i]));
9647 return ret;
9648 };
9650 /**
9651 * Checks for empty text node childs and removes them
9653 * @param {Element} node The element in which to cleanup
9654 * @example
9655 * wysihtml.dom.removeEmptyTextNodes(element);
9656 */
9657 wysihtml.dom.removeEmptyTextNodes = function(node) {
9658 var childNode,
9659 childNodes = wysihtml.lang.array(node.childNodes).get(),
9660 childNodesLength = childNodes.length,
9661 i = 0;
9663 for (; i<childNodesLength; i++) {
9664 childNode = childNodes[i];
9665 if (childNode.nodeType === wysihtml.TEXT_NODE && (/^[\n\r]*$/).test(childNode.data)) {
9666 childNode.parentNode.removeChild(childNode);
9669 };
9671 wysihtml.dom.removeInvisibleSpaces = function(node) {
9672 var textNodes = wysihtml.dom.getTextNodes(node);
9673 for (var n = textNodes.length; n--;) {
9674 textNodes[n].nodeValue = textNodes[n].nodeValue.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
9676 };
9678 /**
9679 * Renames an element (eg. a <div> to a <p>) and keeps its childs
9681 * @param {Element} element The list element which should be renamed
9682 * @param {Element} newNodeName The desired tag name
9684 * @example
9685 * <!-- Assume the following dom: -->
9686 * <ul id="list">
9687 * <li>eminem</li>
9688 * <li>dr. dre</li>
9689 * <li>50 Cent</li>
9690 * </ul>
9692 * <script>
9693 * wysihtml.dom.renameElement(document.getElementById("list"), "ol");
9694 * </script>
9696 * <!-- Will result in: -->
9697 * <ol>
9698 * <li>eminem</li>
9699 * <li>dr. dre</li>
9700 * <li>50 Cent</li>
9701 * </ol>
9702 */
9703 wysihtml.dom.renameElement = function(element, newNodeName) {
9704 var newElement = element.ownerDocument.createElement(newNodeName),
9705 firstChild;
9706 while (firstChild = element.firstChild) {
9707 newElement.appendChild(firstChild);
9709 wysihtml.dom.copyAttributes(["align", "className"]).from(element).to(newElement);
9711 if (element.parentNode) {
9712 element.parentNode.replaceChild(newElement, element);
9715 return newElement;
9716 };
9718 /**
9719 * Takes an element, removes it and replaces it with it's childs
9721 * @param {Object} node The node which to replace with it's child nodes
9722 * @example
9723 * <div id="foo">
9724 * <span>hello</span>
9725 * </div>
9726 * <script>
9727 * // Remove #foo and replace with it's children
9728 * wysihtml.dom.replaceWithChildNodes(document.getElementById("foo"));
9729 * </script>
9730 */
9731 wysihtml.dom.replaceWithChildNodes = function(node) {
9732 if (!node.parentNode) {
9733 return;
9736 while (node.firstChild) {
9737 node.parentNode.insertBefore(node.firstChild, node);
9739 node.parentNode.removeChild(node);
9740 };
9742 /**
9743 * Unwraps an unordered/ordered list
9745 * @param {Element} element The list element which should be unwrapped
9747 * @example
9748 * <!-- Assume the following dom: -->
9749 * <ul id="list">
9750 * <li>eminem</li>
9751 * <li>dr. dre</li>
9752 * <li>50 Cent</li>
9753 * </ul>
9755 * <script>
9756 * wysihtml.dom.resolveList(document.getElementById("list"));
9757 * </script>
9759 * <!-- Will result in: -->
9760 * eminem<br>
9761 * dr. dre<br>
9762 * 50 Cent<br>
9763 */
9764 (function(dom) {
9765 function _isBlockElement(node) {
9766 return dom.getStyle("display").from(node) === "block";
9769 function _isLineBreak(node) {
9770 return node.nodeName === "BR";
9773 function _appendLineBreak(element) {
9774 var lineBreak = element.ownerDocument.createElement("br");
9775 element.appendChild(lineBreak);
9778 function resolveList(list, useLineBreaks) {
9779 if (!list.nodeName.match(/^(MENU|UL|OL)$/)) {
9780 return;
9783 var doc = list.ownerDocument,
9784 fragment = doc.createDocumentFragment(),
9785 previousSibling = wysihtml.dom.domNode(list).prev({ignoreBlankTexts: true}),
9786 nextSibling = wysihtml.dom.domNode(list).next({ignoreBlankTexts: true}),
9787 firstChild,
9788 lastChild,
9789 isLastChild,
9790 shouldAppendLineBreak,
9791 paragraph,
9792 listItem,
9793 lastListItem = list.lastElementChild || list.lastChild,
9794 isLastItem;
9796 if (useLineBreaks) {
9797 // Insert line break if list is after a non-block element
9798 if (previousSibling && !_isBlockElement(previousSibling) && !_isLineBreak(previousSibling)) {
9799 _appendLineBreak(fragment);
9802 while (listItem = (list.firstElementChild || list.firstChild)) {
9803 lastChild = listItem.lastChild;
9804 isLastItem = listItem === lastListItem;
9805 while (firstChild = listItem.firstChild) {
9806 isLastChild = firstChild === lastChild;
9807 // This needs to be done before appending it to the fragment, as it otherwise will lose style information
9808 shouldAppendLineBreak = (!isLastItem || (nextSibling && !_isBlockElement(nextSibling))) && isLastChild && !_isBlockElement(firstChild) && !_isLineBreak(firstChild);
9809 fragment.appendChild(firstChild);
9810 if (shouldAppendLineBreak) {
9811 _appendLineBreak(fragment);
9815 listItem.parentNode.removeChild(listItem);
9817 } else {
9818 while (listItem = (list.firstElementChild || list.firstChild)) {
9819 if (listItem.querySelector && listItem.querySelector("div, p, ul, ol, menu, blockquote, h1, h2, h3, h4, h5, h6")) {
9820 while (firstChild = listItem.firstChild) {
9821 fragment.appendChild(firstChild);
9823 } else {
9824 paragraph = doc.createElement("p");
9825 while (firstChild = listItem.firstChild) {
9826 paragraph.appendChild(firstChild);
9828 fragment.appendChild(paragraph);
9830 listItem.parentNode.removeChild(listItem);
9834 list.parentNode.replaceChild(fragment, list);
9837 dom.resolveList = resolveList;
9838 })(wysihtml.dom);
9840 /**
9841 * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way
9843 * Browser Compatibility:
9844 * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted"
9845 * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...)
9847 * Please note that this class can't benefit from the HTML5 sandbox attribute for the following reasons:
9848 * - sandboxing doesn't work correctly with inlined content (src="javascript:'<html>...</html>'")
9849 * - sandboxing of physical documents causes that the dom isn't accessible anymore from the outside (iframe.contentWindow, ...)
9850 * - setting the "allow-same-origin" flag would fix that, but then still javascript and dom events refuse to fire
9851 * - therefore the "allow-scripts" flag is needed, which then would deactivate any security, as the js executed inside the iframe
9852 * can do anything as if the sandbox attribute wasn't set
9854 * @param {Function} [readyCallback] Method that gets invoked when the sandbox is ready
9855 * @param {Object} [config] Optional parameters
9857 * @example
9858 * new wysihtml.dom.Sandbox(function(sandbox) {
9859 * sandbox.getWindow().document.body.innerHTML = '<img src=foo.gif onerror="alert(document.cookie)">';
9860 * });
9861 */
9862 (function(wysihtml) {
9863 var /**
9864 * Default configuration
9865 */
9866 doc = document,
9867 /**
9868 * Properties to unset/protect on the window object
9869 */
9870 windowProperties = [
9871 "parent", "top", "opener", "frameElement", "frames",
9872 "localStorage", "globalStorage", "sessionStorage", "indexedDB"
9873 ],
9874 /**
9875 * Properties on the window object which are set to an empty function
9876 */
9877 windowProperties2 = [
9878 "open", "close", "openDialog", "showModalDialog",
9879 "alert", "confirm", "prompt",
9880 "openDatabase", "postMessage",
9881 "XMLHttpRequest", "XDomainRequest"
9882 ],
9883 /**
9884 * Properties to unset/protect on the document object
9885 */
9886 documentProperties = [
9887 "referrer",
9888 "write", "open", "close"
9889 ];
9891 wysihtml.dom.Sandbox = Base.extend(
9892 /** @scope wysihtml.dom.Sandbox.prototype */ {
9894 constructor: function(readyCallback, config) {
9895 this.callback = readyCallback || wysihtml.EMPTY_FUNCTION;
9896 this.config = wysihtml.lang.object({}).merge(config).get();
9897 if (!this.config.className) {
9898 this.config.className = "wysihtml-sandbox";
9900 this.editableArea = this._createIframe();
9901 },
9903 insertInto: function(element) {
9904 if (typeof(element) === "string") {
9905 element = doc.getElementById(element);
9908 element.appendChild(this.editableArea);
9909 },
9911 getIframe: function() {
9912 return this.editableArea;
9913 },
9915 getWindow: function() {
9916 this._readyError();
9917 },
9919 getDocument: function() {
9920 this._readyError();
9921 },
9923 destroy: function() {
9924 var iframe = this.getIframe();
9925 iframe.parentNode.removeChild(iframe);
9926 },
9928 _readyError: function() {
9929 throw new Error("wysihtml.Sandbox: Sandbox iframe isn't loaded yet");
9930 },
9932 /**
9933 * Creates the sandbox iframe
9935 * Some important notes:
9936 * - We can't use HTML5 sandbox for now:
9937 * setting it causes that the iframe's dom can't be accessed from the outside
9938 * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom
9939 * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired.
9940 * In order to make this happen we need to set the "allow-scripts" flag.
9941 * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all.
9942 * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document)
9943 * - IE needs to have the security="restricted" attribute set before the iframe is
9944 * inserted into the dom tree
9945 * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even
9946 * though it supports it
9947 * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore
9948 * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely
9949 * on the onreadystatechange event
9950 */
9951 _createIframe: function() {
9952 var that = this,
9953 iframe = doc.createElement("iframe");
9954 iframe.className = this.config.className;
9955 wysihtml.dom.setAttributes({
9956 "security": "restricted",
9957 "allowtransparency": "true",
9958 "frameborder": 0,
9959 "width": 0,
9960 "height": 0,
9961 "marginwidth": 0,
9962 "marginheight": 0
9963 }).on(iframe);
9965 // Setting the src like this prevents ssl warnings in IE6
9966 if (wysihtml.browser.throwsMixedContentWarningWhenIframeSrcIsEmpty()) {
9967 iframe.src = "javascript:'<html></html>'";
9970 iframe.onload = function() {
9971 iframe.onreadystatechange = iframe.onload = null;
9972 that._onLoadIframe(iframe);
9973 };
9975 iframe.onreadystatechange = function() {
9976 if (/loaded|complete/.test(iframe.readyState)) {
9977 iframe.onreadystatechange = iframe.onload = null;
9978 that._onLoadIframe(iframe);
9980 };
9982 return iframe;
9983 },
9985 /**
9986 * Callback for when the iframe has finished loading
9987 */
9988 _onLoadIframe: function(iframe) {
9989 // don't resume when the iframe got unloaded (eg. by removing it from the dom)
9990 if (!wysihtml.dom.contains(doc.documentElement, iframe)) {
9991 return;
9994 var that = this,
9995 iframeWindow = iframe.contentWindow,
9996 iframeDocument = iframe.contentWindow.document,
9997 charset = doc.characterSet || doc.charset || "utf-8",
9998 sandboxHtml = this._getHtml({
9999 charset: charset,
10000 stylesheets: this.config.stylesheets
10001 });
10003 // Create the basic dom tree including proper DOCTYPE and charset
10004 iframeDocument.open("text/html", "replace");
10005 iframeDocument.write(sandboxHtml);
10006 iframeDocument.close();
10008 this.getWindow = function() { return iframe.contentWindow; };
10009 this.getDocument = function() { return iframe.contentWindow.document; };
10011 // Catch js errors and pass them to the parent's onerror event
10012 // addEventListener("error") doesn't work properly in some browsers
10013 // TODO: apparently this doesn't work in IE9!
10014 iframeWindow.onerror = function(errorMessage, fileName, lineNumber) {
10015 throw new Error("wysihtml.Sandbox: " + errorMessage, fileName, lineNumber);
10016 };
10018 if (!wysihtml.browser.supportsSandboxedIframes()) {
10019 // Unset a bunch of sensitive variables
10020 // Please note: This isn't hack safe!
10021 // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information
10022 // IE is secure though, which is the most important thing, since IE is the only browser, who
10023 // takes over scripts & styles into contentEditable elements when copied from external websites
10024 // or applications (Microsoft Word, ...)
10025 var i, length;
10026 for (i=0, length=windowProperties.length; i<length; i++) {
10027 this._unset(iframeWindow, windowProperties[i]);
10029 for (i=0, length=windowProperties2.length; i<length; i++) {
10030 this._unset(iframeWindow, windowProperties2[i], wysihtml.EMPTY_FUNCTION);
10032 for (i=0, length=documentProperties.length; i<length; i++) {
10033 this._unset(iframeDocument, documentProperties[i]);
10035 // This doesn't work in Safari 5
10036 // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit
10037 this._unset(iframeDocument, "cookie", "", true);
10040 if (wysihtml.polyfills) {
10041 wysihtml.polyfills(iframeWindow, iframeDocument).apply();
10044 this.loaded = true;
10046 // Trigger the callback
10047 setTimeout(function() { that.callback(that); }, 0);
10048 },
10050 _getHtml: function(templateVars) {
10051 var stylesheets = templateVars.stylesheets,
10052 html = "",
10053 i = 0,
10054 length;
10055 stylesheets = typeof(stylesheets) === "string" ? [stylesheets] : stylesheets;
10056 if (stylesheets) {
10057 length = stylesheets.length;
10058 for (; i<length; i++) {
10059 html += '<link rel="stylesheet" href="' + stylesheets[i] + '">';
10062 templateVars.stylesheets = html;
10064 return wysihtml.lang.string(
10065 '<!DOCTYPE html><html><head>'
10066 + '<meta charset="#{charset}">#{stylesheets}</head>'
10067 + '<body></body></html>'
10068 ).interpolate(templateVars);
10069 },
10071 /**
10072 * Method to unset/override existing variables
10073 * @example
10074 * // Make cookie unreadable and unwritable
10075 * this._unset(document, "cookie", "", true);
10076 */
10077 _unset: function(object, property, value, setter) {
10078 try { object[property] = value; } catch(e) {}
10080 try { object.__defineGetter__(property, function() { return value; }); } catch(e) {}
10081 if (setter) {
10082 try { object.__defineSetter__(property, function() {}); } catch(e) {}
10085 if (!wysihtml.browser.crashesWhenDefineProperty(property)) {
10086 try {
10087 var config = {
10088 get: function() { return value; }
10089 };
10090 if (setter) {
10091 config.set = function() {};
10093 Object.defineProperty(object, property, config);
10094 } catch(e) {}
10097 });
10098 })(wysihtml);
10100 (function() {
10101 var mapping = {
10102 "className": "class"
10103 };
10104 wysihtml.dom.setAttributes = function(attributes) {
10105 return {
10106 on: function(element) {
10107 for (var i in attributes) {
10108 element.setAttribute(mapping[i] || i, attributes[i]);
10111 };
10112 };
10113 })();
10115 wysihtml.dom.setStyles = function(styles) {
10116 return {
10117 on: function(element) {
10118 var style = element.style;
10119 if (typeof(styles) === "string") {
10120 style.cssText += ";" + styles;
10121 return;
10123 for (var i in styles) {
10124 if (i === "float") {
10125 style.cssFloat = styles[i];
10126 style.styleFloat = styles[i];
10127 } else {
10128 style[i] = styles[i];
10132 };
10133 };
10135 /**
10136 * Simulate HTML5 placeholder attribute
10138 * Needed since
10139 * - div[contentEditable] elements don't support it
10140 * - older browsers (such as IE8 and Firefox 3.6) don't support it at all
10142 * @param {Object} parent Instance of main wysihtml.Editor class
10143 * @param {Element} view Instance of wysihtml.views.* class
10144 * @param {String} placeholderText
10146 * @example
10147 * wysihtml.dom.simulatePlaceholder(this, composer, "Foobar");
10148 */
10149 (function(dom) {
10150 dom.simulatePlaceholder = function(editor, view, placeholderText, placeholderClassName) {
10151 var CLASS_NAME = placeholderClassName || "wysihtml-placeholder",
10152 unset = function() {
10153 var composerIsVisible = view.element.offsetWidth > 0 && view.element.offsetHeight > 0;
10154 if (view.hasPlaceholderSet()) {
10155 view.clear();
10156 view.element.focus();
10157 if (composerIsVisible ) {
10158 setTimeout(function() {
10159 var sel = view.selection.getSelection();
10160 if (!sel.focusNode || !sel.anchorNode) {
10161 view.selection.selectNode(view.element.firstChild || view.element);
10163 }, 0);
10166 view.placeholderSet = false;
10167 dom.removeClass(view.element, CLASS_NAME);
10168 },
10169 set = function() {
10170 if (view.isEmpty() && !view.placeholderSet) {
10171 view.placeholderSet = true;
10172 view.setValue(placeholderText, false);
10173 dom.addClass(view.element, CLASS_NAME);
10175 };
10177 editor
10178 .on("set_placeholder", set)
10179 .on("unset_placeholder", unset)
10180 .on("focus:composer", unset)
10181 .on("paste:composer", unset)
10182 .on("blur:composer", set);
10184 set();
10185 };
10186 })(wysihtml.dom);
10188 (function(dom) {
10189 var documentElement = document.documentElement;
10190 if ("textContent" in documentElement) {
10191 dom.setTextContent = function(element, text) {
10192 element.textContent = text;
10193 };
10195 dom.getTextContent = function(element) {
10196 return element.textContent;
10197 };
10198 } else if ("innerText" in documentElement) {
10199 dom.setTextContent = function(element, text) {
10200 element.innerText = text;
10201 };
10203 dom.getTextContent = function(element) {
10204 return element.innerText;
10205 };
10206 } else {
10207 dom.setTextContent = function(element, text) {
10208 element.nodeValue = text;
10209 };
10211 dom.getTextContent = function(element) {
10212 return element.nodeValue;
10213 };
10215 })(wysihtml.dom);
10217 /* Unwraps element and returns list of childNodes that the node contained.
10219 * Example:
10220 * var childnodes = wysihtml.dom.unwrap(document.querySelector('.unwrap-me'));
10221 */
10223 wysihtml.dom.unwrap = function(node) {
10224 var children = [];
10225 if (node.parentNode) {
10226 while (node.lastChild) {
10227 children.unshift(node.lastChild);
10228 wysihtml.dom.insert(node.lastChild).after(node);
10230 node.parentNode.removeChild(node);
10232 return children;
10233 };
10235 /**
10236 * Fix most common html formatting misbehaviors of browsers implementation when inserting
10237 * content via copy & paste contentEditable
10239 * @author Christopher Blum
10240 */
10241 wysihtml.quirks.cleanPastedHTML = (function() {
10243 var styleToRegex = function (styleStr) {
10244 var trimmedStr = wysihtml.lang.string(styleStr).trim(),
10245 escapedStr = trimmedStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
10247 return new RegExp("^((?!^" + escapedStr + "$).)*$", "i");
10248 };
10250 var extendRulesWithStyleExceptions = function (rules, exceptStyles) {
10251 var newRules = wysihtml.lang.object(rules).clone(true),
10252 tag, style;
10254 for (tag in newRules.tags) {
10256 if (newRules.tags.hasOwnProperty(tag)) {
10257 if (newRules.tags[tag].keep_styles) {
10258 for (style in newRules.tags[tag].keep_styles) {
10259 if (newRules.tags[tag].keep_styles.hasOwnProperty(style)) {
10260 if (exceptStyles[style]) {
10261 newRules.tags[tag].keep_styles[style] = styleToRegex(exceptStyles[style]);
10269 return newRules;
10270 };
10272 var pickRuleset = function(ruleset, html) {
10273 var pickedSet, defaultSet;
10275 if (!ruleset) {
10276 return null;
10279 for (var i = 0, max = ruleset.length; i < max; i++) {
10280 if (!ruleset[i].condition) {
10281 defaultSet = ruleset[i].set;
10283 if (ruleset[i].condition && ruleset[i].condition.test(html)) {
10284 return ruleset[i].set;
10288 return defaultSet;
10289 };
10291 return function(html, options) {
10292 var exceptStyles = {
10293 'color': wysihtml.dom.getStyle("color").from(options.referenceNode),
10294 'fontSize': wysihtml.dom.getStyle("font-size").from(options.referenceNode)
10295 },
10296 rules = extendRulesWithStyleExceptions(pickRuleset(options.rules, html) || {}, exceptStyles),
10297 newHtml;
10299 newHtml = wysihtml.dom.parse(html, {
10300 "rules": rules,
10301 "cleanUp": true, // <span> elements, empty or without attributes, should be removed/replaced with their content
10302 "context": options.referenceNode.ownerDocument,
10303 "uneditableClass": options.uneditableClass,
10304 "clearInternals" : true, // don't paste temprorary selection and other markings
10305 "unjoinNbsps" : true
10306 });
10308 return newHtml;
10309 };
10311 })();
10313 /**
10314 * IE and Opera leave an empty paragraph in the contentEditable element after clearing it
10316 * @param {Object} contentEditableElement The contentEditable element to observe for clearing events
10317 * @exaple
10318 * wysihtml.quirks.ensureProperClearing(myContentEditableElement);
10319 */
10320 wysihtml.quirks.ensureProperClearing = (function() {
10321 var clearIfNecessary = function() {
10322 var element = this;
10323 setTimeout(function() {
10324 var innerHTML = element.innerHTML.toLowerCase();
10325 if (innerHTML == "<p>&nbsp;</p>" ||
10326 innerHTML == "<p>&nbsp;</p><p>&nbsp;</p>") {
10327 element.innerHTML = "";
10329 }, 0);
10330 };
10332 return function(composer) {
10333 wysihtml.dom.observe(composer.element, ["cut", "keydown"], clearIfNecessary);
10334 };
10335 })();
10337 // See https://bugzilla.mozilla.org/show_bug.cgi?id=664398
10338 //
10339 // In Firefox this:
10340 // var d = document.createElement("div");
10341 // d.innerHTML ='<a href="~"></a>';
10342 // d.innerHTML;
10343 // will result in:
10344 // <a href="%7E"></a>
10345 // which is wrong
10346 (function(wysihtml) {
10347 var TILDE_ESCAPED = "%7E";
10348 wysihtml.quirks.getCorrectInnerHTML = function(element) {
10349 var innerHTML = element.innerHTML;
10350 if (innerHTML.indexOf(TILDE_ESCAPED) === -1) {
10351 return innerHTML;
10354 var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"),
10355 url,
10356 urlToSearch,
10357 length,
10358 i;
10359 for (i=0, length=elementsWithTilde.length; i<length; i++) {
10360 url = elementsWithTilde[i].href || elementsWithTilde[i].src;
10361 urlToSearch = wysihtml.lang.string(url).replace("~").by(TILDE_ESCAPED);
10362 innerHTML = wysihtml.lang.string(innerHTML).replace(urlToSearch).by(url);
10364 return innerHTML;
10365 };
10366 })(wysihtml);
10368 /**
10369 * Force rerendering of a given element
10370 * Needed to fix display misbehaviors of IE
10372 * @param {Element} element The element object which needs to be rerendered
10373 * @example
10374 * wysihtml.quirks.redraw(document.body);
10375 */
10376 (function(wysihtml) {
10377 var CLASS_NAME = "wysihtml-quirks-redraw";
10379 wysihtml.quirks.redraw = function(element) {
10380 wysihtml.dom.addClass(element, CLASS_NAME);
10381 wysihtml.dom.removeClass(element, CLASS_NAME);
10383 // Following hack is needed for firefox to make sure that image resize handles are properly removed
10384 try {
10385 var doc = element.ownerDocument;
10386 doc.execCommand("italic", false, null);
10387 doc.execCommand("italic", false, null);
10388 } catch(e) {}
10389 };
10390 })(wysihtml);
10392 (function(wysihtml) {
10394 // List of supported color format parsing methods
10395 // If radix is not defined 10 is expected as default
10396 var colorParseMethods = {
10397 rgba : {
10398 regex: /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d\.]+)\s*\)/i,
10399 name: "rgba"
10400 },
10401 rgb : {
10402 regex: /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i,
10403 name: "rgb"
10404 },
10405 hex6 : {
10406 regex: /^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])/i,
10407 name: "hex",
10408 radix: 16
10409 },
10410 hex3 : {
10411 regex: /^#([0-9a-f])([0-9a-f])([0-9a-f])/i,
10412 name: "hex",
10413 radix: 16
10415 },
10416 // Takes a style key name as an argument and makes a regex that can be used to the match key:value pair from style string
10417 makeParamRegExp = function (p) {
10418 return new RegExp("(^|\\s|;)" + p + "\\s*:\\s*[^;$]+", "gi");
10419 };
10421 // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns suitable parsing method for it
10422 function getColorParseMethod (colorStr) {
10423 var prop, colorTypeConf;
10425 for (prop in colorParseMethods) {
10426 if (!colorParseMethods.hasOwnProperty(prop)) { continue; }
10428 colorTypeConf = colorParseMethods[prop];
10430 if (colorTypeConf.regex.test(colorStr)) {
10431 return colorTypeConf;
10436 // Takes color string value ("#abc", "rgb(1,2,3)", ...) as an argument and returns the type of that color format "hex", "rgb", "rgba".
10437 function getColorFormat (colorStr) {
10438 var type = getColorParseMethod(colorStr);
10440 return type ? type.name : undefined;
10443 // Public API functions for styleParser
10444 wysihtml.quirks.styleParser = {
10446 // Takes color string value as an argument and returns suitable parsing method for it
10447 getColorParseMethod : getColorParseMethod,
10449 // Takes color string value as an argument and returns the type of that color format "hex", "rgb", "rgba".
10450 getColorFormat : getColorFormat,
10452 /* Parses a color string to and array of [red, green, blue, alpha].
10453 * paramName: optional argument to parse color value directly from style string parameter
10455 * Examples:
10456 * var colorArray = wysihtml.quirks.styleParser.parseColor("#ABC"); // [170, 187, 204, 1]
10457 * var colorArray = wysihtml.quirks.styleParser.parseColor("#AABBCC"); // [170, 187, 204, 1]
10458 * var colorArray = wysihtml.quirks.styleParser.parseColor("rgb(1,2,3)"); // [1, 2, 3, 1]
10459 * var colorArray = wysihtml.quirks.styleParser.parseColor("rgba(1,2,3,0.5)"); // [1, 2, 3, 0.5]
10461 * var colorArray = wysihtml.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "background-color"); // [170, 187, 204, 1]
10462 * var colorArray = wysihtml.quirks.styleParser.parseColor("background-color: #ABC; color: #000;", "color"); // [0, 0, 0, 1]
10463 */
10464 parseColor : function (stylesStr, paramName) {
10465 var paramsRegex, params, colorType, colorMatch, radix,
10466 colorStr = stylesStr;
10468 if (paramName) {
10469 paramsRegex = makeParamRegExp(paramName);
10471 if (!(params = stylesStr.match(paramsRegex))) { return false; }
10473 params = params.pop().split(":")[1];
10474 colorStr = wysihtml.lang.string(params).trim();
10477 if (!(colorType = getColorParseMethod(colorStr))) { return false; }
10478 if (!(colorMatch = colorStr.match(colorType.regex))) { return false; }
10480 radix = colorType.radix || 10;
10482 if (colorType === colorParseMethods.hex3) {
10483 colorMatch.shift();
10484 colorMatch.push(1);
10485 return wysihtml.lang.array(colorMatch).map(function(d, idx) {
10486 return (idx < 3) ? (parseInt(d, radix) * radix) + parseInt(d, radix): parseFloat(d);
10487 });
10490 colorMatch.shift();
10492 if (!colorMatch[3]) {
10493 colorMatch.push(1);
10496 return wysihtml.lang.array(colorMatch).map(function(d, idx) {
10497 return (idx < 3) ? parseInt(d, radix): parseFloat(d);
10498 });
10499 },
10501 /* Takes rgba color array [r,g,b,a] as a value and formats it to color string with given format type
10502 * If no format is given, rgba/rgb is returned based on alpha value
10504 * Example:
10505 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "hash"); // "#AABBCC"
10506 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "hex"); // "AABBCC"
10507 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "csv"); // "170, 187, 204, 1"
10508 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgba"); // "rgba(170,187,204,1)"
10509 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1], "rgb"); // "rgb(170,187,204)"
10511 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 0.5]); // "rgba(170,187,204,0.5)"
10512 * var colorStr = wysihtml.quirks.styleParser.unparseColor([170, 187, 204, 1]); // "rgb(170,187,204)"
10513 */
10514 unparseColor: function(val, colorFormat) {
10515 var hexRadix = 16;
10517 if (colorFormat === "hex") {
10518 return (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase();
10519 } else if (colorFormat === "hash") {
10520 return "#" + (val[0].toString(hexRadix) + val[1].toString(hexRadix) + val[2].toString(hexRadix)).toUpperCase();
10521 } else if (colorFormat === "rgb") {
10522 return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
10523 } else if (colorFormat === "rgba") {
10524 return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
10525 } else if (colorFormat === "csv") {
10526 return val[0] + "," + val[1] + "," + val[2] + "," + val[3];
10529 if (val[3] && val[3] !== 1) {
10530 return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")";
10531 } else {
10532 return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")";
10534 },
10536 // Parses font size value from style string
10537 parseFontSize: function(stylesStr) {
10538 var params = stylesStr.match(makeParamRegExp("font-size"));
10539 if (params) {
10540 return wysihtml.lang.string(params[params.length - 1].split(":")[1]).trim();
10542 return false;
10544 };
10546 })(wysihtml);
10548 /**
10549 * Selection API
10551 * @example
10552 * var selection = new wysihtml.Selection(editor);
10553 */
10554 (function(wysihtml) {
10555 var dom = wysihtml.dom;
10557 function _getCumulativeOffsetTop(element) {
10558 var top = 0;
10559 if (element.parentNode) {
10560 do {
10561 top += element.offsetTop || 0;
10562 element = element.offsetParent;
10563 } while (element);
10565 return top;
10568 // Provides the depth of ``descendant`` relative to ``ancestor``
10569 function getDepth(ancestor, descendant) {
10570 var ret = 0;
10571 while (descendant !== ancestor) {
10572 ret++;
10573 descendant = descendant.parentNode;
10574 if (!descendant)
10575 throw new Error("not a descendant of ancestor!");
10577 return ret;
10580 function getRangeNode(node, offset) {
10581 if (node.nodeType === 3) {
10582 return node;
10583 } else {
10584 return node.childNodes[offset] || node;
10588 function getWebkitSelectionFixNode(container) {
10589 var blankNode = document.createElement('span');
10591 var placeholderRemover = function(event) {
10592 // Self-destructs the caret and keeps the text inserted into it by user
10593 var lastChild;
10595 container.removeEventListener('mouseup', placeholderRemover);
10596 container.removeEventListener('keydown', placeholderRemover);
10597 container.removeEventListener('touchstart', placeholderRemover);
10598 container.removeEventListener('focus', placeholderRemover);
10599 container.removeEventListener('blur', placeholderRemover);
10600 container.removeEventListener('paste', delayedPlaceholderRemover);
10601 container.removeEventListener('drop', delayedPlaceholderRemover);
10602 container.removeEventListener('beforepaste', delayedPlaceholderRemover);
10604 if (blankNode && blankNode.parentNode) {
10605 blankNode.parentNode.removeChild(blankNode);
10607 },
10608 delayedPlaceholderRemover = function (event) {
10609 if (blankNode && blankNode.parentNode) {
10610 setTimeout(placeholderRemover, 0);
10612 };
10614 blankNode.appendChild(container.ownerDocument.createTextNode(wysihtml.INVISIBLE_SPACE));
10615 blankNode.className = '_wysihtml-temp-caret-fix';
10616 blankNode.style.display = 'block';
10617 blankNode.style.minWidth = '1px';
10618 blankNode.style.height = '0px';
10620 container.addEventListener('mouseup', placeholderRemover);
10621 container.addEventListener('keydown', placeholderRemover);
10622 container.addEventListener('touchstart', placeholderRemover);
10623 container.addEventListener('focus', placeholderRemover);
10624 container.addEventListener('blur', placeholderRemover);
10625 container.addEventListener('paste', delayedPlaceholderRemover);
10626 container.addEventListener('drop', delayedPlaceholderRemover);
10627 container.addEventListener('beforepaste', delayedPlaceholderRemover);
10629 return blankNode;
10632 // Should fix the obtained ranges that cannot surrond contents normally to apply changes upon
10633 // Being considerate to firefox that sets range start start out of span and end inside on doubleclick initiated selection
10634 function expandRangeToSurround(range) {
10635 if (range.canSurroundContents()) return;
10637 var common = range.commonAncestorContainer,
10638 start_depth = getDepth(common, range.startContainer),
10639 end_depth = getDepth(common, range.endContainer);
10641 while(!range.canSurroundContents()) {
10642 // 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.
10643 if (start_depth > end_depth) {
10644 range.setStartBefore(range.startContainer);
10645 start_depth = getDepth(common, range.startContainer);
10647 else {
10648 range.setEndAfter(range.endContainer);
10649 end_depth = getDepth(common, range.endContainer);
10654 wysihtml.Selection = Base.extend(
10655 /** @scope wysihtml.Selection.prototype */ {
10656 constructor: function(editor, contain, unselectableClass) {
10657 // Make sure that our external range library is initialized
10658 rangy.init();
10660 this.editor = editor;
10661 this.composer = editor.composer;
10662 this.doc = this.composer.doc;
10663 this.win = this.composer.win;
10664 this.contain = contain;
10665 this.unselectableClass = unselectableClass || false;
10666 },
10668 /**
10669 * Get the current selection as a bookmark to be able to later restore it
10671 * @return {Object} An object that represents the current selection
10672 */
10673 getBookmark: function() {
10674 var range = this.getRange();
10675 return range && range.cloneRange();
10676 },
10678 /**
10679 * Restore a selection retrieved via wysihtml.Selection.prototype.getBookmark
10681 * @param {Object} bookmark An object that represents the current selection
10682 */
10683 setBookmark: function(bookmark) {
10684 if (!bookmark) {
10685 return;
10688 this.setSelection(bookmark);
10689 },
10691 /**
10692 * Set the caret in front of the given node
10694 * @param {Object} node The element or text node where to position the caret in front of
10695 * @example
10696 * selection.setBefore(myElement);
10697 */
10698 setBefore: function(node) {
10699 var range = rangy.createRange(this.doc);
10700 range.setStartBefore(node);
10701 range.setEndBefore(node);
10702 return this.setSelection(range);
10703 },
10705 // Constructs a self removing whitespace (ain absolute positioned span) for placing selection caret when normal methods fail.
10706 // Webkit has an issue with placing caret into places where there are no textnodes near by.
10707 createTemporaryCaretSpaceAfter: function (node) {
10708 var caretPlaceholder = this.doc.createElement('span'),
10709 caretPlaceholderText = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE),
10710 placeholderRemover = (function(event) {
10711 // Self-destructs the caret and keeps the text inserted into it by user
10712 var lastChild;
10714 this.contain.removeEventListener('mouseup', placeholderRemover);
10715 this.contain.removeEventListener('keydown', keyDownHandler);
10716 this.contain.removeEventListener('touchstart', placeholderRemover);
10717 this.contain.removeEventListener('focus', placeholderRemover);
10718 this.contain.removeEventListener('blur', placeholderRemover);
10719 this.contain.removeEventListener('paste', delayedPlaceholderRemover);
10720 this.contain.removeEventListener('drop', delayedPlaceholderRemover);
10721 this.contain.removeEventListener('beforepaste', delayedPlaceholderRemover);
10723 // If user inserted sth it is in the placeholder and sgould be unwrapped and stripped of invisible whitespace hack
10724 // Otherwise the wrapper can just be removed
10725 if (caretPlaceholder && caretPlaceholder.parentNode) {
10726 caretPlaceholder.innerHTML = caretPlaceholder.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
10727 if ((/[^\s]+/).test(caretPlaceholder.innerHTML)) {
10728 lastChild = caretPlaceholder.lastChild;
10729 wysihtml.dom.unwrap(caretPlaceholder);
10730 this.setAfter(lastChild);
10731 } else {
10732 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
10736 }).bind(this),
10737 delayedPlaceholderRemover = function (event) {
10738 if (caretPlaceholder && caretPlaceholder.parentNode) {
10739 setTimeout(placeholderRemover, 0);
10741 },
10742 keyDownHandler = function(event) {
10743 if (event.which !== 8 && event.which !== 91 && event.which !== 17 && (event.which !== 86 || (!event.ctrlKey && !event.metaKey))) {
10744 placeholderRemover();
10746 };
10748 caretPlaceholder.className = '_wysihtml-temp-caret-fix';
10749 caretPlaceholder.style.position = 'absolute';
10750 caretPlaceholder.style.display = 'block';
10751 caretPlaceholder.style.minWidth = '1px';
10752 caretPlaceholder.style.zIndex = '99999';
10753 caretPlaceholder.appendChild(caretPlaceholderText);
10755 node.parentNode.insertBefore(caretPlaceholder, node.nextSibling);
10756 this.setBefore(caretPlaceholderText);
10758 // Remove the caret fix on any of the following events (some are delayed as content change happens after event)
10759 this.contain.addEventListener('mouseup', placeholderRemover);
10760 this.contain.addEventListener('keydown', keyDownHandler);
10761 this.contain.addEventListener('touchstart', placeholderRemover);
10762 this.contain.addEventListener('focus', placeholderRemover);
10763 this.contain.addEventListener('blur', placeholderRemover);
10764 this.contain.addEventListener('paste', delayedPlaceholderRemover);
10765 this.contain.addEventListener('drop', delayedPlaceholderRemover);
10766 this.contain.addEventListener('beforepaste', delayedPlaceholderRemover);
10768 return caretPlaceholder;
10769 },
10771 /**
10772 * Set the caret after the given node
10774 * @param {Object} node The element or text node where to position the caret in front of
10775 * @example
10776 * selection.setBefore(myElement);
10777 * callback is an optional parameter accepting a function to execute when selection ahs been set
10778 */
10779 setAfter: function(node, notVisual, callback) {
10780 var win = this.win,
10781 range = rangy.createRange(this.doc),
10782 fixWebkitSelection = function() {
10783 // Webkit fails to add selection if there are no textnodes in that region
10784 // (like an uneditable container at the end of content).
10785 var parent = node.parentNode,
10786 lastSibling = parent ? parent.childNodes[parent.childNodes.length - 1] : null;
10788 if (!sel || (lastSibling === node && node.nodeType === 1 && win.getComputedStyle(node).display === "block")) {
10789 if (notVisual) {
10790 // If setAfter is used as internal between actions, self-removing caretPlaceholder has simpler implementation
10791 // and remove itself in call stack end instead on user interaction
10792 var caretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
10793 node.parentNode.insertBefore(caretPlaceholder, node.nextSibling);
10794 this.selectNode(caretPlaceholder);
10795 setTimeout(function() {
10796 if (caretPlaceholder && caretPlaceholder.parentNode) {
10797 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
10799 }, 0);
10800 } else {
10801 this.createTemporaryCaretSpaceAfter(node);
10804 }.bind(this),
10805 sel;
10807 range.setStartAfter(node);
10808 range.setEndAfter(node);
10810 // In IE contenteditable must be focused before we can set selection
10811 // thus setting the focus if activeElement is not this composer
10812 if (!document.activeElement || document.activeElement !== this.composer.element) {
10813 var scrollPos = this.composer.getScrollPos();
10814 this.composer.element.focus();
10815 this.composer.setScrollPos(scrollPos);
10816 setTimeout(function() {
10817 sel = this.setSelection(range);
10818 fixWebkitSelection();
10819 if (callback) {
10820 callback(sel);
10822 }.bind(this), 0);
10823 } else {
10824 sel = this.setSelection(range);
10825 fixWebkitSelection();
10826 if (callback) {
10827 callback(sel);
10830 },
10832 /**
10833 * Ability to select/mark nodes
10835 * @param {Element} node The node/element to select
10836 * @example
10837 * selection.selectNode(document.getElementById("my-image"));
10838 */
10839 selectNode: function(node, avoidInvisibleSpace) {
10840 var range = rangy.createRange(this.doc),
10841 isElement = node.nodeType === wysihtml.ELEMENT_NODE,
10842 canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"),
10843 content = isElement ? node.innerHTML : node.data,
10844 isEmpty = (content === "" || content === wysihtml.INVISIBLE_SPACE),
10845 displayStyle = dom.getStyle("display").from(node),
10846 isBlockElement = (displayStyle === "block" || displayStyle === "list-item");
10848 if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) {
10849 // Make sure that caret is visible in node by inserting a zero width no breaking space
10850 try { node.innerHTML = wysihtml.INVISIBLE_SPACE; } catch(e) {}
10852 if (canHaveHTML) {
10853 range.selectNodeContents(node);
10854 } else {
10855 range.selectNode(node);
10858 if (canHaveHTML && isEmpty && isElement) {
10859 range.collapse(isBlockElement);
10860 } else if (canHaveHTML && isEmpty) {
10861 range.setStartAfter(node);
10862 range.setEndAfter(node);
10865 this.setSelection(range);
10866 },
10868 /**
10869 * Get the node which contains the selection
10871 * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange"
10872 * @return {Object} The node that contains the caret
10873 * @example
10874 * var nodeThatContainsCaret = selection.getSelectedNode();
10875 */
10876 getSelectedNode: function(controlRange) {
10877 var selection,
10878 range;
10880 if (controlRange && this.doc.selection && this.doc.selection.type === "Control") {
10881 range = this.doc.selection.createRange();
10882 if (range && range.length) {
10883 return range.item(0);
10887 selection = this.getSelection(this.doc);
10888 if (selection.focusNode === selection.anchorNode) {
10889 return selection.focusNode;
10890 } else {
10891 range = this.getRange(this.doc);
10892 return range ? range.commonAncestorContainer : this.doc.body;
10894 },
10896 fixSelBorders: function() {
10897 var range = this.getRange();
10898 expandRangeToSurround(range);
10899 this.setSelection(range);
10900 },
10902 getSelectedOwnNodes: function(controlRange) {
10903 var selection,
10904 ranges = this.getOwnRanges(),
10905 ownNodes = [];
10907 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
10908 ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body);
10910 return ownNodes;
10911 },
10913 findNodesInSelection: function(nodeTypes) {
10914 var ranges = this.getOwnRanges(),
10915 nodes = [], curNodes;
10916 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
10917 curNodes = ranges[i].getNodes([1], function(node) {
10918 return wysihtml.lang.array(nodeTypes).contains(node.nodeName);
10919 });
10920 nodes = nodes.concat(curNodes);
10922 return nodes;
10923 },
10925 filterElements: function(filter) {
10926 var ranges = this.getOwnRanges(),
10927 nodes = [], curNodes;
10929 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
10930 curNodes = ranges[i].getNodes([1], function(element){
10931 return filter(element, ranges[i]);
10932 });
10933 nodes = nodes.concat(curNodes);
10935 return nodes;
10936 },
10938 containsUneditable: function() {
10939 var uneditables = this.getOwnUneditables(),
10940 selection = this.getSelection();
10942 for (var i = 0, maxi = uneditables.length; i < maxi; i++) {
10943 if (selection.containsNode(uneditables[i])) {
10944 return true;
10948 return false;
10949 },
10951 // Deletes selection contents making sure uneditables/unselectables are not partially deleted
10952 // Triggers wysihtml:uneditable:delete custom event on all deleted uneditables if customevents suppoorted
10953 deleteContents: function() {
10954 var range = this.getRange();
10955 this.deleteRangeContents(range);
10956 this.setSelection(range);
10957 },
10959 // Makes sure all uneditable sare notified before deleting contents
10960 deleteRangeContents: function (range) {
10961 var startParent, endParent, uneditables, ev;
10963 if (this.unselectableClass) {
10964 if ((startParent = wysihtml.dom.getParentElement(range.startContainer, { query: "." + this.unselectableClass }, false, this.contain))) {
10965 range.setStartBefore(startParent);
10967 if ((endParent = wysihtml.dom.getParentElement(range.endContainer, { query: "." + this.unselectableClass }, false, this.contain))) {
10968 range.setEndAfter(endParent);
10971 // If customevents present notify uneditable elements of being deleted
10972 uneditables = range.getNodes([1], (function (node) {
10973 return wysihtml.dom.hasClass(node, this.unselectableClass);
10974 }).bind(this));
10975 for (var i = uneditables.length; i--;) {
10976 try {
10977 ev = new CustomEvent("wysihtml:uneditable:delete");
10978 uneditables[i].dispatchEvent(ev);
10979 } catch (err) {}
10982 range.deleteContents();
10983 },
10985 getCaretNode: function () {
10986 var selection = this.getSelection();
10987 return (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null;
10988 },
10990 getPreviousNode: function(node, ignoreEmpty) {
10991 var displayStyle;
10992 if (!node) {
10993 var selection = this.getSelection();
10994 node = (selection && selection.anchorNode) ? getRangeNode(selection.anchorNode, selection.anchorOffset) : null;
10997 if (node === this.contain) {
10998 return false;
11001 var ret = node.previousSibling,
11002 parent;
11004 if (ret === this.contain) {
11005 return false;
11008 if (ret && ret.nodeType !== 3 && ret.nodeType !== 1) {
11009 // do not count comments and other node types
11010 ret = this.getPreviousNode(ret, ignoreEmpty);
11011 } else if (ret && ret.nodeType === 3 && (/^\s*$/).test(ret.textContent)) {
11012 // do not count empty textnodes as previous nodes
11013 ret = this.getPreviousNode(ret, ignoreEmpty);
11014 } else if (ignoreEmpty && ret && ret.nodeType === 1) {
11015 // Do not count empty nodes if param set.
11016 // Contenteditable tends to bypass and delete these silently when deleting with caret when element is inline-like
11017 displayStyle = wysihtml.dom.getStyle("display").from(ret);
11018 if (
11019 !wysihtml.lang.array(["BR", "HR", "IMG"]).contains(ret.nodeName) &&
11020 !wysihtml.lang.array(["block", "inline-block", "flex", "list-item", "table"]).contains(displayStyle) &&
11021 (/^[\s]*$/).test(ret.innerHTML)
11022 ) {
11023 ret = this.getPreviousNode(ret, ignoreEmpty);
11025 } else if (!ret && node !== this.contain) {
11026 parent = node.parentNode;
11027 if (parent !== this.contain) {
11028 ret = this.getPreviousNode(parent, ignoreEmpty);
11032 return (ret !== this.contain) ? ret : false;
11033 },
11035 // Gather info about caret location (caret node, previous and next node)
11036 getNodesNearCaret: function() {
11037 if (!this.isCollapsed()) {
11038 throw "Selection must be caret when using selection.getNodesNearCaret()";
11041 var r = this.getOwnRanges(),
11042 caretNode, prevNode, nextNode, offset;
11044 if (r && r.length > 0) {
11045 if (r[0].startContainer.nodeType === 1) {
11046 caretNode = r[0].startContainer.childNodes[r[0].startOffset - 1];
11047 if (!caretNode && r[0].startOffset === 0) {
11048 // Is first position before all nodes
11049 nextNode = r[0].startContainer.childNodes[0];
11050 } else if (caretNode) {
11051 prevNode = caretNode.previousSibling;
11052 nextNode = caretNode.nextSibling;
11054 } else {
11055 if (r[0].startOffset === 0 && r[0].startContainer.previousSibling) {
11056 caretNode = r[0].startContainer.previousSibling;
11057 if (caretNode.nodeType === 3) {
11058 offset = caretNode.data.length;
11060 } else {
11061 caretNode = r[0].startContainer;
11062 offset = r[0].startOffset;
11064 prevNode = caretNode.previousSibling;
11065 nextNode = caretNode.nextSibling;
11068 return {
11069 "caretNode": caretNode,
11070 "prevNode": prevNode,
11071 "nextNode": nextNode,
11072 "textOffset": offset
11073 };
11076 return null;
11077 },
11079 getSelectionParentsByTag: function(tagName) {
11080 var nodes = this.getSelectedOwnNodes(),
11081 curEl, parents = [];
11083 for (var i = 0, maxi = nodes.length; i < maxi; i++) {
11084 curEl = (nodes[i].nodeName && nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml.dom.getParentElement(nodes[i], { query: 'li'}, false, this.contain);
11085 if (curEl) {
11086 parents.push(curEl);
11089 return (parents.length) ? parents : null;
11090 },
11092 getRangeToNodeEnd: function() {
11093 if (this.isCollapsed()) {
11094 var range = this.getRange(),
11095 sNode, pos, lastR;
11096 if (range) {
11097 sNode = range.startContainer;
11098 pos = range.startOffset;
11099 lastR = rangy.createRange(this.doc);
11101 lastR.selectNodeContents(sNode);
11102 lastR.setStart(sNode, pos);
11103 return lastR;
11106 },
11108 getRangeToNodeBeginning: function() {
11109 if (this.isCollapsed()) {
11110 var range = this.getRange(),
11111 sNode = range.startContainer,
11112 pos = range.startOffset,
11113 lastR = rangy.createRange(this.doc);
11115 lastR.selectNodeContents(sNode);
11116 lastR.setEnd(sNode, pos);
11117 return lastR;
11119 },
11121 // This function returns if caret is last in a node (no textual visible content follows)
11122 caretIsInTheEndOfNode: function(ignoreIfSpaceIsBeforeCaret) {
11123 var r = rangy.createRange(this.doc),
11124 s = this.getSelection(),
11125 rangeToNodeEnd = this.getRangeToNodeEnd(),
11126 endc, endtxt, beginc, begintxt;
11128 if (rangeToNodeEnd) {
11129 endc = rangeToNodeEnd.cloneContents();
11130 endtxt = endc.textContent;
11132 if ((/^\s*$/).test(endtxt)) {
11133 if (ignoreIfSpaceIsBeforeCaret) {
11134 beginc = this.getRangeToNodeBeginning().cloneContents();
11135 begintxt = beginc.textContent;
11136 return !(/[\u00A0 ][\s\uFEFF]*$/).test(begintxt);
11137 } else {
11138 return true;
11140 } else {
11141 return false;
11143 } else {
11144 return false;
11146 },
11148 caretIsFirstInSelection: function(includeLineBreaks) {
11149 var r = rangy.createRange(this.doc),
11150 s = this.getSelection(),
11151 range = this.getRange(),
11152 startNode = getRangeNode(range.startContainer, range.startOffset);
11154 if (startNode) {
11155 if (startNode.nodeType === wysihtml.TEXT_NODE) {
11156 if (!startNode.parentNode) {
11157 return false;
11159 if (!this.isCollapsed() || (startNode.parentNode.firstChild !== startNode && !wysihtml.dom.domNode(startNode.previousSibling).is.block())) {
11160 return false;
11162 var ws = this.win.getComputedStyle(startNode.parentNode).whiteSpace;
11163 return (ws === "pre" || ws === "pre-wrap") ? range.startOffset === 0 : (/^\s*$/).test(startNode.data.substr(0,range.startOffset));
11164 } else if (includeLineBreaks && wysihtml.dom.domNode(startNode).is.lineBreak()) {
11165 return true;
11166 } else {
11167 r.selectNodeContents(this.getRange().commonAncestorContainer);
11168 r.collapse(true);
11169 return (this.isCollapsed() && (r.startContainer === s.anchorNode || r.endContainer === s.anchorNode) && r.startOffset === s.anchorOffset);
11172 },
11174 caretIsInTheBeginnig: function(ofNode) {
11175 var selection = this.getSelection(),
11176 node = selection.anchorNode,
11177 offset = selection.anchorOffset;
11178 if (ofNode && node) {
11179 return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml.dom.getParentElement(node.parentNode, { query: ofNode }, 1)));
11180 } else if (node) {
11181 return (offset === 0 && !this.getPreviousNode(node, true));
11183 },
11185 // Returns object describing node/text before selection
11186 // If includePrevLeaves is true returns also previous last leaf child if selection is in the beginning of current node
11187 getBeforeSelection: function(includePrevLeaves) {
11188 var sel = this.getSelection(),
11189 startNode = (sel.isBackwards()) ? sel.focusNode : sel.anchorNode,
11190 startOffset = (sel.isBackwards()) ? sel.focusOffset : sel.anchorOffset,
11191 rng = this.createRange(), endNode, inTmpCaret;
11193 // If start is textnode and all is whitespace before caret. Set start offset to 0
11194 if (startNode && startNode.nodeType === 3 && (/^\s*$/).test(startNode.data.slice(0, startOffset))) {
11195 startOffset = 0;
11198 // Escape temproray helper nodes if selection in them
11199 inTmpCaret = wysihtml.dom.getParentElement(startNode, { query: '._wysihtml-temp-caret-fix' }, 1);
11200 if (inTmpCaret) {
11201 startNode = inTmpCaret.parentNode;
11202 startOffset = Array.prototype.indexOf.call(startNode.childNodes, inTmpCaret);
11205 if (startNode) {
11206 if (startOffset > 0) {
11207 if (startNode.nodeType === 3) {
11208 rng.setStart(startNode, 0);
11209 rng.setEnd(startNode, startOffset);
11210 return {
11211 type: "text",
11212 range: rng,
11213 offset : startOffset,
11214 node: startNode
11215 };
11216 } else {
11217 rng.setStartBefore(startNode.childNodes[0]);
11218 endNode = startNode.childNodes[startOffset - 1];
11219 rng.setEndAfter(endNode);
11220 return {
11221 type: "element",
11222 range: rng,
11223 offset : startOffset,
11224 node: endNode
11225 };
11227 } else {
11228 rng.setStartAndEnd(startNode, 0);
11230 if (includePrevLeaves) {
11231 var prevNode = this.getPreviousNode(startNode, true),
11232 prevLeaf = null;
11234 if(prevNode) {
11235 if (prevNode.nodeType === 1 && wysihtml.dom.hasClass(prevNode, this.unselectableClass)) {
11236 prevLeaf = prevNode;
11237 } else {
11238 prevLeaf = wysihtml.dom.domNode(prevNode).lastLeafNode();
11242 if (prevLeaf) {
11243 return {
11244 type: "leafnode",
11245 range: rng,
11246 offset : startOffset,
11247 node: prevLeaf
11248 };
11252 return {
11253 type: "none",
11254 range: rng,
11255 offset : startOffset,
11256 node: startNode
11257 };
11260 return null;
11261 },
11263 // TODO: Figure out a method from following 2 that would work universally
11264 executeAndRestoreRangy: function(method, restoreScrollPosition) {
11265 var sel = rangy.saveSelection(this.win);
11266 if (!sel) {
11267 method();
11268 } else {
11269 try {
11270 method();
11271 } catch(e) {
11272 setTimeout(function() { throw e; }, 0);
11275 rangy.restoreSelection(sel);
11276 },
11278 // TODO: has problems in chrome 12. investigate block level and uneditable area inbetween
11279 executeAndRestore: function(method, restoreScrollPosition) {
11280 var body = this.doc.body,
11281 oldScrollTop = restoreScrollPosition && body.scrollTop,
11282 oldScrollLeft = restoreScrollPosition && body.scrollLeft,
11283 className = "_wysihtml-temp-placeholder",
11284 placeholderHtml = '<span class="' + className + '">' + wysihtml.INVISIBLE_SPACE + '</span>',
11285 range = this.getRange(true),
11286 caretPlaceholder,
11287 newCaretPlaceholder,
11288 nextSibling, prevSibling,
11289 node, node2, range2,
11290 newRange;
11292 // Nothing selected, execute and say goodbye
11293 if (!range) {
11294 method(body, body);
11295 return;
11298 if (!range.collapsed) {
11299 range2 = range.cloneRange();
11300 node2 = range2.createContextualFragment(placeholderHtml);
11301 range2.collapse(false);
11302 range2.insertNode(node2);
11303 range2.detach();
11306 node = range.createContextualFragment(placeholderHtml);
11307 range.insertNode(node);
11309 if (node2) {
11310 caretPlaceholder = this.contain.querySelectorAll("." + className);
11311 range.setStartBefore(caretPlaceholder[0]);
11312 range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]);
11314 this.setSelection(range);
11316 // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder
11317 try {
11318 method(range.startContainer, range.endContainer);
11319 } catch(e) {
11320 setTimeout(function() { throw e; }, 0);
11322 caretPlaceholder = this.contain.querySelectorAll("." + className);
11323 if (caretPlaceholder && caretPlaceholder.length) {
11324 newRange = rangy.createRange(this.doc);
11325 nextSibling = caretPlaceholder[0].nextSibling;
11326 if (caretPlaceholder.length > 1) {
11327 prevSibling = caretPlaceholder[caretPlaceholder.length -1].previousSibling;
11329 if (prevSibling && nextSibling) {
11330 newRange.setStartBefore(nextSibling);
11331 newRange.setEndAfter(prevSibling);
11332 } else {
11333 newCaretPlaceholder = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
11334 dom.insert(newCaretPlaceholder).after(caretPlaceholder[0]);
11335 newRange.setStartBefore(newCaretPlaceholder);
11336 newRange.setEndAfter(newCaretPlaceholder);
11338 this.setSelection(newRange);
11339 for (var i = caretPlaceholder.length; i--;) {
11340 caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]);
11343 } else {
11344 // fallback for when all hell breaks loose
11345 this.contain.focus();
11348 if (restoreScrollPosition) {
11349 body.scrollTop = oldScrollTop;
11350 body.scrollLeft = oldScrollLeft;
11353 // Remove it again, just to make sure that the placeholder is definitely out of the dom tree
11354 try {
11355 caretPlaceholder.parentNode.removeChild(caretPlaceholder);
11356 } catch(e2) {}
11357 },
11359 set: function(node, offset) {
11360 var newRange = rangy.createRange(this.doc);
11361 newRange.setStart(node, offset || 0);
11362 this.setSelection(newRange);
11363 },
11365 /**
11366 * Insert html at the caret or selection position and move the cursor after the inserted html
11367 * Replaces selection content if present
11369 * @param {String} html HTML string to insert
11370 * @example
11371 * selection.insertHTML("<p>foobar</p>");
11372 */
11373 insertHTML: function(html) {
11374 var range = this.getRange(),
11375 node = this.doc.createElement('DIV'),
11376 fragment = this.doc.createDocumentFragment(),
11377 lastChild, lastEditorElement;
11379 if (range) {
11380 range.deleteContents();
11381 node.innerHTML = html;
11382 lastChild = node.lastChild;
11384 while (node.firstChild) {
11385 fragment.appendChild(node.firstChild);
11387 range.insertNode(fragment);
11389 lastEditorElement = this.contain.lastChild;
11390 while (lastEditorElement && lastEditorElement.nodeType === 3 && lastEditorElement.previousSibling && (/^\s*$/).test(lastEditorElement.data)) {
11391 lastEditorElement = lastEditorElement.previousSibling;
11394 if (lastChild) {
11395 // fixes some pad cases mostly on webkit where last nr is needed
11396 if (lastEditorElement && lastChild === lastEditorElement && lastChild.nodeType === 1) {
11397 this.contain.appendChild(this.doc.createElement('br'));
11399 this.setAfter(lastChild);
11402 },
11404 /**
11405 * Insert a node at the caret position and move the cursor behind it
11407 * @param {Object} node HTML string to insert
11408 * @example
11409 * selection.insertNode(document.createTextNode("foobar"));
11410 */
11411 insertNode: function(node) {
11412 var range = this.getRange();
11413 if (range) {
11414 range.deleteContents();
11415 range.insertNode(node);
11417 },
11419 canAppendChild: function (node) {
11420 var anchorNode, anchorNodeTagNameLower,
11421 voidElements = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"],
11422 range = this.getRange();
11424 anchorNode = node || range.startContainer;
11426 if (anchorNode) {
11427 anchorNodeTagNameLower = (anchorNode.tagName || anchorNode.nodeName).toLowerCase();
11430 return voidElements.indexOf(anchorNodeTagNameLower) === -1;
11431 },
11433 splitElementAtCaret: function (element, insertNode) {
11434 var sel = this.getSelection(),
11435 range, contentAfterRangeStart,
11436 firstChild, lastChild, childNodes;
11438 if (sel.rangeCount > 0) {
11439 range = sel.getRangeAt(0).cloneRange(); // Create a copy of the selection range to work with
11441 range.setEndAfter(element); // Place the end of the range after the element
11442 contentAfterRangeStart = range.extractContents(); // Extract the contents of the element after the caret into a fragment
11444 childNodes = contentAfterRangeStart.childNodes;
11446 // Empty elements are cleaned up from extracted content
11447 for (var i = childNodes.length; i --;) {
11448 if (!wysihtml.dom.domNode(childNodes[i]).is.visible()) {
11449 contentAfterRangeStart.removeChild(childNodes[i]);
11453 element.parentNode.insertBefore(contentAfterRangeStart, element.nextSibling);
11455 if (insertNode) {
11456 firstChild = insertNode.firstChild || insertNode;
11457 lastChild = insertNode.lastChild || insertNode;
11459 element.parentNode.insertBefore(insertNode, element.nextSibling);
11461 // Select inserted node contents
11462 if (firstChild && lastChild) {
11463 range.setStartBefore(firstChild);
11464 range.setEndAfter(lastChild);
11465 this.setSelection(range);
11467 } else {
11468 range.setStartAfter(element);
11469 range.setEndAfter(element);
11472 if (!wysihtml.dom.domNode(element).is.visible()) {
11473 if (wysihtml.dom.getTextContent(element) === '') {
11474 element.parentNode.removeChild(element);
11475 } else {
11476 element.parentNode.replaceChild(this.doc.createTextNode(" "), element);
11482 },
11484 /**
11485 * Wraps current selection with the given node
11487 * @param {Object} node The node to surround the selected elements with
11488 */
11489 surround: function(nodeOptions) {
11490 var ranges = this.getOwnRanges(),
11491 node, nodes = [];
11492 if (ranges.length == 0) {
11493 return nodes;
11496 for (var i = ranges.length; i--;) {
11497 node = this.doc.createElement(nodeOptions.nodeName);
11498 nodes.push(node);
11499 if (nodeOptions.className) {
11500 node.className = nodeOptions.className;
11502 if (nodeOptions.cssStyle) {
11503 node.setAttribute('style', nodeOptions.cssStyle);
11505 try {
11506 // This only works when the range boundaries are not overlapping other elements
11507 ranges[i].surroundContents(node);
11508 this.selectNode(node);
11509 } catch(e) {
11510 // fallback
11511 node.appendChild(ranges[i].extractContents());
11512 ranges[i].insertNode(node);
11515 return nodes;
11516 },
11518 /**
11519 * Scroll the current caret position into the view
11520 * FIXME: This is a bit hacky, there might be a smarter way of doing this
11522 * @example
11523 * selection.scrollIntoView();
11524 */
11525 scrollIntoView: function() {
11526 var doc = this.doc,
11527 tolerance = 5, // px
11528 hasScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight,
11529 tempElement = doc._wysihtmlScrollIntoViewElement = doc._wysihtmlScrollIntoViewElement || (function() {
11530 var element = doc.createElement("span");
11531 // The element needs content in order to be able to calculate it's position properly
11532 element.innerHTML = wysihtml.INVISIBLE_SPACE;
11533 return element;
11534 })(),
11535 offsetTop;
11537 if (hasScrollBars) {
11538 this.insertNode(tempElement);
11539 offsetTop = _getCumulativeOffsetTop(tempElement);
11540 tempElement.parentNode.removeChild(tempElement);
11541 if (offsetTop >= (doc.body.scrollTop + doc.documentElement.offsetHeight - tolerance)) {
11542 doc.body.scrollTop = offsetTop;
11545 },
11547 /**
11548 * Select line where the caret is in
11549 */
11550 selectLine: function() {
11551 var r = rangy.createRange();
11552 if (wysihtml.browser.supportsSelectionModify()) {
11553 this._selectLine_W3C();
11554 } else if (r.nativeRange && r.nativeRange.getBoundingClientRect) {
11555 // For IE Edge as it ditched the old api and did not fully implement the new one (as expected)
11556 this._selectLineUniversal();
11558 },
11560 includeRangyRangeHelpers: function() {
11561 var s = this.getSelection(),
11562 r = s.getRangeAt(0),
11563 isHelperNode = function(node) {
11564 return (node && node.nodeType === 1 && node.classList.contains('rangySelectionBoundary'));
11565 },
11566 getNodeLength = function (node) {
11567 if (node.nodeType === 1) {
11568 return node.childNodes && node.childNodes.length || 0;
11569 } else {
11570 return node.data && node.data.length || 0;
11572 },
11573 anode = s.anchorNode.nodeType === 1 ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode,
11574 fnode = s.focusNode.nodeType === 1 ? s.focusNode.childNodes[s.focusOffset] : s.focusNode;
11576 if (fnode && s.focusOffset === getNodeLength(fnode) && fnode.nextSibling && isHelperNode(fnode.nextSibling)) {
11577 r.setEndAfter(fnode.nextSibling);
11579 if (anode && s.anchorOffset === 0 && anode.previousSibling && isHelperNode(anode.previousSibling)) {
11580 r.setStartBefore(anode.previousSibling);
11582 r.select();
11583 },
11585 /**
11586 * See https://developer.mozilla.org/en/DOM/Selection/modify
11587 */
11588 _selectLine_W3C: function() {
11589 var selection = this.win.getSelection(),
11590 initialBoundry = [selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset];
11592 selection.modify("move", "left", "lineboundary");
11593 selection.modify("extend", "right", "lineboundary");
11595 // IF lineboundary extending did not change selection try universal fallback (FF fails sometimes without a reason)
11596 if (selection.anchorNode === initialBoundry[0] &&
11597 selection.anchorOffset === initialBoundry[1] &&
11598 selection.focusNode === initialBoundry[2] &&
11599 selection.focusOffset === initialBoundry[3]
11600 ) {
11601 this._selectLineUniversal();
11602 } else {
11603 this.includeRangyRangeHelpers();
11605 },
11607 // collapses selection to current line beginning or end
11608 toLineBoundary: function (location, collapse) {
11609 collapse = (typeof collapse === 'undefined') ? false : collapse;
11610 if (wysihtml.browser.supportsSelectionModify()) {
11611 var selection = this.win.getSelection();
11613 selection.modify("extend", location, "lineboundary");
11614 if (collapse) {
11615 if (location === "left") {
11616 selection.collapseToStart();
11617 } else if (location === "right") {
11618 selection.collapseToEnd();
11622 },
11624 getRangeRect: function(r) {
11625 var textNode = this.doc.createTextNode("i"),
11626 testNode = this.doc.createTextNode("i"),
11627 rect, cr;
11629 /*testNode.style.visibility = "hidden";
11630 testNode.style.width = "0px";
11631 testNode.style.display = "inline-block";
11632 testNode.style.overflow = "hidden";
11633 testNode.appendChild(textNode);*/
11635 if (r.collapsed) {
11636 r.insertNode(testNode);
11637 r.selectNode(testNode);
11638 rect = r.nativeRange.getBoundingClientRect();
11639 r.deleteContents();
11641 } else {
11642 rect = r.nativeRange.getBoundingClientRect();
11645 return rect;
11647 },
11649 _selectLineUniversal: function() {
11650 var s = this.getSelection(),
11651 r = s.getRangeAt(0),
11652 rect,
11653 startRange, endRange, testRange,
11654 count = 0,
11655 amount, testRect, found,
11656 that = this,
11657 isLineBreakingElement = function(el) {
11658 return el && el.nodeType === 1 && (that.win.getComputedStyle(el).display === "block" || wysihtml.lang.array(['BR', 'HR']).contains(el.nodeName));
11659 },
11660 prevNode = function(node) {
11661 var pnode = node;
11662 if (pnode) {
11663 while (pnode && ((pnode.nodeType === 1 && pnode.classList.contains('rangySelectionBoundary')) || (pnode.nodeType === 3 && (/^\s*$/).test(pnode.data)))) {
11664 pnode = pnode.previousSibling;
11667 return pnode;
11668 };
11670 startRange = r.cloneRange();
11671 endRange = r.cloneRange();
11673 if (r.collapsed) {
11674 // Collapsed state can not have a bounding rect. Thus need to expand it at least by 1 character first while not crossing line boundary
11675 // TODO: figure out a shorter and more readable way
11676 if (r.startContainer.nodeType === 3 && r.startOffset < r.startContainer.data.length) {
11677 r.moveEnd('character', 1);
11678 } else if (r.startContainer.nodeType === 1 && r.startContainer.childNodes[r.startOffset] && r.startContainer.childNodes[r.startOffset].nodeType === 3 && r.startContainer.childNodes[r.startOffset].data.length > 0) {
11679 r.moveEnd('character', 1);
11680 } else if (
11681 r.startOffset > 0 &&
11683 r.startContainer.nodeType === 3 ||
11685 r.startContainer.nodeType === 1 &&
11686 !isLineBreakingElement(prevNode(r.startContainer.childNodes[r.startOffset - 1]))
11689 ) {
11690 r.moveStart('character', -1);
11693 if (!r.collapsed) {
11694 r.insertNode(this.doc.createTextNode(wysihtml.INVISIBLE_SPACE));
11697 // Is probably just empty line as can not be expanded
11698 rect = r.nativeRange.getBoundingClientRect();
11699 // If startnode is not line break allready move the start position of range by -1 character until clientRect top changes;
11700 do {
11701 amount = r.moveStart('character', -1);
11702 testRect = r.nativeRange.getBoundingClientRect();
11704 if (!testRect || Math.floor(testRect.top) !== Math.floor(rect.top)) {
11705 r.moveStart('character', 1);
11706 found = true;
11708 count++;
11709 } while (amount !== 0 && !found && count < 2000);
11710 count = 0;
11711 found = false;
11712 rect = r.nativeRange.getBoundingClientRect();
11714 if (r.endContainer !== this.contain || (this.contain.lastChild && this.contain.childNodes[r.endOffset] !== this.contain.lastChild)) {
11715 do {
11716 amount = r.moveEnd('character', 1);
11717 testRect = r.nativeRange.getBoundingClientRect();
11718 if (!testRect || Math.floor(testRect.bottom) !== Math.floor(rect.bottom)) {
11719 r.moveEnd('character', -1);
11721 // Fix a IE line end marked by linebreak element although caret is before it
11722 // If causes problems should be changed to be applied only to IE
11723 if (r.endContainer && r.endContainer.nodeType === 1 && r.endContainer.childNodes[r.endOffset] && r.endContainer.childNodes[r.endOffset].nodeType === 1 && r.endContainer.childNodes[r.endOffset].nodeName === "BR" && r.endContainer.childNodes[r.endOffset].previousSibling) {
11724 if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 1) {
11725 r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.childNodes.length);
11726 } else if (r.endContainer.childNodes[r.endOffset].previousSibling.nodeType === 3) {
11727 r.setEnd(r.endContainer.childNodes[r.endOffset].previousSibling, r.endContainer.childNodes[r.endOffset].previousSibling.data.length);
11730 found = true;
11732 count++;
11733 } while (amount !== 0 && !found && count < 2000);
11735 r.select();
11736 this.includeRangyRangeHelpers();
11737 },
11739 getText: function() {
11740 var selection = this.getSelection();
11741 return selection ? selection.toString() : "";
11742 },
11744 getNodes: function(nodeType, filter) {
11745 var range = this.getRange();
11746 if (range) {
11747 return range.getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter);
11748 } else {
11749 return [];
11751 },
11753 // Gets all the elements in selection with nodeType
11754 // Ignores the elements not belonging to current editable area
11755 // If filter is defined nodes must pass the filter function with true to be included in list
11756 getOwnNodes: function(nodeType, filter, splitBounds) {
11757 var ranges = this.getOwnRanges(),
11758 nodes = [];
11759 for (var r = 0, rmax = ranges.length; r < rmax; r++) {
11760 if (ranges[r]) {
11761 if (splitBounds) {
11762 ranges[r].splitBoundaries();
11764 nodes = nodes.concat(ranges[r].getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter));
11768 return nodes;
11769 },
11771 fixRangeOverflow: function(range) {
11772 if (this.contain && this.contain.firstChild && range) {
11773 var containment = range.compareNode(this.contain);
11774 if (containment !== 2) {
11775 if (containment === 1) {
11776 range.setStartBefore(this.contain.firstChild);
11778 if (containment === 0) {
11779 range.setEndAfter(this.contain.lastChild);
11781 if (containment === 3) {
11782 range.setStartBefore(this.contain.firstChild);
11783 range.setEndAfter(this.contain.lastChild);
11785 } else if (this._detectInlineRangeProblems(range)) {
11786 var previousElementSibling = range.endContainer.previousElementSibling;
11787 if (previousElementSibling) {
11788 range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling));
11792 },
11794 _endOffsetForNode: function(node) {
11795 var range = document.createRange();
11796 range.selectNodeContents(node);
11797 return range.endOffset;
11798 },
11800 _detectInlineRangeProblems: function(range) {
11801 var position = dom.compareDocumentPosition(range.startContainer, range.endContainer);
11802 return (
11803 range.endOffset == 0 &&
11804 position & 4 //Node.DOCUMENT_POSITION_FOLLOWING
11805 );
11806 },
11808 getRange: function(dontFix) {
11809 var selection = this.getSelection(),
11810 range = selection && selection.rangeCount && selection.getRangeAt(0);
11812 if (dontFix !== true) {
11813 this.fixRangeOverflow(range);
11816 return range;
11817 },
11819 getOwnUneditables: function() {
11820 var allUneditables = dom.query(this.contain, '.' + this.unselectableClass),
11821 deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass);
11823 return wysihtml.lang.array(allUneditables).without(deepUneditables);
11824 },
11826 // Returns an array of ranges that belong only to this editable
11827 // Needed as uneditable block in contenteditabel can split range into pieces
11828 // If manipulating content reverse loop is usually needed as manipulation can shift subsequent ranges
11829 getOwnRanges: function() {
11830 var ranges = [],
11831 r = this.getRange(),
11832 tmpRanges;
11834 if (r) { ranges.push(r); }
11836 if (this.unselectableClass && this.contain && r) {
11837 var uneditables = this.getOwnUneditables(),
11838 tmpRange;
11839 if (uneditables.length > 0) {
11840 for (var i = 0, imax = uneditables.length; i < imax; i++) {
11841 tmpRanges = [];
11842 for (var j = 0, jmax = ranges.length; j < jmax; j++) {
11843 if (ranges[j]) {
11844 switch (ranges[j].compareNode(uneditables[i])) {
11845 case 2:
11846 // all selection inside uneditable. remove
11847 break;
11848 case 3:
11849 //section begins before and ends after uneditable. spilt
11850 tmpRange = ranges[j].cloneRange();
11851 tmpRange.setEndBefore(uneditables[i]);
11852 tmpRanges.push(tmpRange);
11854 tmpRange = ranges[j].cloneRange();
11855 tmpRange.setStartAfter(uneditables[i]);
11856 tmpRanges.push(tmpRange);
11857 break;
11858 default:
11859 // in all other cases uneditable does not touch selection. dont modify
11860 tmpRanges.push(ranges[j]);
11863 ranges = tmpRanges;
11868 return ranges;
11869 },
11871 getSelection: function() {
11872 return rangy.getSelection(this.win);
11873 },
11875 // Sets selection in document to a given range
11876 // Set selection method detects if it fails to set any selection in document and returns null on fail
11877 // (especially needed in webkit where some ranges just can not create selection for no reason)
11878 setSelection: function(range) {
11879 var selection = rangy.getSelection(this.win);
11880 selection.setSingleRange(range);
11881 return (selection && selection.anchorNode && selection.focusNode) ? selection : null;
11882 },
11886 // Webkit has an ancient error of not selecting all contents when uneditable block element is first or last in editable area
11887 selectAll: function() {
11888 var range = this.createRange(),
11889 composer = this.composer,
11890 that = this,
11891 blankEndNode = getWebkitSelectionFixNode(this.composer.element),
11892 blankStartNode = getWebkitSelectionFixNode(this.composer.element),
11893 s;
11895 var doSelect = function() {
11896 range.setStart(composer.element, 0);
11897 range.setEnd(composer.element, composer.element.childNodes.length);
11898 s = that.setSelection(range);
11899 };
11901 var notSelected = function() {
11902 return !s || (s.nativeSelection && s.nativeSelection.type && (s.nativeSelection.type === "Caret" || s.nativeSelection.type === "None"));
11905 wysihtml.dom.removeInvisibleSpaces(this.composer.element);
11906 doSelect();
11908 if (this.composer.element.firstChild && notSelected()) {
11909 // Try fixing end
11910 this.composer.element.appendChild(blankEndNode);
11911 doSelect();
11913 if (notSelected()) {
11914 // Remove end fix
11915 blankEndNode.parentNode.removeChild(blankEndNode);
11917 // Try fixing beginning
11918 this.composer.element.insertBefore(blankStartNode, this.composer.element.firstChild);
11919 doSelect();
11921 if (notSelected()) {
11922 // Try fixing both
11923 this.composer.element.appendChild(blankEndNode);
11924 doSelect();
11928 },
11930 createRange: function() {
11931 return rangy.createRange(this.doc);
11932 },
11934 isCollapsed: function() {
11935 return this.getSelection().isCollapsed;
11936 },
11938 getHtml: function() {
11939 return this.getSelection().toHtml();
11940 },
11942 getPlainText: function () {
11943 return this.getSelection().toString();
11944 },
11946 isEndToEndInNode: function(nodeNames) {
11947 var range = this.getRange(),
11948 parentElement = range.commonAncestorContainer,
11949 startNode = range.startContainer,
11950 endNode = range.endContainer;
11953 if (parentElement.nodeType === wysihtml.TEXT_NODE) {
11954 parentElement = parentElement.parentNode;
11957 if (startNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(startNode.data.substr(range.startOffset))) {
11958 return false;
11961 if (endNode.nodeType === wysihtml.TEXT_NODE && !(/^\s*$/).test(endNode.data.substr(range.endOffset))) {
11962 return false;
11965 while (startNode && startNode !== parentElement) {
11966 if (startNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, startNode)) {
11967 return false;
11969 if (wysihtml.dom.domNode(startNode).prev({ignoreBlankTexts: true})) {
11970 return false;
11972 startNode = startNode.parentNode;
11975 while (endNode && endNode !== parentElement) {
11976 if (endNode.nodeType !== wysihtml.TEXT_NODE && !wysihtml.dom.contains(parentElement, endNode)) {
11977 return false;
11979 if (wysihtml.dom.domNode(endNode).next({ignoreBlankTexts: true})) {
11980 return false;
11982 endNode = endNode.parentNode;
11985 return (wysihtml.lang.array(nodeNames).contains(parentElement.nodeName)) ? parentElement : false;
11986 },
11988 isInThisEditable: function() {
11989 var sel = this.getSelection(),
11990 fnode = sel.focusNode,
11991 anode = sel.anchorNode;
11993 // In IE node contains will not work for textnodes, thus taking parentNode
11994 if (fnode && fnode.nodeType !== 1) {
11995 fnode = fnode.parentNode;
11998 if (anode && anode.nodeType !== 1) {
11999 anode = anode.parentNode;
12002 return anode && fnode &&
12003 (wysihtml.dom.contains(this.composer.element, fnode) || this.composer.element === fnode) &&
12004 (wysihtml.dom.contains(this.composer.element, anode) || this.composer.element === anode);
12005 },
12007 deselect: function() {
12008 var sel = this.getSelection();
12009 sel && sel.removeAllRanges();
12011 });
12013 })(wysihtml);
12015 /**
12016 * Rich Text Query/Formatting Commands
12018 * @example
12019 * var commands = new wysihtml.Commands(editor);
12020 */
12021 wysihtml.Commands = Base.extend(
12022 /** @scope wysihtml.Commands.prototype */ {
12023 constructor: function(editor) {
12024 this.editor = editor;
12025 this.composer = editor.composer;
12026 this.doc = this.composer.doc;
12027 },
12029 /**
12030 * Check whether the browser supports the given command
12032 * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
12033 * @example
12034 * commands.supports("createLink");
12035 */
12036 support: function(command) {
12037 return wysihtml.browser.supportsCommand(this.doc, command);
12038 },
12040 /**
12041 * Check whether the browser supports the given command
12043 * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList")
12044 * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...)
12045 * @example
12046 * commands.exec("insertImage", "http://a1.twimg.com/profile_images/113868655/schrei_twitter_reasonably_small.jpg");
12047 */
12048 exec: function(command, value) {
12049 var obj = wysihtml.commands[command],
12050 args = wysihtml.lang.array(arguments).get(),
12051 method = obj && obj.exec,
12052 result = null;
12054 // If composer ahs placeholder unset it before command
12055 // Do not apply on commands that are behavioral
12056 if (this.composer.hasPlaceholderSet() && !wysihtml.lang.array(['styleWithCSS', 'enableObjectResizing', 'enableInlineTableEditing']).contains(command)) {
12057 this.composer.element.innerHTML = "";
12058 this.composer.selection.selectNode(this.composer.element);
12061 this.editor.fire("beforecommand:composer");
12063 if (method) {
12064 args.unshift(this.composer);
12065 result = method.apply(obj, args);
12066 } else {
12067 try {
12068 // try/catch for buggy firefox
12069 result = this.doc.execCommand(command, false, value);
12070 } catch(e) {}
12073 this.editor.fire("aftercommand:composer");
12074 return result;
12075 },
12077 remove: function(command, commandValue) {
12078 var obj = wysihtml.commands[command],
12079 args = wysihtml.lang.array(arguments).get(),
12080 method = obj && obj.remove;
12081 if (method) {
12082 args.unshift(this.composer);
12083 return method.apply(obj, args);
12085 },
12087 /**
12088 * Check whether the current command is active
12089 * If the caret is within a bold text, then calling this with command "bold" should return true
12091 * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList")
12092 * @param {String} [commandValue] The command value parameter (eg. for "insertImage" the image src)
12093 * @return {Boolean} Whether the command is active
12094 * @example
12095 * var isCurrentSelectionBold = commands.state("bold");
12096 */
12097 state: function(command, commandValue) {
12098 var obj = wysihtml.commands[command],
12099 args = wysihtml.lang.array(arguments).get(),
12100 method = obj && obj.state;
12101 if (method) {
12102 args.unshift(this.composer);
12103 return method.apply(obj, args);
12104 } else {
12105 try {
12106 // try/catch for buggy firefox
12107 return this.doc.queryCommandState(command);
12108 } catch(e) {
12109 return false;
12112 },
12114 /* Get command state parsed value if command has stateValue parsing function */
12115 stateValue: function(command) {
12116 var obj = wysihtml.commands[command],
12117 args = wysihtml.lang.array(arguments).get(),
12118 method = obj && obj.stateValue;
12119 if (method) {
12120 args.unshift(this.composer);
12121 return method.apply(obj, args);
12122 } else {
12123 return false;
12126 });
12128 (function(wysihtml) {
12130 var nodeOptions = {
12131 nodeName: "A",
12132 toggle: false
12133 };
12135 function getOptions(value) {
12136 var options = typeof value === 'object' ? value : {'href': value};
12137 return wysihtml.lang.object({}).merge(nodeOptions).merge({'attribute': value}).get();
12140 wysihtml.commands.createLink = {
12141 exec: function(composer, command, value) {
12142 var opts = getOptions(value);
12144 if (composer.selection.isCollapsed() && !this.state(composer, command)) {
12145 var textNode = composer.doc.createTextNode(opts.attribute.href);
12146 composer.selection.insertNode(textNode);
12147 composer.selection.selectNode(textNode);
12149 wysihtml.commands.formatInline.exec(composer, command, opts);
12150 },
12152 state: function(composer, command) {
12153 return wysihtml.commands.formatInline.state(composer, command, nodeOptions);
12155 };
12157 })(wysihtml);
12159 /* Formatblock
12160 * Is used to insert block level elements
12161 * It tries to solve the case that some block elements should not contain other block level elements (h1-6, p, ...)
12163 */
12164 (function(wysihtml) {
12166 var dom = wysihtml.dom,
12167 // When the caret is within a H1 and the H4 is invoked, the H1 should turn into H4
12168 // instead of creating a H4 within a H1 which would result in semantically invalid html
12169 UNNESTABLE_BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre",
12170 BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote",
12171 INLINE_ELEMENTS = "b, big, i, small, tt, abbr, acronym, cite, code, dfn, em, kbd, strong, samp, var, a, bdo, br, q, span, sub, sup, button, label, textarea, input, select, u";
12173 function correctOptionsForSimilarityCheck(options) {
12174 return {
12175 nodeName: options.nodeName || null,
12176 className: (!options.classRegExp) ? options.className || null : null,
12177 classRegExp: options.classRegExp || null,
12178 styleProperty: options.styleProperty || null
12179 };
12182 function getRangeNode(node, offset) {
12183 if (node.nodeType === 3) {
12184 return node;
12185 } else {
12186 return node.childNodes[offset] || node;
12190 // Returns if node is a line break
12191 function isBr(n) {
12192 return n && n.nodeType === 1 && n.nodeName === "BR";
12195 // Is block level element
12196 function isBlock(n, composer) {
12197 return n && n.nodeType === 1 && composer.win.getComputedStyle(n).display === "block";
12200 // Returns if node is the rangy selection bookmark element (that must not be taken into account in most situatons and is removed on selection restoring)
12201 function isBookmark(n) {
12202 return n && n.nodeType === 1 && n.classList.contains('rangySelectionBoundary');
12205 // Is line breaking node
12206 function isLineBreaking(n, composer) {
12207 return isBr(n) || isBlock(n, composer);
12210 // Removes empty block level elements
12211 function cleanup(composer, newBlockElements) {
12212 wysihtml.dom.removeInvisibleSpaces(composer.element);
12213 var container = composer.element,
12214 allElements = container.querySelectorAll(BLOCK_ELEMENTS),
12215 noEditQuery = composer.config.classNames.uneditableContainer + ([""]).concat(BLOCK_ELEMENTS.split(',')).join(", " + composer.config.classNames.uneditableContainer + ' '),
12216 uneditables = container.querySelectorAll(noEditQuery),
12217 elements = wysihtml.lang.array(allElements).without(uneditables), // Lets not touch uneditable elements and their contents
12218 nbIdx;
12220 for (var i = elements.length; i--;) {
12221 if (elements[i].innerHTML.replace(/[\uFEFF]/g, '') === "" && (newBlockElements.length === 0 || elements[i] !== newBlockElements[newBlockElements.length - 1])) {
12222 // If cleanup removes some new block elements. remove them from newblocks array too
12223 nbIdx = wysihtml.lang.array(newBlockElements).indexOf(elements[i]);
12224 if (nbIdx > -1) {
12225 newBlockElements.splice(nbIdx, 1);
12227 elements[i].parentNode.removeChild(elements[i]);
12231 return newBlockElements;
12234 function defaultNodeName(composer) {
12235 return composer.config.useLineBreaks ? "DIV" : "P";
12238 // The outermost un-nestable block element parent of from node
12239 function findOuterBlock(node, container, allBlocks) {
12240 var n = node,
12241 block = null;
12243 while (n && container && n !== container) {
12244 if (n.nodeType === 1 && n.matches(allBlocks ? BLOCK_ELEMENTS : UNNESTABLE_BLOCK_ELEMENTS)) {
12245 block = n;
12247 n = n.parentNode;
12250 return block;
12253 // Clone for splitting the inner inline element out of its parent inline elements context
12254 // For example if selection is in bold and italic, clone the outer nodes and wrap these around content and return
12255 function cloneOuterInlines(node, container) {
12256 var n = node,
12257 innerNode,
12258 parentNode,
12259 el = null,
12260 el2;
12262 while (n && container && n !== container) {
12263 if (n.nodeType === 1 && n.matches(INLINE_ELEMENTS)) {
12264 parentNode = n;
12265 if (el === null) {
12266 el = n.cloneNode(false);
12267 innerNode = el;
12268 } else {
12269 el2 = n.cloneNode(false);
12270 el2.appendChild(el);
12271 el = el2;
12274 n = n.parentNode;
12277 return {
12278 parent: parentNode,
12279 outerNode: el,
12280 innerNode: innerNode
12281 };
12284 // Formats an element according to options nodeName, className, styleProperty, styleValue
12285 // If element is not defined, creates new element
12286 // if opotions is null, remove format instead
12287 function applyOptionsToElement(element, options, composer) {
12289 if (!element) {
12290 element = composer.doc.createElement(options.nodeName || defaultNodeName(composer));
12291 // Add invisible space as otherwise webkit cannot set selection or range to it correctly
12292 element.appendChild(composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE));
12295 if (options.nodeName && element.nodeName !== options.nodeName) {
12296 element = dom.renameElement(element, options.nodeName);
12299 // Remove similar classes before applying className
12300 if (options.classRegExp) {
12301 element.className = element.className.replace(options.classRegExp, "");
12303 if (options.className) {
12304 element.classList.add(options.className);
12307 if (options.styleProperty && typeof options.styleValue !== "undefined") {
12308 element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
12311 return element;
12314 // Unsets element properties by options
12315 // If nodename given and matches current element, element is unwrapped or converted to default node (depending on presence of class and style attributes)
12316 function removeOptionsFromElement(element, options, composer) {
12317 var style, classes,
12318 prevNode = element.previousSibling,
12319 nextNode = element.nextSibling,
12320 unwrapped = false;
12322 if (options.styleProperty) {
12323 element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = '';
12325 if (options.className) {
12326 element.classList.remove(options.className);
12329 if (options.classRegExp) {
12330 element.className = element.className.replace(options.classRegExp, "");
12333 // Clean up blank class attribute
12334 if (element.getAttribute('class') !== null && element.getAttribute('class').trim() === "") {
12335 element.removeAttribute('class');
12338 if (options.nodeName && element.nodeName.toLowerCase() === options.nodeName.toLowerCase()) {
12339 style = element.getAttribute('style');
12340 if (!style || style.trim() === '') {
12341 dom.unwrap(element);
12342 unwrapped = true;
12343 } else {
12344 element = dom.renameElement(element, defaultNodeName(composer));
12348 // Clean up blank style attribute
12349 if (element.getAttribute('style') !== null && element.getAttribute('style').trim() === "") {
12350 element.removeAttribute('style');
12353 if (unwrapped) {
12354 applySurroundingLineBreaks(prevNode, nextNode, composer);
12358 // Unwraps block level elements from inside content
12359 // Useful as not all block level elements can contain other block-levels
12360 function unwrapBlocksFromContent(element) {
12361 var blocks = element.querySelectorAll(BLOCK_ELEMENTS) || [], // Find unnestable block elements in extracted contents
12362 nextEl, prevEl;
12364 for (var i = blocks.length; i--;) {
12365 nextEl = wysihtml.dom.domNode(blocks[i]).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
12366 prevEl = wysihtml.dom.domNode(blocks[i]).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
12368 if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
12369 if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
12370 blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
12373 if (nextEl && nextEl.nodeType !== 1 && nextEl.nodeName !== 'BR') {
12374 if ((blocks[i].innerHTML || blocks[i].nodeValue || '').trim() !== '') {
12375 blocks[i].parentNode.insertBefore(blocks[i].ownerDocument.createElement('BR'), nextEl);
12378 wysihtml.dom.unwrap(blocks[i]);
12382 // Fix ranges that visually cover whole block element to actually cover the block
12383 function fixRangeCoverage(range, composer) {
12384 var node,
12385 start = range.startContainer,
12386 end = range.endContainer;
12388 // If range has only one childNode and it is end to end the range, extend the range to contain the container element too
12389 // This ensures the wrapper node is modified and optios added to it
12390 if (start && start.nodeType === 1 && start === end) {
12391 if (start.firstChild === start.lastChild && range.endOffset === 1) {
12392 if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
12393 range.setStartBefore(start);
12394 range.setEndAfter(end);
12397 return;
12400 // If range starts outside of node and ends inside at textrange and covers the whole node visually, extend end to cover the node end too
12401 if (start && start.nodeType === 1 && end.nodeType === 3) {
12402 if (start.firstChild === end && range.endOffset === end.data.length) {
12403 if (start !== composer.element && start.nodeName !== 'LI' && start.nodeName !== 'TD') {
12404 range.setEndAfter(start);
12407 return;
12410 // If range ends outside of node and starts inside at textrange and covers the whole node visually, extend start to cover the node start too
12411 if (end && end.nodeType === 1 && start.nodeType === 3) {
12412 if (end.firstChild === start && range.startOffset === 0) {
12413 if (end !== composer.element && end.nodeName !== 'LI' && end.nodeName !== 'TD') {
12414 range.setStartBefore(end);
12417 return;
12420 // If range covers a whole textnode and the textnode is the only child of node, extend range to node
12421 if (start && start.nodeType === 3 && start === end && start.parentNode.childNodes.length === 1) {
12422 if (range.endOffset == end.data.length && range.startOffset === 0) {
12423 node = start.parentNode;
12424 if (node !== composer.element && node.nodeName !== 'LI' && node.nodeName !== 'TD') {
12425 range.setStartBefore(node);
12426 range.setEndAfter(node);
12429 return;
12433 // Scans ranges array for insertion points that are not allowed to insert block tags fixes/splits illegal ranges
12434 // Some places do not allow block level elements inbetween (inside ul and outside li)
12435 // TODO: might need extending for other nodes besides li (maybe dd,dl,dt)
12436 function fixNotPermittedInsertionPoints(ranges) {
12437 var newRanges = [],
12438 lis, j, maxj, tmpRange, rangePos, closestLI;
12440 for (var i = 0, maxi = ranges.length; i < maxi; i++) {
12442 // Fixes range start and end positions if inside UL or OL element (outside of LI)
12443 if (ranges[i].startContainer.nodeType === 1 && ranges[i].startContainer.matches('ul, ol')) {
12444 ranges[i].setStart(ranges[i].startContainer.childNodes[ranges[i].startOffset], 0);
12446 if (ranges[i].endContainer.nodeType === 1 && ranges[i].endContainer.matches('ul, ol')) {
12447 closestLI = ranges[i].endContainer.childNodes[Math.max(ranges[i].endOffset - 1, 0)];
12448 if (closestLI.childNodes) {
12449 ranges[i].setEnd(closestLI, closestLI.childNodes.length);
12453 // Get all LI eleemnts in selection (fully or partially covered)
12454 // And make sure ranges are either inside LI or outside UL/OL
12455 // Split and add new ranges as needed to cover same range content
12456 // TODO: Needs improvement to accept DL, DD, DT
12457 lis = ranges[i].getNodes([1], function(node) {
12458 return node.nodeName === "LI";
12459 });
12460 if (lis.length > 0) {
12462 for (j = 0, maxj = lis.length; j < maxj; j++) {
12463 rangePos = ranges[i].compareNode(lis[j]);
12465 // Fixes start of range that crosses LI border
12466 if (rangePos === ranges[i].NODE_AFTER || rangePos === ranges[i].NODE_INSIDE) {
12467 // Range starts before and ends inside the node
12469 tmpRange = ranges[i].cloneRange();
12470 closestLI = wysihtml.dom.domNode(lis[j]).prev({nodeTypes: [1]});
12472 if (closestLI) {
12473 tmpRange.setEnd(closestLI, closestLI.childNodes.length);
12474 } else if (lis[j].closest('ul, ol')) {
12475 tmpRange.setEndBefore(lis[j].closest('ul, ol'));
12476 } else {
12477 tmpRange.setEndBefore(lis[j]);
12479 newRanges.push(tmpRange);
12480 ranges[i].setStart(lis[j], 0);
12483 // Fixes end of range that crosses li border
12484 if (rangePos === ranges[i].NODE_BEFORE || rangePos === ranges[i].NODE_INSIDE) {
12485 // Range starts inside the node and ends after node
12487 tmpRange = ranges[i].cloneRange();
12488 tmpRange.setEnd(lis[j], lis[j].childNodes.length);
12489 newRanges.push(tmpRange);
12491 // Find next LI in list and if present set range to it, else
12492 closestLI = wysihtml.dom.domNode(lis[j]).next({nodeTypes: [1]});
12493 if (closestLI) {
12494 ranges[i].setStart(closestLI, 0);
12495 } else if (lis[j].closest('ul, ol')) {
12496 ranges[i].setStartAfter(lis[j].closest('ul, ol'));
12497 } else {
12498 ranges[i].setStartAfter(lis[j]);
12502 newRanges.push(ranges[i]);
12503 } else {
12504 newRanges.push(ranges[i]);
12507 return newRanges;
12510 // Return options object with nodeName set if original did not have any
12511 // Node name is set to local or global default
12512 function getOptionsWithNodename(options, defaultName, composer) {
12513 var correctedOptions = (options) ? wysihtml.lang.object(options).clone(true) : null;
12514 if (correctedOptions) {
12515 correctedOptions.nodeName = correctedOptions.nodeName || defaultName || defaultNodeName(composer);
12517 return correctedOptions;
12520 // Injects document fragment to range ensuring outer elements are split to a place where block elements are allowed to be inserted
12521 // Also wraps empty clones of split parent tags around fragment to keep formatting
12522 // If firstOuterBlock is given assume that instead of finding outer (useful for solving cases of some blocks are allowed into others while others are not)
12523 function injectFragmentToRange(fragment, range, composer, firstOuterBlock) {
12524 var rangeStartContainer = range.startContainer,
12525 firstOuterBlock = firstOuterBlock || findOuterBlock(rangeStartContainer, composer.element, true),
12526 outerInlines, first, last, prev, next;
12528 if (firstOuterBlock) {
12529 // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between
12530 first = fragment.firstChild;
12531 last = fragment.lastChild;
12533 composer.selection.splitElementAtCaret(firstOuterBlock, fragment);
12535 next = wysihtml.dom.domNode(last).next({nodeTypes: [1,3], ignoreBlankTexts: true});
12536 prev = wysihtml.dom.domNode(first).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
12538 if (first && !isLineBreaking(first, composer) && prev && !isLineBreaking(prev, composer)) {
12539 first.parentNode.insertBefore(composer.doc.createElement('br'), first);
12542 if (last && !isLineBreaking(last, composer) && next && !isLineBreaking(next, composer)) {
12543 next.parentNode.insertBefore(composer.doc.createElement('br'), next);
12546 } else {
12547 // Ensure node does not get inserted into an inline where it is not allowed
12548 outerInlines = cloneOuterInlines(rangeStartContainer, composer.element);
12549 if (outerInlines.outerNode && outerInlines.innerNode && outerInlines.parent) {
12550 if (fragment.childNodes.length === 1) {
12551 while(fragment.firstChild.firstChild) {
12552 outerInlines.innerNode.appendChild(fragment.firstChild.firstChild);
12554 fragment.firstChild.appendChild(outerInlines.outerNode);
12556 composer.selection.splitElementAtCaret(outerInlines.parent, fragment);
12557 } else {
12558 var fc = fragment.firstChild,
12559 lc = fragment.lastChild;
12561 range.insertNode(fragment);
12562 // restore range position as it might get lost in webkit sometimes
12563 range.setStartBefore(fc);
12564 range.setEndAfter(lc);
12569 // Removes all block formatting from range
12570 function clearRangeBlockFromating(range, closestBlockName, composer) {
12571 var r = range.cloneRange(),
12572 prevNode = getRangeNode(r.startContainer, r.startOffset).previousSibling,
12573 nextNode = getRangeNode(r.endContainer, r.endOffset).nextSibling,
12574 content = r.extractContents(),
12575 fragment = composer.doc.createDocumentFragment(),
12576 children, blocks,
12577 first = true;
12579 while(content.firstChild) {
12580 // Iterate over all selection content first level childNodes
12581 if (content.firstChild.nodeType === 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
12582 // If node is a block element
12583 // Split block formating and add new block to wrap caret
12585 unwrapBlocksFromContent(content.firstChild);
12586 children = wysihtml.dom.unwrap(content.firstChild);
12588 // Add line break before if needed
12589 if (children.length > 0) {
12590 if (
12591 (fragment.lastChild && (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer))) ||
12592 (!fragment.lastChild && prevNode && (prevNode.nodeType !== 1 || isLineBreaking(prevNode, composer)))
12593 ){
12594 fragment.appendChild(composer.doc.createElement('BR'));
12598 for (var c = 0, cmax = children.length; c < cmax; c++) {
12599 fragment.appendChild(children[c]);
12602 // Add line break after if needed
12603 if (children.length > 0) {
12604 if (fragment.lastChild.nodeType !== 1 || !isLineBreaking(fragment.lastChild, composer)) {
12605 if (nextNode || fragment.lastChild !== content.lastChild) {
12606 fragment.appendChild(composer.doc.createElement('BR'));
12611 } else {
12612 fragment.appendChild(content.firstChild);
12615 first = false;
12617 blocks = wysihtml.lang.array(fragment.childNodes).get();
12618 injectFragmentToRange(fragment, r, composer);
12619 return blocks;
12622 // When block node is inserted, look surrounding nodes and remove surplous linebreak tags (as block format breaks line itself)
12623 function removeSurroundingLineBreaks(prevNode, nextNode, composer) {
12624 var prevPrev = prevNode && wysihtml.dom.domNode(prevNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
12625 if (isBr(nextNode)) {
12626 nextNode.parentNode.removeChild(nextNode);
12628 if (isBr(prevNode) && (!prevPrev || prevPrev.nodeType !== 1 || composer.win.getComputedStyle(prevPrev).display !== "block")) {
12629 prevNode.parentNode.removeChild(prevNode);
12633 function applySurroundingLineBreaks(prevNode, nextNode, composer) {
12634 var prevPrev;
12636 if (prevNode && isBookmark(prevNode)) {
12637 prevNode = prevNode.previousSibling;
12639 if (nextNode && isBookmark(nextNode)) {
12640 nextNode = nextNode.nextSibling;
12643 prevPrev = prevNode && prevNode.previousSibling;
12645 if (prevNode && (prevNode.nodeType !== 1 || (composer.win.getComputedStyle(prevNode).display !== "block" && !isBr(prevNode))) && prevNode.parentNode) {
12646 prevNode.parentNode.insertBefore(composer.doc.createElement('br'), prevNode.nextSibling);
12649 if (nextNode && (nextNode.nodeType !== 1 || composer.win.getComputedStyle(nextNode).display !== "block") && nextNode.parentNode) {
12650 nextNode.parentNode.insertBefore(composer.doc.createElement('br'), nextNode);
12654 var isWhitespaceBefore = function (textNode, offset) {
12655 var str = textNode.data ? textNode.data.slice(0, offset) : "";
12656 return (/^\s*$/).test(str);
12659 var isWhitespaceAfter = function (textNode, offset) {
12660 var str = textNode.data ? textNode.data.slice(offset) : "";
12661 return (/^\s*$/).test(str);
12664 var trimBlankTextsAndBreaks = function(fragment) {
12665 if (fragment) {
12666 while (fragment.firstChild && fragment.firstChild.nodeType === 3 && (/^\s*$/).test(fragment.firstChild.data) && fragment.lastChild !== fragment.firstChild) {
12667 fragment.removeChild(fragment.firstChild);
12670 while (fragment.lastChild && fragment.lastChild.nodeType === 3 && (/^\s*$/).test(fragment.lastChild.data) && fragment.lastChild !== fragment.firstChild) {
12671 fragment.removeChild(fragment.lastChild);
12674 if (fragment.firstChild && fragment.firstChild.nodeType === 1 && fragment.firstChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) {
12675 fragment.removeChild(fragment.firstChild);
12678 if (fragment.lastChild && fragment.lastChild.nodeType === 1 && fragment.lastChild.nodeName === "BR" && fragment.lastChild !== fragment.firstChild) {
12679 fragment.removeChild(fragment.lastChild);
12684 // Wrap the range with a block level element
12685 // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur
12686 function wrapRangeWithElement(range, options, closestBlockName, composer) {
12687 var similarOptions = options ? correctOptionsForSimilarityCheck(options) : null,
12688 r = range.cloneRange(),
12689 rangeStartContainer = r.startContainer,
12690 startNode = getRangeNode(r.startContainer, r.startOffset),
12691 endNode = getRangeNode(r.endContainer, r.endOffset),
12692 prevNode = (r.startContainer === startNode && startNode.nodeType === 3 && !isWhitespaceBefore(startNode, r.startOffset)) ? startNode : wysihtml.dom.domNode(startNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true}),
12693 nextNode = (
12695 r.endContainer.nodeType === 1 &&
12696 r.endContainer.childNodes[r.endOffset] === endNode &&
12698 endNode.nodeType === 1 ||
12699 !isWhitespaceAfter(endNode, r.endOffset) &&
12700 !wysihtml.dom.domNode(endNode).is.rangyBookmark()
12702 ) || (
12703 r.endContainer === endNode &&
12704 endNode.nodeType === 3 &&
12705 !isWhitespaceAfter(endNode, r.endOffset)
12707 ) ? endNode : wysihtml.dom.domNode(endNode).next({nodeTypes: [1,3], ignoreBlankTexts: true}),
12708 content = r.extractContents(),
12709 fragment = composer.doc.createDocumentFragment(),
12710 similarOuterBlock = similarOptions ? wysihtml.dom.getParentElement(rangeStartContainer, similarOptions, null, composer.element) : null,
12711 splitAllBlocks = !closestBlockName || !options || (options.nodeName === "BLOCKQUOTE" && closestBlockName === "BLOCKQUOTE"),
12712 firstOuterBlock = similarOuterBlock || findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start
12713 wrapper, blocks, children,
12714 firstc, lastC;
12716 if (wysihtml.dom.domNode(nextNode).is.rangyBookmark()) {
12717 endNode = nextNode;
12718 nextNode = endNode.nextSibling;
12721 trimBlankTextsAndBreaks(content);
12723 if (options && options.nodeName === "BLOCKQUOTE") {
12725 // If blockquote is to be inserted no quessing just add it as outermost block on line or selection
12726 var tmpEl = applyOptionsToElement(null, options, composer);
12727 tmpEl.appendChild(content);
12728 fragment.appendChild(tmpEl);
12729 blocks = [tmpEl];
12731 } else {
12733 if (!content.firstChild) {
12734 // IF selection is caret (can happen if line is empty) add format around tag
12735 fragment.appendChild(applyOptionsToElement(null, options, composer));
12736 } else {
12738 while(content.firstChild) {
12739 // Iterate over all selection content first level childNodes
12741 if (content.firstChild.nodeType == 1 && content.firstChild.matches(BLOCK_ELEMENTS)) {
12743 // If node is a block element
12744 // Escape(split) block formatting at caret
12745 applyOptionsToElement(content.firstChild, options, composer);
12746 if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
12747 unwrapBlocksFromContent(content.firstChild);
12749 fragment.appendChild(content.firstChild);
12751 } else {
12753 // Wrap subsequent non-block nodes inside new block element
12754 wrapper = applyOptionsToElement(null, getOptionsWithNodename(options, closestBlockName, composer), composer);
12755 while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) {
12756 if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) {
12757 unwrapBlocksFromContent(content.firstChild);
12759 wrapper.appendChild(content.firstChild);
12761 fragment.appendChild(wrapper);
12766 blocks = wysihtml.lang.array(fragment.childNodes).get();
12768 injectFragmentToRange(fragment, r, composer, firstOuterBlock);
12769 removeSurroundingLineBreaks(prevNode, nextNode, composer);
12771 // Fix webkit madness by inserting linebreak rangy after cursor marker to blank last block
12772 // (if it contains rangy bookmark, so selection can be restored later correctly)
12773 if (blocks.length > 0 &&
12775 typeof blocks[blocks.length - 1].lastChild === "undefined" || wysihtml.dom.domNode(blocks[blocks.length - 1].lastChild).is.rangyBookmark()
12777 ) {
12778 blocks[blocks.length - 1].appendChild(composer.doc.createElement('br'));
12780 return blocks;
12783 // Find closest block level element
12784 function getParentBlockNodeName(element, composer) {
12785 var parentNode = wysihtml.dom.getParentElement(element, {
12786 query: BLOCK_ELEMENTS
12787 }, null, composer.element);
12789 return (parentNode) ? parentNode.nodeName : null;
12792 // Expands caret to cover the closest block that:
12793 // * cannot contain other block level elements (h1-6,p, etc)
12794 // * Has the same nodeName that is to be inserted
12795 // * has insertingNodeName
12796 // * is DIV if insertingNodeName is not present
12797 //
12798 // If nothing found selects the current line
12799 function expandCaretToBlock(composer, insertingNodeName) {
12800 var parent = wysihtml.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, {
12801 query: UNNESTABLE_BLOCK_ELEMENTS + ', ' + (insertingNodeName ? insertingNodeName.toLowerCase() : 'div'),
12802 }, null, composer.element),
12803 range;
12805 if (parent) {
12806 range = composer.selection.createRange();
12807 range.selectNode(parent);
12808 composer.selection.setSelection(range);
12809 } else if (!composer.isEmpty()) {
12810 composer.selection.selectLine();
12814 // Set selection to begin inside first created block element (beginning of it) and end inside (and after content) of last block element
12815 // TODO: Checking nodetype might be unnescescary as nodes inserted by formatBlock are nodetype 1 anyway
12816 function selectElements(newBlockElements, composer) {
12817 var range = composer.selection.createRange(),
12818 lastEl = newBlockElements[newBlockElements.length - 1],
12819 lastOffset = (lastEl.nodeType === 1 && lastEl.childNodes) ? lastEl.childNodes.length | 0 : lastEl.length || 0;
12821 range.setStart(newBlockElements[0], 0);
12822 range.setEnd(lastEl, lastOffset);
12823 range.select();
12826 // Get all ranges from selection (takes out uneditables and out of editor parts) and apply format to each
12827 // Return created/modified block level elements
12828 // Method can be either "apply" or "remove"
12829 function formatSelection(method, composer, options) {
12830 var ranges = composer.selection.getOwnRanges(),
12831 newBlockElements = [],
12832 closestBlockName;
12834 // Some places do not allow block level elements inbetween (inside ul and outside li, inside table and outside of td/th)
12835 ranges = fixNotPermittedInsertionPoints(ranges);
12837 for (var i = ranges.length; i--;) {
12838 fixRangeCoverage(ranges[i], composer);
12839 closestBlockName = getParentBlockNodeName(ranges[i].startContainer, composer);
12840 if (method === "remove") {
12841 newBlockElements = newBlockElements.concat(clearRangeBlockFromating(ranges[i], closestBlockName, composer));
12842 } else {
12843 newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, closestBlockName, composer));
12846 return newBlockElements;
12849 // If properties is passed as a string, look for tag with that tagName/query
12850 function parseOptions(options) {
12851 if (typeof options === "string") {
12852 options = {
12853 nodeName: options.toUpperCase()
12854 };
12856 return options;
12859 function caretIsOnEmptyLine(composer) {
12860 var caretInfo;
12861 if (composer.selection.isCollapsed()) {
12862 caretInfo = composer.selection.getNodesNearCaret();
12863 if (caretInfo && caretInfo.caretNode) {
12864 if (
12865 // caret is allready breaknode
12866 wysihtml.dom.domNode(caretInfo.caretNode).is.lineBreak() ||
12867 // caret is textnode
12868 (caretInfo.caretNode.nodeType === 3 && caretInfo.textOffset === 0 && (!caretInfo.prevNode || wysihtml.dom.domNode(caretInfo.prevNode).is.lineBreak())) ||
12869 // Caret is temprorary rangy selection marker
12870 (caretInfo.caretNode.nodeType === 1 && caretInfo.caretNode.classList.contains('rangySelectionBoundary') &&
12871 (!caretInfo.prevNode || wysihtml.dom.domNode(caretInfo.prevNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.prevNode).is.block()) &&
12872 (!caretInfo.nextNode || wysihtml.dom.domNode(caretInfo.nextNode).is.lineBreak() || wysihtml.dom.domNode(caretInfo.nextNode).is.block())
12874 ) {
12875 return true;
12879 return false;
12882 wysihtml.commands.formatBlock = {
12883 exec: function(composer, command, options) {
12884 options = parseOptions(options);
12885 var newBlockElements = [],
12886 ranges, range, bookmark, state, closestBlockName;
12888 // Find if current format state is active if options.toggle is set as true
12889 // In toggle case active state elemets are formatted instead of working directly on selection
12890 if (options && options.toggle) {
12891 state = this.state(composer, command, options);
12893 if (state) {
12894 // Remove format from state nodes if toggle set and state on and selection is collapsed
12895 bookmark = rangy.saveSelection(composer.win);
12896 for (var j = 0, jmax = state.length; j < jmax; j++) {
12897 removeOptionsFromElement(state[j], options, composer);
12900 } else {
12901 // If selection is caret expand it to cover nearest suitable block element or row if none found
12902 if (composer.selection.isCollapsed()) {
12903 bookmark = rangy.saveSelection(composer.win);
12904 if (caretIsOnEmptyLine(composer)) {
12905 composer.selection.selectLine();
12906 } else {
12907 expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
12910 if (options) {
12911 newBlockElements = formatSelection("apply", composer, options);
12912 } else {
12913 // Options == null means block formatting should be removed from selection
12914 newBlockElements = formatSelection("remove", composer);
12919 // Remove empty block elements that may be left behind
12920 // Also remove them from new blocks list
12921 newBlockElements = cleanup(composer, newBlockElements);
12923 // Restore selection
12924 if (bookmark) {
12925 rangy.restoreSelection(bookmark);
12926 } else {
12927 selectElements(newBlockElements, composer);
12929 },
12931 // Removes all block formatting from selection
12932 remove: function(composer, command, options) {
12933 options = parseOptions(options);
12934 var newBlockElements, bookmark;
12936 // If selection is caret expand it to cover nearest suitable block element or row if none found
12937 if (composer.selection.isCollapsed()) {
12938 bookmark = rangy.saveSelection(composer.win);
12939 expandCaretToBlock(composer, options && options.nodeName ? options.nodeName.toUpperCase() : undefined);
12942 newBlockElements = formatSelection("remove", composer);
12943 newBlockElements = cleanup(composer, newBlockElements);
12945 // Restore selection
12946 if (bookmark) {
12947 rangy.restoreSelection(bookmark);
12948 } else {
12949 selectElements(newBlockElements, composer);
12951 },
12953 // If options as null is passed returns status describing all block level elements
12954 state: function(composer, command, options) {
12955 options = parseOptions(options);
12957 var nodes = composer.selection.filterElements((function (element) { // Finds matching elements inside selection
12958 return wysihtml.dom.domNode(element).test(options || { query: BLOCK_ELEMENTS });
12959 }).bind(this)),
12960 parentNodes = composer.selection.getSelectedOwnNodes(),
12961 parent;
12963 // Finds matching elements that are parents of selection and adds to nodes list
12964 for (var i = 0, maxi = parentNodes.length; i < maxi; i++) {
12965 parent = dom.getParentElement(parentNodes[i], options || { query: BLOCK_ELEMENTS }, null, composer.element);
12966 if (parent && nodes.indexOf(parent) === -1) {
12967 nodes.push(parent);
12971 return (nodes.length === 0) ? false : nodes;
12974 };
12975 })(wysihtml);
12977 /**
12978 * Unifies all inline tags additions and removals
12979 * See https://github.com/Voog/wysihtml/pull/169 for specification of action
12980 */
12982 (function(wysihtml) {
12984 var defaultTag = "SPAN",
12985 INLINE_ELEMENTS = "b, big, i, small, tt, abbr, acronym, cite, code, dfn, em, kbd, strong, samp, var, a, bdo, br, q, span, sub, sup, button, label, textarea, input, select, u",
12986 queryAliasMap = {
12987 "b": "b, strong",
12988 "strong": "b, strong",
12989 "em": "em, i",
12990 "i": "em, i"
12991 };
12993 function hasNoClass(element) {
12994 return (/^\s*$/).test(element.className);
12997 function hasNoStyle(element) {
12998 return !element.getAttribute('style') || (/^\s*$/).test(element.getAttribute('style'));
13001 // Associative arrays in javascript are really objects and do not have length defined
13002 // Thus have to check emptyness in a different way
13003 function hasNoAttributes(element) {
13004 var attr = wysihtml.dom.getAttributes(element);
13005 return wysihtml.lang.object(attr).isEmpty();
13008 // compares two nodes if they are semantically the same
13009 // Used in cleanup to find consequent semantically similar elements for merge
13010 function isSameNode(element1, element2) {
13011 var classes1, classes2,
13012 attr1, attr2;
13014 if (element1.nodeType !== 1 || element2.nodeType !== 1) {
13015 return false;
13018 if (element1.nodeName !== element2.nodeName) {
13019 return false;
13022 classes1 = element1.className.trim().replace(/\s+/g, ' ').split(' ');
13023 classes2 = element2.className.trim().replace(/\s+/g, ' ').split(' ');
13024 if (wysihtml.lang.array(classes1).without(classes2).length > 0) {
13025 return false;
13028 attr1 = wysihtml.dom.getAttributes(element1);
13029 attr2 = wysihtml.dom.getAttributes(element2);
13031 if (attr1.length !== attr2.length || !wysihtml.lang.object(wysihtml.lang.object(attr1).difference(attr2)).isEmpty()) {
13032 return false;
13035 return true;
13038 function createWrapNode(textNode, options) {
13039 var nodeName = options && options.nodeName || defaultTag,
13040 element = textNode.ownerDocument.createElement(nodeName);
13042 // Remove similar classes before applying className
13043 if (options.classRegExp) {
13044 element.className = element.className.replace(options.classRegExp, "");
13047 if (options.className) {
13048 element.classList.add(options.className);
13051 if (options.styleProperty && typeof options.styleValue !== "undefined") {
13052 element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
13055 if (options.attribute) {
13056 if (typeof options.attribute === "object") {
13057 for (var a in options.attribute) {
13058 if (options.attribute.hasOwnProperty(a)) {
13059 element.setAttribute(a, options.attribute[a]);
13062 } else if (typeof options.attributeValue !== "undefined") {
13063 element.setAttribute(options.attribute, options.attributeValue);
13067 return element;
13070 // Tests if attr2 list contains all attributes present in attr1
13071 // Note: attr 1 can have more attributes than attr2
13072 function containsSameAttributes(attr1, attr2) {
13073 for (var a in attr1) {
13074 if (attr1.hasOwnProperty(a)) {
13075 if (typeof attr2[a] === undefined || attr2[a] !== attr1[a]) {
13076 return false;
13080 return true;
13083 // If attrbutes and values are the same > remove
13084 // if attributes or values
13085 function updateElementAttributes(element, newAttributes, toggle) {
13086 var attr = wysihtml.dom.getAttributes(element),
13087 fullContain = containsSameAttributes(newAttributes, attr),
13088 attrDifference = wysihtml.lang.object(attr).difference(newAttributes),
13089 a, b;
13091 if (fullContain && toggle !== false) {
13092 for (a in newAttributes) {
13093 if (newAttributes.hasOwnProperty(a)) {
13094 element.removeAttribute(a);
13097 } else {
13099 /*if (!wysihtml.lang.object(attrDifference).isEmpty()) {
13100 for (b in attrDifference) {
13101 if (attrDifference.hasOwnProperty(b)) {
13102 element.removeAttribute(b);
13105 }*/
13107 for (a in newAttributes) {
13108 if (newAttributes.hasOwnProperty(a)) {
13109 element.setAttribute(a, newAttributes[a]);
13115 function updateFormatOfElement(element, options) {
13116 var attr, newNode, a, newAttributes, nodeNameQuery, nodeQueryMatch;
13118 if (options.className) {
13119 if (options.toggle !== false && element.classList.contains(options.className)) {
13120 element.classList.remove(options.className);
13121 } else {
13122 if (options.classRegExp) {
13123 element.className = element.className.replace(options.classRegExp, '');
13125 element.classList.add(options.className);
13127 if (hasNoClass(element)) {
13128 element.removeAttribute('class');
13132 // change/remove style
13133 if (options.styleProperty) {
13134 if (options.toggle !== false && element.style[wysihtml.browser.fixStyleKey(options.styleProperty)].trim().replace(/, /g, ",") === options.styleValue) {
13135 element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = '';
13136 } else {
13137 element.style[wysihtml.browser.fixStyleKey(options.styleProperty)] = options.styleValue;
13140 if (hasNoStyle(element)) {
13141 element.removeAttribute('style');
13144 if (options.attribute) {
13145 if (typeof options.attribute === "object") {
13146 newAttributes = options.attribute;
13147 } else {
13148 newAttributes = {};
13149 newAttributes[options.attribute] = options.attributeValue || '';
13151 updateElementAttributes(element, newAttributes, options.toggle);
13155 // Handle similar semantically same elements (queryAliasMap)
13156 nodeNameQuery = options.nodeName ? queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase() : null;
13157 nodeQueryMatch = nodeNameQuery ? wysihtml.dom.domNode(element).test({ query: nodeNameQuery }) : false;
13159 // Unwrap element if no attributes present and node name given
13160 // or no attributes and if no nodename set but node is the default
13161 if (!options.nodeName || options.nodeName === defaultTag || nodeQueryMatch) {
13162 if (
13163 ((options.toggle !== false && nodeQueryMatch) || (!options.nodeName && element.nodeName === defaultTag)) &&
13164 hasNoClass(element) && hasNoStyle(element) && hasNoAttributes(element)
13165 ) {
13166 wysihtml.dom.unwrap(element);
13172 // Fetch all textnodes in selection
13173 // Empty textnodes are ignored except the one containing text caret
13174 function getSelectedTextNodes(selection, splitBounds) {
13175 var textNodes = [];
13177 if (!selection.isCollapsed()) {
13178 textNodes = textNodes.concat(selection.getOwnNodes([3], function(node) {
13179 // Exclude empty nodes except caret node
13180 return (!wysihtml.dom.domNode(node).is.emptyTextNode());
13181 }, splitBounds));
13184 return textNodes;
13187 function findSimilarTextNodeWrapper(textNode, options, container, exact) {
13188 var node = textNode,
13189 similarOptions = exact ? options : correctOptionsForSimilarityCheck(options);
13191 do {
13192 if (node.nodeType === 1 && isSimilarNode(node, similarOptions)) {
13193 return node;
13195 node = node.parentNode;
13196 } while (node && node !== container);
13198 return null;
13201 function correctOptionsForSimilarityCheck(options) {
13202 return {
13203 nodeName: options.nodeName || null,
13204 className: (!options.classRegExp) ? options.className || null : null,
13205 classRegExp: options.classRegExp || null,
13206 styleProperty: options.styleProperty || null
13207 };
13210 // Finds inline node with similar nodeName/style/className
13211 // If nodeName is specified inline node with the same (or alias) nodeName is expected to prove similar regardless of attributes
13212 function isSimilarNode(node, options) {
13213 var o;
13214 if (options.nodeName) {
13215 var query = queryAliasMap[options.nodeName.toLowerCase()] || options.nodeName.toLowerCase();
13216 return wysihtml.dom.domNode(node).test({ query: query });
13217 } else {
13218 o = wysihtml.lang.object(options).clone();
13219 o.query = INLINE_ELEMENTS; // make sure only inline elements with styles and classes are counted
13220 return wysihtml.dom.domNode(node).test(o);
13224 function selectRange(composer, range) {
13225 var d = document.documentElement || document.body,
13226 oldScrollTop = d.scrollTop,
13227 oldScrollLeft = d.scrollLeft,
13228 selection = rangy.getSelection(composer.win);
13230 rangy.getSelection(composer.win).removeAllRanges();
13232 // IE looses focus of contenteditable on removeallranges and can not set new selection unless contenteditable is focused again
13233 try {
13234 rangy.getSelection(composer.win).addRange(range);
13235 } catch (e) {}
13236 if (!composer.doc.activeElement || !wysihtml.dom.contains(composer.element, composer.doc.activeElement)) {
13237 composer.element.focus();
13238 d.scrollTop = oldScrollTop;
13239 d.scrollLeft = oldScrollLeft;
13240 rangy.getSelection(composer.win).addRange(range);
13244 function selectTextNodes(textNodes, composer) {
13245 var range = rangy.createRange(composer.doc),
13246 lastText = textNodes[textNodes.length - 1];
13248 if (textNodes[0] && lastText) {
13249 range.setStart(textNodes[0], 0);
13250 range.setEnd(lastText, lastText.length);
13251 selectRange(composer, range);
13256 function selectTextNode(composer, node, start, end) {
13257 var range = rangy.createRange(composer.doc);
13258 if (node) {
13259 range.setStart(node, start);
13260 range.setEnd(node, typeof end !== 'undefined' ? end : start);
13261 selectRange(composer, range);
13265 function getState(composer, options, exact) {
13266 var searchNodes = getSelectedTextNodes(composer.selection),
13267 nodes = [],
13268 partial = false,
13269 node, range, caretNode;
13271 if (composer.selection.isInThisEditable()) {
13273 if (searchNodes.length === 0 && composer.selection.isCollapsed()) {
13274 caretNode = composer.selection.getSelection().anchorNode;
13275 if (!caretNode) {
13276 // selection not in editor
13277 return {
13278 nodes: [],
13279 partial: false
13280 };
13282 if (caretNode.nodeType === 3) {
13283 searchNodes = [caretNode];
13287 // Handle collapsed selection caret
13288 if (!searchNodes.length) {
13289 range = composer.selection.getOwnRanges()[0];
13290 if (range) {
13291 searchNodes = [range.endContainer];
13295 for (var i = 0, maxi = searchNodes.length; i < maxi; i++) {
13296 node = findSimilarTextNodeWrapper(searchNodes[i], options, composer.element, exact);
13297 if (node) {
13298 nodes.push(node);
13299 } else {
13300 partial = true;
13306 return {
13307 nodes: nodes,
13308 partial: partial
13309 };
13312 // Returns if caret is inside a word in textnode (not on boundary)
13313 // If selection anchornode is not text node, returns false
13314 function caretIsInsideWord(selection) {
13315 var anchor, offset, beforeChar, afterChar;
13316 if (selection) {
13317 anchor = selection.anchorNode;
13318 offset = selection.anchorOffset;
13319 if (anchor && anchor.nodeType === 3 && offset > 0 && offset < anchor.data.length) {
13320 beforeChar = anchor.data[offset - 1];
13321 afterChar = anchor.data[offset];
13322 return (/\w/).test(beforeChar) && (/\w/).test(afterChar);
13325 return false;
13328 // Returns a range and textnode containing object from caret position covering a whole word
13329 // wordOffsety describes the original position of caret in the new textNode
13330 // Caret has to be inside a textNode.
13331 function getRangeForWord(selection) {
13332 var anchor, offset, doc, range, offsetStart, offsetEnd, beforeChar, afterChar,
13333 txtNodes = [];
13334 if (selection) {
13335 anchor = selection.anchorNode;
13336 offset = offsetStart = offsetEnd = selection.anchorOffset;
13337 doc = anchor.ownerDocument;
13338 range = rangy.createRange(doc);
13340 if (anchor && anchor.nodeType === 3) {
13342 while (offsetStart > 0 && (/\w/).test(anchor.data[offsetStart - 1])) {
13343 offsetStart--;
13346 while (offsetEnd < anchor.data.length && (/\w/).test(anchor.data[offsetEnd])) {
13347 offsetEnd++;
13350 range.setStartAndEnd(anchor, offsetStart, offsetEnd);
13351 range.splitBoundaries();
13352 txtNodes = range.getNodes([3], function(node) {
13353 return (!wysihtml.dom.domNode(node).is.emptyTextNode());
13354 });
13356 return {
13357 wordOffset: offset - offsetStart,
13358 range: range,
13359 textNode: txtNodes[0]
13360 };
13364 return false;
13367 // Contents of 2 elements are merged to fitst element. second element is removed as consequence
13368 function mergeContents(element1, element2) {
13369 while (element2.firstChild) {
13370 element1.appendChild(element2.firstChild);
13372 element2.parentNode.removeChild(element2);
13375 function mergeConsequentSimilarElements(elements) {
13376 for (var i = elements.length; i--;) {
13378 if (elements[i] && elements[i].parentNode) { // Test if node is not allready removed in cleanup
13380 if (elements[i].nextSibling && isSameNode(elements[i], elements[i].nextSibling)) {
13381 mergeContents(elements[i], elements[i].nextSibling);
13384 if (elements[i].previousSibling && isSameNode(elements[i] , elements[i].previousSibling)) {
13385 mergeContents(elements[i].previousSibling, elements[i]);
13392 function cleanupAndSetSelection(composer, textNodes, options) {
13393 if (textNodes.length > 0) {
13394 selectTextNodes(textNodes, composer);
13396 mergeConsequentSimilarElements(getState(composer, options).nodes);
13397 if (textNodes.length > 0) {
13398 selectTextNodes(textNodes, composer);
13402 function cleanupAndSetCaret(composer, textNode, offset, options) {
13403 selectTextNode(composer, textNode, offset);
13404 mergeConsequentSimilarElements(getState(composer, options).nodes);
13405 selectTextNode(composer, textNode, offset);
13408 // Formats a textnode with given options
13409 function formatTextNode(textNode, options) {
13410 var wrapNode = createWrapNode(textNode, options);
13412 textNode.parentNode.insertBefore(wrapNode, textNode);
13413 wrapNode.appendChild(textNode);
13416 // Changes/toggles format of a textnode
13417 function unformatTextNode(textNode, composer, options) {
13418 var container = composer.element,
13419 wrapNode = findSimilarTextNodeWrapper(textNode, options, container),
13420 newWrapNode;
13422 if (wrapNode) {
13423 newWrapNode = wrapNode.cloneNode(false);
13425 wysihtml.dom.domNode(textNode).escapeParent(wrapNode, newWrapNode);
13426 updateFormatOfElement(newWrapNode, options);
13430 // Removes the format around textnode
13431 function removeFormatFromTextNode(textNode, composer, options) {
13432 var container = composer.element,
13433 wrapNode = findSimilarTextNodeWrapper(textNode, options, container);
13435 if (wrapNode) {
13436 wysihtml.dom.domNode(textNode).escapeParent(wrapNode);
13440 // Creates node around caret formated with options
13441 function formatTextRange(range, composer, options) {
13442 var wrapNode = createWrapNode(range.endContainer, options);
13444 range.surroundContents(wrapNode);
13445 composer.selection.selectNode(wrapNode);
13448 // Changes/toggles format of whole selection
13449 function updateFormat(composer, textNodes, state, options) {
13450 var exactState = getState(composer, options, true),
13451 selection = composer.selection.getSelection(),
13452 wordObj, textNode, newNode, i;
13454 if (!textNodes.length) {
13455 // Selection is caret
13458 if (options.toggle !== false) {
13459 if (caretIsInsideWord(selection)) {
13461 // Unformat whole word
13462 wordObj = getRangeForWord(selection);
13463 textNode = wordObj.textNode;
13464 unformatTextNode(wordObj.textNode, composer, options);
13465 cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options);
13467 } else {
13469 // Escape caret out of format
13470 textNode = composer.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
13471 newNode = state.nodes[0].cloneNode(false);
13472 newNode.appendChild(textNode);
13473 composer.selection.splitElementAtCaret(state.nodes[0], newNode);
13474 updateFormatOfElement(newNode, options);
13475 cleanupAndSetSelection(composer, [textNode], options);
13476 var s = composer.selection.getSelection();
13477 if (s.anchorNode && s.focusNode) {
13478 // Has an error in IE when collapsing selection. probably from rangy
13479 try {
13480 s.collapseToEnd();
13481 } catch (e) {}
13484 } else {
13485 // In non-toggle mode the closest state element has to be found and the state updated differently
13486 for (i = state.nodes.length; i--;) {
13487 updateFormatOfElement(state.nodes[i], options);
13491 } else {
13493 if (!exactState.partial && options.toggle !== false) {
13495 // If whole selection (all textnodes) are in the applied format
13496 // remove the format from selection
13497 // Non-toggle mode never removes. Remove has to be called explicitly
13498 for (i = textNodes.length; i--;) {
13499 unformatTextNode(textNodes[i], composer, options);
13502 } else {
13504 // Selection is partially in format
13505 // change it to new if format if textnode allreafy in similar state
13506 // else just apply
13508 for (i = textNodes.length; i--;) {
13510 if (findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) {
13511 unformatTextNode(textNodes[i], composer, options);
13514 if (!findSimilarTextNodeWrapper(textNodes[i], options, composer.element)) {
13515 formatTextNode(textNodes[i], options);
13521 cleanupAndSetSelection(composer, textNodes, options);
13525 // Removes format from selection
13526 function removeFormat(composer, textNodes, state, options) {
13527 var textNode, textOffset, newNode, i,
13528 selection = composer.selection.getSelection();
13530 if (!textNodes.length) {
13531 textNode = selection.anchorNode;
13532 textOffset = selection.anchorOffset;
13534 for (i = state.nodes.length; i--;) {
13535 wysihtml.dom.unwrap(state.nodes[i]);
13538 cleanupAndSetCaret(composer, textNode, textOffset, options);
13539 } else {
13540 for (i = textNodes.length; i--;) {
13541 removeFormatFromTextNode(textNodes[i], composer, options);
13543 cleanupAndSetSelection(composer, textNodes, options);
13547 // Adds format to selection
13548 function applyFormat(composer, textNodes, options) {
13549 var wordObj, i,
13550 selection = composer.selection.getSelection();
13552 if (!textNodes.length) {
13553 // Handle collapsed selection caret and return
13554 if (caretIsInsideWord(selection)) {
13556 wordObj = getRangeForWord(selection);
13557 formatTextNode(wordObj.textNode, options);
13558 cleanupAndSetCaret(composer, wordObj.textNode, wordObj.wordOffset, options);
13560 } else {
13561 var r = composer.selection.getOwnRanges()[0];
13562 if (r) {
13563 formatTextRange(r, composer, options);
13567 } else {
13568 // Handle textnodes in selection and apply format
13569 for (i = textNodes.length; i--;) {
13570 formatTextNode(textNodes[i], options);
13572 cleanupAndSetSelection(composer, textNodes, options);
13576 // If properties is passed as a string, correct options with that nodeName
13577 function fixOptions(options) {
13578 options = (typeof options === "string") ? { nodeName: options } : options;
13579 if (options.nodeName) { options.nodeName = options.nodeName.toUpperCase(); }
13580 return options;
13583 wysihtml.commands.formatInline = {
13585 // Basics:
13586 // In case of plain text or inline state not set wrap all non-empty textnodes with
13587 // In case a similar inline wrapper node is detected on one of textnodes, the wrapper node is changed (if fully contained) or split and changed (partially contained)
13588 // In case of changing mode every textnode is addressed separatly
13589 exec: function(composer, command, options) {
13590 options = fixOptions(options);
13592 // Join adjactent textnodes first
13593 composer.element.normalize();
13595 var textNodes = getSelectedTextNodes(composer.selection, true),
13596 state = getState(composer, options);
13597 if (state.nodes.length > 0) {
13598 // Text allready has the format applied
13599 updateFormat(composer, textNodes, state, options);
13600 } else {
13601 // Selection is not in the applied format
13602 applyFormat(composer, textNodes, options);
13604 composer.element.normalize();
13605 },
13607 remove: function(composer, command, options) {
13608 options = fixOptions(options);
13609 composer.element.normalize();
13611 var textNodes = getSelectedTextNodes(composer.selection, true),
13612 state = getState(composer, options);
13614 if (state.nodes.length > 0) {
13615 // Text allready has the format applied
13616 removeFormat(composer, textNodes, state, options);
13619 composer.element.normalize();
13620 },
13622 state: function(composer, command, options) {
13623 options = fixOptions(options);
13624 var nodes = getState(composer, options, true).nodes;
13625 return (nodes.length === 0) ? false : nodes;
13627 };
13629 })(wysihtml);
13631 (function(wysihtml){
13632 wysihtml.commands.indentList = {
13633 exec: function(composer, command, value) {
13634 var listEls = composer.selection.getSelectionParentsByTag('LI');
13635 if (listEls) {
13636 return this.tryToPushLiLevel(listEls, composer.selection);
13638 return false;
13639 },
13641 state: function(composer, command) {
13642 return false;
13643 },
13645 tryToPushLiLevel: function(liNodes, selection) {
13646 var listTag, list, prevLi, liNode, prevLiList,
13647 found = false;
13649 selection.executeAndRestoreRangy(function() {
13651 for (var i = liNodes.length; i--;) {
13652 liNode = liNodes[i];
13653 listTag = (liNode.parentNode.nodeName === 'OL') ? 'OL' : 'UL';
13654 list = liNode.ownerDocument.createElement(listTag);
13655 prevLi = wysihtml.dom.domNode(liNode).prev({nodeTypes: [wysihtml.ELEMENT_NODE]});
13656 prevLiList = (prevLi) ? prevLi.querySelector('ul, ol') : null;
13658 if (prevLi) {
13659 if (prevLiList) {
13660 prevLiList.appendChild(liNode);
13661 } else {
13662 list.appendChild(liNode);
13663 prevLi.appendChild(list);
13665 found = true;
13669 });
13670 return found;
13672 };
13673 }(wysihtml));
13675 (function(wysihtml){
13676 wysihtml.commands.insertHTML = {
13677 exec: function(composer, command, html) {
13678 composer.selection.insertHTML(html);
13679 },
13681 state: function() {
13682 return false;
13684 };
13685 }(wysihtml));
13687 (function(wysihtml) {
13688 var LINE_BREAK = "<br>" + (wysihtml.browser.needsSpaceAfterLineBreak() ? " " : "");
13690 wysihtml.commands.insertLineBreak = {
13691 exec: function(composer, command) {
13692 composer.selection.insertHTML(LINE_BREAK);
13693 },
13695 state: function() {
13696 return false;
13698 };
13699 })(wysihtml);
13701 wysihtml.commands.insertList = (function(wysihtml) {
13703 var isNode = function(node, name) {
13704 if (node && node.nodeName) {
13705 if (typeof name === 'string') {
13706 name = [name];
13708 for (var n = name.length; n--;) {
13709 if (node.nodeName === name[n]) {
13710 return true;
13714 return false;
13715 };
13717 var findListEl = function(node, nodeName, composer) {
13718 var ret = {
13719 el: null,
13720 other: false
13721 };
13723 if (node) {
13724 var parentLi = wysihtml.dom.getParentElement(node, { query: "li" }, false, composer.element),
13725 otherNodeName = (nodeName === "UL") ? "OL" : "UL";
13727 if (isNode(node, nodeName)) {
13728 ret.el = node;
13729 } else if (isNode(node, otherNodeName)) {
13730 ret = {
13731 el: node,
13732 other: true
13733 };
13734 } else if (parentLi) {
13735 if (isNode(parentLi.parentNode, nodeName)) {
13736 ret.el = parentLi.parentNode;
13737 } else if (isNode(parentLi.parentNode, otherNodeName)) {
13738 ret = {
13739 el : parentLi.parentNode,
13740 other: true
13741 };
13746 // do not count list elements outside of composer
13747 if (ret.el && !composer.element.contains(ret.el)) {
13748 ret.el = null;
13751 return ret;
13752 };
13754 var handleSameTypeList = function(el, nodeName, composer) {
13755 var otherNodeName = (nodeName === "UL") ? "OL" : "UL",
13756 otherLists, innerLists;
13757 // Unwrap list
13758 // <ul><li>foo</li><li>bar</li></ul>
13759 // becomes:
13760 // foo<br>bar<br>
13762 composer.selection.executeAndRestoreRangy(function() {
13763 otherLists = getListsInSelection(otherNodeName, composer);
13764 if (otherLists.length) {
13765 for (var l = otherLists.length; l--;) {
13766 wysihtml.dom.renameElement(otherLists[l], nodeName.toLowerCase());
13768 } else {
13769 innerLists = getListsInSelection(['OL', 'UL'], composer);
13770 for (var i = innerLists.length; i--;) {
13771 wysihtml.dom.resolveList(innerLists[i], composer.config.useLineBreaks);
13773 if (innerLists.length === 0) {
13774 wysihtml.dom.resolveList(el, composer.config.useLineBreaks);
13777 });
13778 };
13780 var handleOtherTypeList = function(el, nodeName, composer) {
13781 var otherNodeName = (nodeName === "UL") ? "OL" : "UL";
13782 // Turn an ordered list into an unordered list
13783 // <ol><li>foo</li><li>bar</li></ol>
13784 // becomes:
13785 // <ul><li>foo</li><li>bar</li></ul>
13786 // Also rename other lists in selection
13787 composer.selection.executeAndRestoreRangy(function() {
13788 var renameLists = [el].concat(getListsInSelection(otherNodeName, composer));
13790 // All selection inner lists get renamed too
13791 for (var l = renameLists.length; l--;) {
13792 wysihtml.dom.renameElement(renameLists[l], nodeName.toLowerCase());
13794 });
13795 };
13797 var getListsInSelection = function(nodeName, composer) {
13798 var ranges = composer.selection.getOwnRanges(),
13799 renameLists = [];
13801 for (var r = ranges.length; r--;) {
13802 renameLists = renameLists.concat(ranges[r].getNodes([1], function(node) {
13803 return isNode(node, nodeName);
13804 }));
13807 return renameLists;
13808 };
13810 var createListFallback = function(nodeName, composer) {
13811 var sel = rangy.saveSelection(composer.win);
13813 // Fallback for Create list
13814 var tempClassName = "_wysihtml-temp-" + new Date().getTime(),
13815 isEmpty, list;
13817 composer.commands.exec("formatBlock", {
13818 "nodeName": "div",
13819 "className": tempClassName
13820 });
13822 var tempElement = composer.element.querySelector("." + tempClassName);
13824 // This space causes new lists to never break on enter
13825 var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g;
13826 tempElement.innerHTML = tempElement.innerHTML.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, "");
13827 if (tempElement) {
13828 isEmpty = (/^(\s|(<br>))+$/i).test(tempElement.innerHTML);
13829 list = wysihtml.dom.convertToList(tempElement, nodeName.toLowerCase(), composer.parent.config.classNames.uneditableContainer);
13830 if (sel) {
13831 rangy.restoreSelection(sel);
13833 if (isEmpty) {
13834 composer.selection.selectNode(list.querySelector("li"), true);
13837 };
13839 return {
13840 exec: function(composer, command, nodeName) {
13841 var doc = composer.doc,
13842 cmd = (nodeName === "OL") ? "insertOrderedList" : "insertUnorderedList",
13843 s = composer.selection.getSelection(),
13844 anode = s.anchorNode.nodeType === 1 && s.anchorNode.firstChild ? s.anchorNode.childNodes[s.anchorOffset] : s.anchorNode,
13845 fnode = s.focusNode.nodeType === 1 && s.focusNode.firstChild ? s.focusNode.childNodes[s.focusOffset] || s.focusNode.lastChild : s.focusNode,
13846 selectedNode, list;
13848 if (s.isBackwards()) {
13849 // swap variables
13850 anode = [fnode, fnode = anode][0];
13853 if (wysihtml.dom.domNode(fnode).is.emptyTextNode(true) && fnode) {
13854 fnode = wysihtml.dom.domNode(fnode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
13856 if (wysihtml.dom.domNode(anode).is.emptyTextNode(true) && anode) {
13857 anode = wysihtml.dom.domNode(anode).next({nodeTypes: [1,3], ignoreBlankTexts: true});
13860 if (anode && fnode) {
13861 if (anode === fnode) {
13862 selectedNode = anode;
13863 } else {
13864 selectedNode = wysihtml.dom.domNode(anode).commonAncestor(fnode, composer.element);
13866 } else {
13867 selectedNode = composer.selection.getSelectedNode();
13870 list = findListEl(selectedNode, nodeName, composer);
13872 if (!list.el) {
13873 if (composer.commands.support(cmd)) {
13874 doc.execCommand(cmd, false, null);
13875 } else {
13876 createListFallback(nodeName, composer);
13878 } else if (list.other) {
13879 handleOtherTypeList(list.el, nodeName, composer);
13880 } else {
13881 handleSameTypeList(list.el, nodeName, composer);
13883 },
13885 state: function(composer, command, nodeName) {
13886 var selectedNode = composer.selection.getSelectedNode(),
13887 list = findListEl(selectedNode, nodeName, composer);
13889 return (list.el && !list.other) ? list.el : false;
13891 };
13893 })(wysihtml);
13895 (function(wysihtml){
13897 wysihtml.commands.outdentList = {
13898 exec: function(composer, command, value) {
13899 var listEls = composer.selection.getSelectionParentsByTag('LI');
13900 if (listEls) {
13901 return this.tryToPullLiLevel(listEls, composer);
13903 return false;
13904 },
13906 state: function(composer, command) {
13907 return false;
13908 },
13910 tryToPullLiLevel: function(liNodes, composer) {
13911 var listNode, outerListNode, outerLiNode, list, prevLi, liNode, afterList,
13912 found = false,
13913 that = this;
13915 composer.selection.executeAndRestoreRangy(function() {
13917 for (var i = liNodes.length; i--;) {
13918 liNode = liNodes[i];
13919 if (liNode.parentNode) {
13920 listNode = liNode.parentNode;
13922 if (listNode.tagName === 'OL' || listNode.tagName === 'UL') {
13923 found = true;
13925 outerListNode = wysihtml.dom.getParentElement(listNode.parentNode, { query: 'ol, ul' }, false, composer.element);
13926 outerLiNode = wysihtml.dom.getParentElement(listNode.parentNode, { query: 'li' }, false, composer.element);
13928 if (outerListNode && outerLiNode) {
13930 if (liNode.nextSibling) {
13931 afterList = that.getAfterList(listNode, liNode);
13932 liNode.appendChild(afterList);
13934 outerListNode.insertBefore(liNode, outerLiNode.nextSibling);
13936 } else {
13938 if (liNode.nextSibling) {
13939 afterList = that.getAfterList(listNode, liNode);
13940 liNode.appendChild(afterList);
13943 for (var j = liNode.childNodes.length; j--;) {
13944 listNode.parentNode.insertBefore(liNode.childNodes[j], listNode.nextSibling);
13947 listNode.parentNode.insertBefore(document.createElement('br'), listNode.nextSibling);
13948 liNode.parentNode.removeChild(liNode);
13952 // cleanup
13953 if (listNode.childNodes.length === 0) {
13954 listNode.parentNode.removeChild(listNode);
13960 });
13961 return found;
13962 },
13964 getAfterList: function(listNode, liNode) {
13965 var nodeName = listNode.nodeName,
13966 newList = document.createElement(nodeName);
13968 while (liNode.nextSibling) {
13969 newList.appendChild(liNode.nextSibling);
13971 return newList;
13974 };
13975 }(wysihtml));
13977 (function(wysihtml){
13978 wysihtml.commands.redo = {
13979 exec: function(composer) {
13980 return composer.undoManager.redo();
13981 },
13983 state: function(composer) {
13984 return false;
13986 };
13987 }(wysihtml));
13989 (function(wysihtml) {
13991 var nodeOptions = {
13992 nodeName: "A"
13993 };
13995 wysihtml.commands.removeLink = {
13996 exec: function(composer, command) {
13997 wysihtml.commands.formatInline.remove(composer, command, nodeOptions);
13998 },
14000 state: function(composer, command) {
14001 return wysihtml.commands.formatInline.state(composer, command, nodeOptions);
14003 };
14005 })(wysihtml);
14007 (function(wysihtml){
14008 wysihtml.commands.undo = {
14009 exec: function(composer) {
14010 return composer.undoManager.undo();
14011 },
14013 state: function(composer) {
14014 return false;
14016 };
14017 }(wysihtml));
14019 /**
14020 * Undo Manager for wysihtml
14021 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface
14022 */
14023 (function(wysihtml) {
14024 var Z_KEY = 90,
14025 Y_KEY = 89,
14026 BACKSPACE_KEY = 8,
14027 DELETE_KEY = 46,
14028 MAX_HISTORY_ENTRIES = 25,
14029 DATA_ATTR_NODE = "data-wysihtml-selection-node",
14030 DATA_ATTR_OFFSET = "data-wysihtml-selection-offset",
14031 UNDO_HTML = '<span id="_wysihtml-undo" class="_wysihtml-temp">' + wysihtml.INVISIBLE_SPACE + '</span>',
14032 REDO_HTML = '<span id="_wysihtml-redo" class="_wysihtml-temp">' + wysihtml.INVISIBLE_SPACE + '</span>',
14033 dom = wysihtml.dom;
14035 function cleanTempElements(doc) {
14036 var tempElement;
14037 while (tempElement = doc.querySelector("._wysihtml-temp")) {
14038 tempElement.parentNode.removeChild(tempElement);
14042 wysihtml.UndoManager = wysihtml.lang.Dispatcher.extend(
14043 /** @scope wysihtml.UndoManager.prototype */ {
14044 constructor: function(editor) {
14045 this.editor = editor;
14046 this.composer = editor.composer;
14047 this.element = this.composer.element;
14049 this.position = 0;
14050 this.historyStr = [];
14051 this.historyDom = [];
14053 this.transact();
14055 this._observe();
14056 },
14058 _observe: function() {
14059 var that = this,
14060 doc = this.composer.sandbox.getDocument(),
14061 lastKey;
14063 // Catch CTRL+Z and CTRL+Y
14064 dom.observe(this.element, "keydown", function(event) {
14065 if (event.altKey || (!event.ctrlKey && !event.metaKey)) {
14066 return;
14069 var keyCode = event.keyCode,
14070 isUndo = keyCode === Z_KEY && !event.shiftKey,
14071 isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY);
14073 if (isUndo) {
14074 that.undo();
14075 event.preventDefault();
14076 } else if (isRedo) {
14077 that.redo();
14078 event.preventDefault();
14080 });
14082 // Catch delete and backspace
14083 dom.observe(this.element, "keydown", function(event) {
14084 var keyCode = event.keyCode;
14085 if (keyCode === lastKey) {
14086 return;
14089 lastKey = keyCode;
14091 if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) {
14092 that.transact();
14094 });
14096 this.editor
14097 .on("newword:composer", function() {
14098 that.transact();
14099 })
14101 .on("beforecommand:composer", function() {
14102 that.transact();
14103 });
14104 },
14106 transact: function() {
14107 var previousHtml = this.historyStr[this.position - 1],
14108 currentHtml = this.composer.getValue(false, false),
14109 composerIsVisible = this.element.offsetWidth > 0 && this.element.offsetHeight > 0,
14110 range, node, offset, element, position;
14112 if (currentHtml === previousHtml) {
14113 return;
14116 var length = this.historyStr.length = this.historyDom.length = this.position;
14117 if (length > MAX_HISTORY_ENTRIES) {
14118 this.historyStr.shift();
14119 this.historyDom.shift();
14120 this.position--;
14123 this.position++;
14125 if (composerIsVisible) {
14126 // Do not start saving selection if composer is not visible
14127 range = this.composer.selection.getRange();
14128 node = (range && range.startContainer) ? range.startContainer : this.element;
14129 offset = (range && range.startOffset) ? range.startOffset : 0;
14131 if (node.nodeType === wysihtml.ELEMENT_NODE) {
14132 element = node;
14133 } else {
14134 element = node.parentNode;
14135 position = this.getChildNodeIndex(element, node);
14138 element.setAttribute(DATA_ATTR_OFFSET, offset);
14139 if (typeof(position) !== "undefined") {
14140 element.setAttribute(DATA_ATTR_NODE, position);
14144 var clone = this.element.cloneNode(!!currentHtml);
14145 this.historyDom.push(clone);
14146 this.historyStr.push(currentHtml);
14148 if (element) {
14149 element.removeAttribute(DATA_ATTR_OFFSET);
14150 element.removeAttribute(DATA_ATTR_NODE);
14153 },
14155 undo: function() {
14156 this.transact();
14158 if (!this.undoPossible()) {
14159 return;
14162 this.set(this.historyDom[--this.position - 1]);
14163 this.editor.fire("undo:composer");
14164 },
14166 redo: function() {
14167 if (!this.redoPossible()) {
14168 return;
14171 this.set(this.historyDom[++this.position - 1]);
14172 this.editor.fire("redo:composer");
14173 },
14175 undoPossible: function() {
14176 return this.position > 1;
14177 },
14179 redoPossible: function() {
14180 return this.position < this.historyStr.length;
14181 },
14183 set: function(historyEntry) {
14184 this.element.innerHTML = "";
14186 var i = 0,
14187 childNodes = historyEntry.childNodes,
14188 length = historyEntry.childNodes.length;
14190 for (; i<length; i++) {
14191 this.element.appendChild(childNodes[i].cloneNode(true));
14194 // Restore selection
14195 var offset,
14196 node,
14197 position;
14199 if (historyEntry.hasAttribute(DATA_ATTR_OFFSET)) {
14200 offset = historyEntry.getAttribute(DATA_ATTR_OFFSET);
14201 position = historyEntry.getAttribute(DATA_ATTR_NODE);
14202 node = this.element;
14203 } else {
14204 node = this.element.querySelector("[" + DATA_ATTR_OFFSET + "]") || this.element;
14205 offset = node.getAttribute(DATA_ATTR_OFFSET);
14206 position = node.getAttribute(DATA_ATTR_NODE);
14207 node.removeAttribute(DATA_ATTR_OFFSET);
14208 node.removeAttribute(DATA_ATTR_NODE);
14211 if (position !== null) {
14212 node = this.getChildNodeByIndex(node, +position);
14215 this.composer.selection.set(node, offset);
14216 },
14218 getChildNodeIndex: function(parent, child) {
14219 var i = 0,
14220 childNodes = parent.childNodes,
14221 length = childNodes.length;
14222 for (; i<length; i++) {
14223 if (childNodes[i] === child) {
14224 return i;
14227 },
14229 getChildNodeByIndex: function(parent, index) {
14230 return parent.childNodes[index];
14232 });
14233 })(wysihtml);
14235 /**
14236 * TODO: the following methods still need unit test coverage
14237 */
14238 wysihtml.views.View = Base.extend(
14239 /** @scope wysihtml.views.View.prototype */ {
14240 constructor: function(parent, textareaElement, config) {
14241 this.parent = parent;
14242 this.element = textareaElement;
14243 this.config = config;
14244 if (!this.config.noTextarea) {
14245 this._observeViewChange();
14247 },
14249 _observeViewChange: function() {
14250 var that = this;
14251 this.parent.on("beforeload", function() {
14252 that.parent.on("change_view", function(view) {
14253 if (view === that.name) {
14254 that.parent.currentView = that;
14255 that.show();
14256 // Using tiny delay here to make sure that the placeholder is set before focusing
14257 setTimeout(function() { that.focus(); }, 0);
14258 } else {
14259 that.hide();
14261 });
14262 });
14263 },
14265 focus: function() {
14266 if (this.element && this.element.ownerDocument && this.element.ownerDocument.querySelector(":focus") === this.element) {
14267 return;
14270 try { if(this.element) { this.element.focus(); } } catch(e) {}
14271 },
14273 hide: function() {
14274 this.element.style.display = "none";
14275 },
14277 show: function() {
14278 this.element.style.display = "";
14279 },
14281 disable: function() {
14282 this.element.setAttribute("disabled", "disabled");
14283 },
14285 enable: function() {
14286 this.element.removeAttribute("disabled");
14288 });
14290 (function(wysihtml) {
14291 var dom = wysihtml.dom,
14292 browser = wysihtml.browser;
14294 wysihtml.views.Composer = wysihtml.views.View.extend(
14295 /** @scope wysihtml.views.Composer.prototype */ {
14296 name: "composer",
14298 constructor: function(parent, editableElement, config) {
14299 this.base(parent, editableElement, config);
14300 if (!this.config.noTextarea) {
14301 this.textarea = this.parent.textarea;
14302 } else {
14303 this.editableArea = editableElement;
14305 if (this.config.contentEditableMode) {
14306 this._initContentEditableArea();
14307 } else {
14308 this._initSandbox();
14310 },
14312 clear: function() {
14313 this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : "<br>";
14314 },
14316 getValue: function(parse, clearInternals) {
14317 var value = this.isEmpty() ? "" : wysihtml.quirks.getCorrectInnerHTML(this.element);
14318 if (parse !== false) {
14319 value = this.parent.parse(value, (clearInternals === false) ? false : true);
14321 return value;
14322 },
14324 setValue: function(html, parse) {
14325 if (parse !== false) {
14326 html = this.parent.parse(html);
14329 try {
14330 this.element.innerHTML = html;
14331 } catch (e) {
14332 this.element.innerText = html;
14334 },
14336 cleanUp: function(rules) {
14337 var bookmark;
14338 if (this.selection && this.selection.isInThisEditable()) {
14339 bookmark = rangy.saveSelection(this.win);
14341 this.parent.parse(this.element, undefined, rules);
14342 if (bookmark) {
14343 rangy.restoreSelection(bookmark);
14345 },
14347 show: function() {
14348 this.editableArea.style.display = this._displayStyle || "";
14350 if (!this.config.noTextarea && !this.textarea.element.disabled) {
14351 // Firefox needs this, otherwise contentEditable becomes uneditable
14352 this.disable();
14353 this.enable();
14355 },
14357 hide: function() {
14358 this._displayStyle = dom.getStyle("display").from(this.editableArea);
14359 if (this._displayStyle === "none") {
14360 this._displayStyle = null;
14362 this.editableArea.style.display = "none";
14363 },
14365 disable: function() {
14366 this.parent.fire("disable:composer");
14367 this.element.removeAttribute("contentEditable");
14368 },
14370 enable: function() {
14371 this.parent.fire("enable:composer");
14372 this.element.setAttribute("contentEditable", "true");
14373 },
14375 focus: function(setToEnd) {
14376 // IE 8 fires the focus event after .focus()
14377 // This is needed by our simulate_placeholder.js to work
14378 // therefore we clear it ourselves this time
14379 if (wysihtml.browser.doesAsyncFocus() && this.hasPlaceholderSet()) {
14380 this.clear();
14383 this.base();
14385 var lastChild = this.element.lastChild;
14386 if (setToEnd && lastChild && this.selection) {
14387 if (lastChild.nodeName === "BR") {
14388 this.selection.setBefore(this.element.lastChild);
14389 } else {
14390 this.selection.setAfter(this.element.lastChild);
14393 },
14395 getScrollPos: function() {
14396 if (this.doc && this.win) {
14397 var pos = {};
14399 if (typeof this.win.pageYOffset !== "undefined") {
14400 pos.y = this.win.pageYOffset;
14401 } else {
14402 pos.y = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollTop;
14405 if (typeof this.win.pageXOffset !== "undefined") {
14406 pos.x = this.win.pageXOffset;
14407 } else {
14408 pos.x = (this.doc.documentElement || this.doc.body.parentNode || this.doc.body).scrollLeft;
14411 return pos;
14413 },
14415 setScrollPos: function(pos) {
14416 if (pos && typeof pos.x !== "undefined" && typeof pos.y !== "undefined") {
14417 this.win.scrollTo(pos.x, pos.y);
14419 },
14421 getTextContent: function() {
14422 return dom.getTextContent(this.element);
14423 },
14425 hasPlaceholderSet: function() {
14426 return this.getTextContent() == ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")) && this.placeholderSet;
14427 },
14429 isEmpty: function() {
14430 var innerHTML = this.element.innerHTML.toLowerCase();
14431 return (/^(\s|<br>|<\/br>|<p>|<\/p>)*$/i).test(innerHTML) ||
14432 innerHTML === "" ||
14433 innerHTML === "<br>" ||
14434 innerHTML === "<p></p>" ||
14435 innerHTML === "<p><br></p>" ||
14436 this.hasPlaceholderSet();
14437 },
14439 _initContentEditableArea: function() {
14440 var that = this;
14441 if (this.config.noTextarea) {
14442 this.sandbox = new dom.ContentEditableArea(function() {
14443 that._create();
14444 }, {
14445 className: this.config.classNames.sandbox
14446 }, this.editableArea);
14447 } else {
14448 this.sandbox = new dom.ContentEditableArea(function() {
14449 that._create();
14450 }, {
14451 className: this.config.classNames.sandbox
14452 });
14453 this.editableArea = this.sandbox.getContentEditable();
14454 dom.insert(this.editableArea).after(this.textarea.element);
14455 this._createWysiwygFormField();
14457 },
14459 _initSandbox: function() {
14460 var that = this;
14461 this.sandbox = new dom.Sandbox(function() {
14462 that._create();
14463 }, {
14464 stylesheets: this.config.stylesheets,
14465 className: this.config.classNames.sandbox
14466 });
14467 this.editableArea = this.sandbox.getIframe();
14469 var textareaElement = this.textarea.element;
14470 dom.insert(this.editableArea).after(textareaElement);
14472 this._createWysiwygFormField();
14473 },
14475 // Creates hidden field which tells the server after submit, that the user used an wysiwyg editor
14476 _createWysiwygFormField: function() {
14477 if (this.textarea.element.form) {
14478 var hiddenField = document.createElement("input");
14479 hiddenField.type = "hidden";
14480 hiddenField.name = "_wysihtml_mode";
14481 hiddenField.value = 1;
14482 dom.insert(hiddenField).after(this.textarea.element);
14484 },
14486 _create: function() {
14487 var that = this;
14488 this.doc = this.sandbox.getDocument();
14489 this.win = this.sandbox.getWindow();
14490 this.element = (this.config.contentEditableMode) ? this.sandbox.getContentEditable() : this.doc.body;
14491 if (!this.config.noTextarea) {
14492 this.textarea = this.parent.textarea;
14493 this.element.innerHTML = this.textarea.getValue(true, false);
14494 } else {
14495 this.cleanUp(); // cleans contenteditable on initiation as it may contain html
14498 // Make sure our selection handler is ready
14499 this.selection = new wysihtml.Selection(this.parent, this.element, this.config.classNames.uneditableContainer);
14501 // Make sure commands dispatcher is ready
14502 this.commands = new wysihtml.Commands(this.parent);
14504 if (!this.config.noTextarea) {
14505 dom.copyAttributes([
14506 "className", "spellcheck", "title", "lang", "dir", "accessKey"
14507 ]).from(this.textarea.element).to(this.element);
14510 this._initAutoLinking();
14512 dom.addClass(this.element, this.config.classNames.composer);
14513 //
14514 // Make the editor look like the original textarea, by syncing styles
14515 if (this.config.style && !this.config.contentEditableMode) {
14516 this.style();
14519 this.observe();
14521 var name = this.config.name;
14522 if (name) {
14523 dom.addClass(this.element, name);
14524 if (!this.config.contentEditableMode) { dom.addClass(this.editableArea, name); }
14527 this.enable();
14529 if (!this.config.noTextarea && this.textarea.element.disabled) {
14530 this.disable();
14533 // Simulate html5 placeholder attribute on contentEditable element
14534 var placeholderText = typeof(this.config.placeholder) === "string"
14535 ? this.config.placeholder
14536 : ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder"));
14537 if (placeholderText) {
14538 dom.simulatePlaceholder(this.parent, this, placeholderText, this.config.classNames.placeholder);
14541 // Make sure that the browser avoids using inline styles whenever possible
14542 this.commands.exec("styleWithCSS", false);
14544 this._initObjectResizing();
14545 this._initUndoManager();
14546 this._initLineBreaking();
14548 // Simulate html5 autofocus on contentEditable element
14549 // This doesn't work on IOS (5.1.1)
14550 if (!this.config.noTextarea && (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) && !browser.isIos()) {
14551 setTimeout(function() { that.focus(true); }, 100);
14554 // IE sometimes leaves a single paragraph, which can't be removed by the user
14555 if (!browser.clearsContentEditableCorrectly()) {
14556 wysihtml.quirks.ensureProperClearing(this);
14559 // Set up a sync that makes sure that textarea and editor have the same content
14560 if (this.initSync && this.config.sync) {
14561 this.initSync();
14564 // Okay hide the textarea, we are ready to go
14565 if (!this.config.noTextarea) { this.textarea.hide(); }
14567 // Fire global (before-)load event
14568 this.parent.fire("beforeload").fire("load");
14569 },
14571 _initAutoLinking: function() {
14572 var that = this,
14573 supportsDisablingOfAutoLinking = browser.canDisableAutoLinking(),
14574 supportsAutoLinking = browser.doesAutoLinkingInContentEditable();
14576 if (supportsDisablingOfAutoLinking) {
14577 this.commands.exec("AutoUrlDetect", false, false);
14580 if (!this.config.autoLink) {
14581 return;
14584 // Only do the auto linking by ourselves when the browser doesn't support auto linking
14585 // OR when he supports auto linking but we were able to turn it off (IE9+)
14586 if (!supportsAutoLinking || (supportsAutoLinking && supportsDisablingOfAutoLinking)) {
14587 this.parent.on("newword:composer", function() {
14588 if (dom.getTextContent(that.element).match(dom.autoLink.URL_REG_EXP)) {
14589 var nodeWithSelection = that.selection.getSelectedNode(),
14590 uneditables = that.element.querySelectorAll("." + that.config.classNames.uneditableContainer),
14591 isInUneditable = false;
14593 for (var i = uneditables.length; i--;) {
14594 if (wysihtml.dom.contains(uneditables[i], nodeWithSelection)) {
14595 isInUneditable = true;
14599 if (!isInUneditable) dom.autoLink(nodeWithSelection, [that.config.classNames.uneditableContainer]);
14601 });
14603 dom.observe(this.element, "blur", function() {
14604 dom.autoLink(that.element, [that.config.classNames.uneditableContainer]);
14605 });
14608 // Assuming we have the following:
14609 // <a href="http://www.google.de">http://www.google.de</a>
14610 // If a user now changes the url in the innerHTML we want to make sure that
14611 // it's synchronized with the href attribute (as long as the innerHTML is still a url)
14612 var // Use a live NodeList to check whether there are any links in the document
14613 links = this.sandbox.getDocument().getElementsByTagName("a"),
14614 // The autoLink helper method reveals a reg exp to detect correct urls
14615 urlRegExp = dom.autoLink.URL_REG_EXP,
14616 getTextContent = function(element) {
14617 var textContent = wysihtml.lang.string(dom.getTextContent(element)).trim();
14618 if (textContent.substr(0, 4) === "www.") {
14619 textContent = "http://" + textContent;
14621 return textContent;
14622 };
14624 dom.observe(this.element, "keydown", function(event) {
14625 if (!links.length) {
14626 return;
14629 var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument),
14630 link = dom.getParentElement(selectedNode, { query: "a" }, 4),
14631 textContent;
14633 if (!link) {
14634 return;
14637 textContent = getTextContent(link);
14638 // keydown is fired before the actual content is changed
14639 // therefore we set a timeout to change the href
14640 setTimeout(function() {
14641 var newTextContent = getTextContent(link);
14642 if (newTextContent === textContent) {
14643 return;
14646 // Only set href when new href looks like a valid url
14647 if (newTextContent.match(urlRegExp)) {
14648 link.setAttribute("href", newTextContent);
14650 }, 0);
14651 });
14652 },
14654 _initObjectResizing: function() {
14655 this.commands.exec("enableObjectResizing", true);
14657 // IE sets inline styles after resizing objects
14658 // The following lines make sure that the width/height css properties
14659 // are copied over to the width/height attributes
14660 if (browser.supportsEvent("resizeend")) {
14661 var properties = ["width", "height"],
14662 propertiesLength = properties.length,
14663 element = this.element;
14665 dom.observe(element, "resizeend", function(event) {
14666 var target = event.target || event.srcElement,
14667 style = target.style,
14668 i = 0,
14669 property;
14671 if (target.nodeName !== "IMG") {
14672 return;
14675 for (; i<propertiesLength; i++) {
14676 property = properties[i];
14677 if (style[property]) {
14678 target.setAttribute(property, parseInt(style[property], 10));
14679 style[property] = "";
14683 // After resizing IE sometimes forgets to remove the old resize handles
14684 wysihtml.quirks.redraw(element);
14685 });
14687 },
14689 _initUndoManager: function() {
14690 this.undoManager = new wysihtml.UndoManager(this.parent);
14691 },
14693 _initLineBreaking: function() {
14694 var that = this,
14695 USE_NATIVE_LINE_BREAK_INSIDE_TAGS = "li, p, h1, h2, h3, h4, h5, h6",
14696 LIST_TAGS = "ul, ol, menu";
14698 function adjust(selectedNode) {
14699 var parentElement = dom.getParentElement(selectedNode, { query: "p, div" }, 2);
14700 if (parentElement && dom.contains(that.element, parentElement)) {
14701 that.selection.executeAndRestoreRangy(function() {
14702 if (that.config.useLineBreaks) {
14703 if (!parentElement.firstChild || (parentElement.firstChild === parentElement.lastChild && parentElement.firstChild.nodeType === 1 && parentElement.firstChild.classList.contains('rangySelectionBoundary'))) {
14704 parentElement.appendChild(that.doc.createElement('br'));
14706 dom.replaceWithChildNodes(parentElement);
14707 } else if (parentElement.nodeName !== "P") {
14708 dom.renameElement(parentElement, "p");
14710 });
14714 // Ensures when editor is empty and not line breaks mode, the inital state has a paragraph in it on focus with caret inside paragraph
14715 if (!this.config.useLineBreaks) {
14716 dom.observe(this.element, ["focus"], function() {
14717 if (that.isEmpty()) {
14718 setTimeout(function() {
14719 var paragraph = that.doc.createElement("P");
14720 that.element.innerHTML = "";
14721 that.element.appendChild(paragraph);
14722 if (!browser.displaysCaretInEmptyContentEditableCorrectly()) {
14723 paragraph.innerHTML = "<br>";
14724 that.selection.setBefore(paragraph.firstChild);
14725 } else {
14726 that.selection.selectNode(paragraph, true);
14728 }, 0);
14730 });
14733 dom.observe(this.element, "keydown", function(event) {
14734 var keyCode = event.keyCode;
14736 if (event.shiftKey || event.ctrlKey || event.defaultPrevented) {
14737 return;
14740 if (keyCode !== wysihtml.ENTER_KEY && keyCode !== wysihtml.BACKSPACE_KEY) {
14741 return;
14743 var blockElement = dom.getParentElement(that.selection.getSelectedNode(), { query: USE_NATIVE_LINE_BREAK_INSIDE_TAGS }, 4);
14744 if (blockElement) {
14745 setTimeout(function() {
14746 // Unwrap paragraph after leaving a list or a H1-6
14747 var selectedNode = that.selection.getSelectedNode(),
14748 list;
14750 if (blockElement.nodeName === "LI") {
14751 if (!selectedNode) {
14752 return;
14755 list = dom.getParentElement(selectedNode, { query: LIST_TAGS }, 2);
14757 if (!list) {
14758 adjust(selectedNode);
14762 if (keyCode === wysihtml.ENTER_KEY && blockElement.nodeName.match(/^H[1-6]$/)) {
14763 adjust(selectedNode);
14765 }, 0);
14766 return;
14768 if (that.config.useLineBreaks && keyCode === wysihtml.ENTER_KEY && !wysihtml.browser.insertsLineBreaksOnReturn()) {
14769 event.preventDefault();
14770 that.commands.exec("insertLineBreak");
14772 });
14774 });
14775 })(wysihtml);
14777 (function(wysihtml) {
14778 var dom = wysihtml.dom,
14779 doc = document,
14780 win = window,
14781 HOST_TEMPLATE = doc.createElement("div"),
14782 /**
14783 * Styles to copy from textarea to the composer element
14784 */
14785 TEXT_FORMATTING = [
14786 "background-color",
14787 "color", "cursor",
14788 "font-family", "font-size", "font-style", "font-variant", "font-weight",
14789 "line-height", "letter-spacing",
14790 "text-align", "text-decoration", "text-indent", "text-rendering",
14791 "word-break", "word-wrap", "word-spacing"
14792 ],
14793 /**
14794 * Styles to copy from textarea to the iframe
14795 */
14796 BOX_FORMATTING = [
14797 "background-color",
14798 "border-collapse",
14799 "border-bottom-color", "border-bottom-style", "border-bottom-width",
14800 "border-left-color", "border-left-style", "border-left-width",
14801 "border-right-color", "border-right-style", "border-right-width",
14802 "border-top-color", "border-top-style", "border-top-width",
14803 "clear", "display", "float",
14804 "margin-bottom", "margin-left", "margin-right", "margin-top",
14805 "outline-color", "outline-offset", "outline-width", "outline-style",
14806 "padding-left", "padding-right", "padding-top", "padding-bottom",
14807 "position", "top", "left", "right", "bottom", "z-index",
14808 "vertical-align", "text-align",
14809 "-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing",
14810 "-webkit-box-shadow", "-moz-box-shadow", "-ms-box-shadow","box-shadow",
14811 "-webkit-border-top-right-radius", "-moz-border-radius-topright", "border-top-right-radius",
14812 "-webkit-border-bottom-right-radius", "-moz-border-radius-bottomright", "border-bottom-right-radius",
14813 "-webkit-border-bottom-left-radius", "-moz-border-radius-bottomleft", "border-bottom-left-radius",
14814 "-webkit-border-top-left-radius", "-moz-border-radius-topleft", "border-top-left-radius",
14815 "width", "height"
14816 ],
14817 ADDITIONAL_CSS_RULES = [
14818 "html { height: 100%; }",
14819 "body { height: 100%; padding: 1px 0 0 0; margin: -1px 0 0 0; }",
14820 "body > p:first-child { margin-top: 0; }",
14821 "._wysihtml-temp { display: none; }",
14822 wysihtml.browser.isGecko ?
14823 "body.placeholder { color: graytext !important; }" :
14824 "body.placeholder { color: #a9a9a9 !important; }",
14825 // Ensure that user see's broken images and can delete them
14826 "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }"
14827 ];
14829 /**
14830 * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
14831 * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
14833 * Other browsers need a more hacky way: (pssst don't tell my mama)
14834 * In order to prevent the element being scrolled into view when focusing it, we simply
14835 * move it out of the scrollable area, focus it, and reset it's position
14836 */
14837 var focusWithoutScrolling = function(element) {
14838 if (element.setActive) {
14839 // Following line could cause a js error when the textarea is invisible
14840 // See https://github.com/xing/wysihtml5/issues/9
14841 try { element.setActive(); } catch(e) {}
14842 } else {
14843 var elementStyle = element.style,
14844 originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
14845 originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
14846 originalStyles = {
14847 position: elementStyle.position,
14848 top: elementStyle.top,
14849 left: elementStyle.left,
14850 WebkitUserSelect: elementStyle.WebkitUserSelect
14851 };
14853 dom.setStyles({
14854 position: "absolute",
14855 top: "-99999px",
14856 left: "-99999px",
14857 // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
14858 WebkitUserSelect: "none"
14859 }).on(element);
14861 element.focus();
14863 dom.setStyles(originalStyles).on(element);
14865 if (win.scrollTo) {
14866 // Some browser extensions unset this method to prevent annoyances
14867 // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
14868 // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
14869 win.scrollTo(originalScrollLeft, originalScrollTop);
14872 };
14875 wysihtml.views.Composer.prototype.style = function() {
14876 var that = this,
14877 originalActiveElement = doc.querySelector(":focus"),
14878 textareaElement = this.textarea.element,
14879 hasPlaceholder = textareaElement.hasAttribute("placeholder"),
14880 originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"),
14881 originalDisplayValue = textareaElement.style.display,
14882 originalDisabled = textareaElement.disabled,
14883 displayValueForCopying;
14885 this.focusStylesHost = HOST_TEMPLATE.cloneNode(false);
14886 this.blurStylesHost = HOST_TEMPLATE.cloneNode(false);
14887 this.disabledStylesHost = HOST_TEMPLATE.cloneNode(false);
14889 // Remove placeholder before copying (as the placeholder has an affect on the computed style)
14890 if (hasPlaceholder) {
14891 textareaElement.removeAttribute("placeholder");
14894 if (textareaElement === originalActiveElement) {
14895 textareaElement.blur();
14898 // enable for copying styles
14899 textareaElement.disabled = false;
14901 // set textarea to display="none" to get cascaded styles via getComputedStyle
14902 textareaElement.style.display = displayValueForCopying = "none";
14904 if ((textareaElement.getAttribute("rows") && dom.getStyle("height").from(textareaElement) === "auto") ||
14905 (textareaElement.getAttribute("cols") && dom.getStyle("width").from(textareaElement) === "auto")) {
14906 textareaElement.style.display = displayValueForCopying = originalDisplayValue;
14909 // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) ---------
14910 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.editableArea).andTo(this.blurStylesHost);
14912 // --------- editor styles ---------
14913 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost);
14915 // --------- apply standard rules ---------
14916 dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument);
14918 // --------- :disabled styles ---------
14919 textareaElement.disabled = true;
14920 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
14921 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.disabledStylesHost);
14922 textareaElement.disabled = originalDisabled;
14924 // --------- :focus styles ---------
14925 textareaElement.style.display = originalDisplayValue;
14926 focusWithoutScrolling(textareaElement);
14927 textareaElement.style.display = displayValueForCopying;
14929 dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost);
14930 dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost);
14932 // reset textarea
14933 textareaElement.style.display = originalDisplayValue;
14935 dom.copyStyles(["display"]).from(textareaElement).to(this.editableArea);
14937 // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus
14938 // this is needed for when the change_view event is fired where the iframe is hidden and then
14939 // the blur event fires and re-displays it
14940 var boxFormattingStyles = wysihtml.lang.array(BOX_FORMATTING).without(["display"]);
14942 // --------- restore focus ---------
14943 if (originalActiveElement) {
14944 focusWithoutScrolling(originalActiveElement);
14945 } else {
14946 textareaElement.blur();
14949 // --------- restore placeholder ---------
14950 if (hasPlaceholder) {
14951 textareaElement.setAttribute("placeholder", originalPlaceholder);
14954 // --------- Sync focus/blur styles ---------
14955 this.parent.on("focus:composer", function() {
14956 dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.editableArea);
14957 dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element);
14958 });
14960 this.parent.on("blur:composer", function() {
14961 dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
14962 dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element);
14963 });
14965 this.parent.observe("disable:composer", function() {
14966 dom.copyStyles(boxFormattingStyles) .from(that.disabledStylesHost).to(that.editableArea);
14967 dom.copyStyles(TEXT_FORMATTING) .from(that.disabledStylesHost).to(that.element);
14968 });
14970 this.parent.observe("enable:composer", function() {
14971 dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea);
14972 dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element);
14973 });
14975 return this;
14976 };
14977 })(wysihtml);
14979 /**
14980 * Taking care of events
14981 * - Simulating 'change' event on contentEditable element
14982 * - Handling drag & drop logic
14983 * - Catch paste events
14984 * - Dispatch proprietary newword:composer event
14985 * - Keyboard shortcuts
14986 */
14987 (function(wysihtml) {
14988 var dom = wysihtml.dom,
14989 domNode = dom.domNode,
14990 browser = wysihtml.browser,
14991 /**
14992 * Map keyCodes to query commands
14993 */
14994 shortcuts = {
14995 "66": "bold", // B
14996 "73": "italic", // I
14997 "85": "underline" // U
14998 };
15000 var actions = {
15002 // Adds multiple eventlisteners to target, bound to one callback
15003 // TODO: If needed elsewhere make it part of wysihtml.dom or sth
15004 addListeners: function (target, events, callback) {
15005 for(var i = 0, max = events.length; i < max; i++) {
15006 target.addEventListener(events[i], callback, false);
15008 },
15010 // Removes multiple eventlisteners from target, bound to one callback
15011 // TODO: If needed elsewhere make it part of wysihtml.dom or sth
15012 removeListeners: function (target, events, callback) {
15013 for(var i = 0, max = events.length; i < max; i++) {
15014 target.removeEventListener(events[i], callback, false);
15016 },
15018 // Override for giving user ability to delete last line break in table cell
15019 fixLastBrDeletionInTable: function(composer, force) {
15020 if (composer.selection.caretIsInTheEndOfNode()) {
15021 var sel = composer.selection.getSelection(),
15022 aNode = sel.anchorNode;
15023 if (aNode && aNode.nodeType === 1 && (wysihtml.dom.getParentElement(aNode, {query: 'td, th'}, false, composer.element) || force)) {
15024 var nextNode = aNode.childNodes[sel.anchorOffset];
15025 if (nextNode && nextNode.nodeType === 1 & nextNode.nodeName === "BR") {
15026 nextNode.parentNode.removeChild(nextNode);
15027 return true;
15031 return false;
15032 },
15034 // If found an uneditable before caret then notify it before deletion
15035 handleUneditableDeletion: function(composer) {
15036 var before = composer.selection.getBeforeSelection(true);
15037 if (before && (before.type === "element" || before.type === "leafnode") && before.node.nodeType === 1 && before.node.classList.contains(composer.config.classNames.uneditableContainer)) {
15038 if (actions.fixLastBrDeletionInTable(composer, true)) {
15039 return true;
15041 try {
15042 var ev = new CustomEvent("wysihtml:uneditable:delete", {bubbles: true, cancelable: false});
15043 before.node.dispatchEvent(ev);
15044 } catch (err) {}
15045 before.node.parentNode.removeChild(before.node);
15046 return true;
15048 return false;
15049 },
15051 // Deletion with caret in the beginning of headings and other block elvel elements needs special attention
15052 // Not allways does it concate text to previous block node correctly (browsers do unexpected miracles here especially webkit)
15053 fixDeleteInTheBeginningOfBlock: function(composer) {
15054 var selection = composer.selection,
15055 prevNode = selection.getPreviousNode();
15057 if (selection.caretIsFirstInSelection(wysihtml.browser.usesControlRanges()) && prevNode) {
15058 if (prevNode.nodeType === 1 &&
15059 wysihtml.dom.domNode(prevNode).is.block() &&
15060 !domNode(prevNode).test({
15061 query: "ol, ul, table, tr, dl"
15062 })
15063 ) {
15064 if ((/^\s*$/).test(prevNode.textContent || prevNode.innerText)) {
15065 // If heading is empty remove the heading node
15066 prevNode.parentNode.removeChild(prevNode);
15067 return true;
15068 } else {
15069 if (prevNode.lastChild) {
15070 var selNode = prevNode.lastChild,
15071 selectedNode = selection.getSelectedNode(),
15072 commonAncestorNode = domNode(prevNode).commonAncestor(selectedNode, composer.element),
15073 curNode = wysihtml.dom.getParentElement(selectedNode, {
15074 query: "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote"
15075 }, false, commonAncestorNode || composer.element);
15077 if (curNode) {
15078 domNode(curNode).transferContentTo(prevNode, true);
15079 selection.setAfter(selNode);
15080 return true;
15081 } else if (wysihtml.browser.usesControlRanges()) {
15082 selectedNode = selection.getCaretNode();
15083 domNode(selectedNode).transferContentTo(prevNode, true);
15084 selection.setAfter(selNode);
15085 return true;
15091 return false;
15092 },
15094 /* In IE when deleting with caret at the begining of LI, list gets broken into half instead of merging the LI with previous */
15095 /* This does not match other browsers an is less intuitive from UI standpoint, thus has to be fixed */
15096 fixDeleteInTheBeginningOfLi: function(composer) {
15097 if (wysihtml.browser.hasLiDeletingProblem()) {
15098 var selection = composer.selection.getSelection(),
15099 aNode = selection.anchorNode,
15100 listNode, prevNode, firstNode,
15101 isInBeginnig = composer.selection.caretIsFirstInSelection(),
15102 prevNode,
15103 intermediaryNode;
15105 // Fix caret at the beginnig of first textNode in LI
15106 if (aNode.nodeType === 3 && selection.anchorOffset === 0 && aNode === aNode.parentNode.firstChild) {
15107 aNode = aNode.parentNode;
15108 isInBeginnig = true;
15111 if (isInBeginnig && aNode && aNode.nodeType === 1 && aNode.nodeName === "LI") {
15112 prevNode = domNode(aNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
15113 if (!prevNode && aNode.parentNode && (aNode.parentNode.nodeName === "UL" || aNode.parentNode.nodeName === "OL")) {
15114 prevNode = domNode(aNode.parentNode).prev({nodeTypes: [1,3], ignoreBlankTexts: true});
15115 intermediaryNode = aNode.parentNode;
15117 if (prevNode) {
15118 firstNode = aNode.firstChild;
15119 domNode(aNode).transferContentTo(prevNode, true);
15121 if (intermediaryNode && intermediaryNode.children.length === 0){
15122 intermediaryNode.remove();
15125 if (firstNode) {
15126 composer.selection.setBefore(firstNode);
15127 } else if (prevNode) {
15128 if (prevNode.nodeType === 1) {
15129 if (prevNode.lastChild) {
15130 composer.selection.setAfter(prevNode.lastChild);
15131 } else {
15132 composer.selection.selectNode(prevNode);
15134 } else {
15135 composer.selection.setAfter(prevNode);
15138 return true;
15142 return false;
15143 },
15145 fixDeleteInTheBeginningOfControlSelection: function(composer) {
15146 var selection = composer.selection,
15147 prevNode = selection.getPreviousNode(),
15148 selectedNode = selection.getSelectedNode(),
15149 afterCaretNode;
15151 if (selection.caretIsFirstInSelection()) {
15152 if (selectedNode.nodeType === 3) {
15153 selectedNode = selectedNode.parentNode;
15155 afterCaretNode = selectedNode.firstChild;
15156 domNode(selectedNode).transferContentTo(prevNode, true);
15157 if (afterCaretNode) {
15158 composer.selection.setBefore(afterCaretNode);
15160 return true;
15162 return false;
15163 },
15165 // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature)
15166 // Returns true if some corrections is applied so events know when to prevent default
15167 doLineBreaksModeEnterWithCaret: function(composer) {
15168 var breakNodes = "p, pre, div, blockquote",
15169 caretInfo, parent, txtNode,
15170 ret = false;
15172 caretInfo = composer.selection.getNodesNearCaret();
15173 if (caretInfo) {
15175 if (caretInfo.caretNode || caretInfo.nextNode) {
15176 parent = dom.getParentElement(caretInfo.caretNode || caretInfo.nextNode, { query: breakNodes }, 2);
15177 if (parent === composer.element) {
15178 parent = undefined;
15182 if (parent && caretInfo.caretNode) {
15183 if (domNode(caretInfo.caretNode).is.lineBreak()) {
15185 if (composer.config.doubleLineBreakEscapesBlock) {
15186 // Double enter (enter on blank line) exits block element in useLineBreaks mode.
15187 ret = true;
15188 caretInfo.caretNode.parentNode.removeChild(caretInfo.caretNode);
15190 // Ensure surplous line breaks are not added to preceding element
15191 if (domNode(caretInfo.nextNode).is.lineBreak()) {
15192 caretInfo.nextNode.parentNode.removeChild(caretInfo.nextNode);
15195 var brNode = composer.doc.createElement('br');
15196 if (domNode(caretInfo.nextNode).is.lineBreak() && caretInfo.nextNode === parent.lastChild) {
15197 parent.parentNode.insertBefore(brNode, parent.nextSibling);
15198 } else {
15199 composer.selection.splitElementAtCaret(parent, brNode);
15202 // Ensure surplous blank lines are not added to preceding element
15203 if (caretInfo.nextNode && caretInfo.nextNode.nodeType === 3) {
15204 // Replaces blank lines at the beginning of textnode
15205 caretInfo.nextNode.data = caretInfo.nextNode.data.replace(/^ *[\r\n]+/, '');
15207 composer.selection.setBefore(brNode);
15210 } else if (caretInfo.caretNode.nodeType === 3 && wysihtml.browser.hasCaretBlockElementIssue() && caretInfo.textOffset === caretInfo.caretNode.data.length && !caretInfo.nextNode) {
15212 // This fixes annoying webkit issue when you press enter at the end of a block then seemingly nothing happens.
15213 // in reality one line break is generated and cursor is reported after it, but when entering something cursor jumps before the br
15214 ret = true;
15215 var br1 = composer.doc.createElement('br'),
15216 br2 = composer.doc.createElement('br'),
15217 f = composer.doc.createDocumentFragment();
15218 f.appendChild(br1);
15219 f.appendChild(br2);
15220 composer.selection.insertNode(f);
15221 composer.selection.setBefore(br2);
15226 return ret;
15228 };
15230 var handleDeleteKeyPress = function(event, composer) {
15231 var selection = composer.selection,
15232 element = composer.element;
15234 if (selection.isCollapsed()) {
15235 /**
15236 * when the editor is empty in useLineBreaks = false mode, preserve
15237 * the default value in it which is <p><br></p>
15238 */
15239 if (composer.isEmpty() && !composer.config.useLineBreaks) {
15240 event.preventDefault();
15241 return;
15243 if (actions.handleUneditableDeletion(composer)) {
15244 event.preventDefault();
15245 return;
15247 if (actions.fixDeleteInTheBeginningOfLi(composer)) {
15248 event.preventDefault();
15249 return;
15251 if (actions.fixDeleteInTheBeginningOfBlock(composer)) {
15252 event.preventDefault();
15253 return;
15255 if (actions.fixLastBrDeletionInTable(composer)) {
15256 event.preventDefault();
15257 return;
15259 if (wysihtml.browser.usesControlRanges()) {
15260 if (actions.fixDeleteInTheBeginningOfControlSelection(composer)) {
15261 event.preventDefault();
15262 return;
15265 } else {
15266 if (selection.containsUneditable()) {
15267 event.preventDefault();
15268 selection.deleteContents();
15271 };
15273 var handleEnterKeyPress = function(event, composer) {
15274 if (composer.config.useLineBreaks && !event.shiftKey && !event.ctrlKey) {
15275 // Fixes some misbehaviours of enters in linebreaks mode (natively a bit unsupported feature)
15277 var breakNodes = "p, pre, div, blockquote",
15278 caretInfo, parent, txtNode;
15280 if (composer.selection.isCollapsed()) {
15281 if (actions.doLineBreaksModeEnterWithCaret(composer)) {
15282 event.preventDefault();
15287 if (browser.hasCaretAtLinkEndInsertionProblems() && composer.selection.caretIsInTheEndOfNode()) {
15288 var target = composer.selection.getSelectedNode(true),
15289 targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
15290 invisibleSpace, space;
15292 if (targetEl && targetEl.closest('a') && target.nodeType === 3 && target === targetEl.lastChild) {
15293 // Seems like enter was pressed and caret was at the end of link node
15294 // This means user wants to escape the link now (caret is last in link node too).
15295 composer.selection.setAfter(targetEl);
15298 };
15300 var handleTabKeyDown = function(composer, element, shiftKey) {
15301 if (!composer.selection.isCollapsed()) {
15302 composer.selection.deleteContents();
15303 } else if (composer.selection.caretIsInTheBeginnig('li')) {
15304 if (shiftKey) {
15305 if (composer.commands.exec('outdentList')) return;
15306 } else {
15307 if (composer.commands.exec('indentList')) return;
15311 // Is &emsp; close enough to tab. Could not find enough counter arguments for now.
15312 composer.commands.exec("insertHTML", "&emsp;");
15313 };
15315 var handleDomNodeRemoved = function(event) {
15316 if (this.domNodeRemovedInterval) {
15317 clearInterval(domNodeRemovedInterval);
15319 this.parent.fire("destroy:composer");
15320 };
15322 // Listens to "drop", "paste", "mouseup", "focus", "keyup" events and fires
15323 var handleUserInteraction = function (event) {
15324 this.parent.fire("beforeinteraction", event).fire("beforeinteraction:composer", event);
15325 setTimeout((function() {
15326 this.parent.fire("interaction", event).fire("interaction:composer", event);
15327 }).bind(this), 0);
15328 };
15330 var handleFocus = function(event) {
15331 this.parent.fire("focus", event).fire("focus:composer", event);
15333 // Delay storing of state until all focus handler are fired
15334 // especially the one which resets the placeholder
15335 setTimeout((function() {
15336 this.focusState = this.getValue(false, false);
15337 }).bind(this), 0);
15338 };
15340 var handleBlur = function(event) {
15341 if (this.focusState !== this.getValue(false, false)) {
15342 //create change event if supported (all except IE8)
15343 var changeevent = event;
15344 if(typeof Object.create == 'function') {
15345 changeevent = Object.create(event, { type: { value: 'change' } });
15347 this.parent.fire("change", changeevent).fire("change:composer", changeevent);
15349 this.parent.fire("blur", event).fire("blur:composer", event);
15350 };
15352 var handlePaste = function(event) {
15353 this.parent.fire(event.type, event).fire(event.type + ":composer", event);
15354 if (event.type === "paste") {
15355 setTimeout((function() {
15356 this.parent.fire("newword:composer");
15357 }).bind(this), 0);
15359 };
15361 var handleCopy = function(event) {
15362 if (this.config.copyedFromMarking) {
15363 // If supported the copied source can be based directly on selection
15364 // 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.
15365 if (wysihtml.browser.supportsModernPaste()) {
15366 event.clipboardData.setData("text/html", this.config.copyedFromMarking + this.selection.getHtml());
15367 event.clipboardData.setData("text/plain", this.selection.getPlainText());
15368 event.preventDefault();
15370 this.parent.fire(event.type, event).fire(event.type + ":composer", event);
15372 };
15374 var handleKeyUp = function(event) {
15375 var keyCode = event.keyCode;
15376 if (keyCode === wysihtml.SPACE_KEY || keyCode === wysihtml.ENTER_KEY) {
15377 this.parent.fire("newword:composer");
15379 };
15381 var handleMouseDown = function(event) {
15382 if (!browser.canSelectImagesInContentEditable()) {
15383 // Make sure that images are selected when clicking on them
15384 var target = event.target,
15385 allImages = this.element.querySelectorAll('img'),
15386 notMyImages = this.element.querySelectorAll('.' + this.config.classNames.uneditableContainer + ' img'),
15387 myImages = wysihtml.lang.array(allImages).without(notMyImages);
15389 if (target.nodeName === "IMG" && wysihtml.lang.array(myImages).contains(target)) {
15390 this.selection.selectNode(target);
15394 // Saves mousedown position for IE controlSelect fix
15395 if (wysihtml.browser.usesControlRanges()) {
15396 this.selection.lastMouseDownPos = {x: event.clientX, y: event.clientY};
15397 setTimeout(function() {
15398 delete this.selection.lastMouseDownPos;
15399 }.bind(this), 0);
15401 };
15403 // IE has this madness of control selects of overflowed and some other elements (weird box around element on selection and second click selects text)
15404 // This fix handles the second click problem by adding cursor to the right position under cursor inside when controlSelection is made
15405 var handleIEControlSelect = function(event) {
15406 var target = event.target,
15407 pos = this.selection.lastMouseDownPos;
15408 if (pos) {
15409 var caretPosition = document.body.createTextRange();
15410 setTimeout(function() {
15411 try {
15412 caretPosition.moveToPoint(pos.x, pos.y);
15413 caretPosition.select();
15414 } catch (e) {}
15415 }.bind(this), 0);
15417 };
15419 var handleClick = function(event) {
15420 if (this.config.classNames.uneditableContainer) {
15421 // If uneditables is configured, makes clicking on uneditable move caret after clicked element (so it can be deleted like text)
15422 // If uneditable needs text selection itself event.stopPropagation can be used to prevent this behaviour
15423 var uneditable = wysihtml.dom.getParentElement(event.target, { query: "." + this.config.classNames.uneditableContainer }, false, this.element);
15424 if (uneditable) {
15425 this.selection.setAfter(uneditable);
15428 };
15430 var handleDrop = function(event) {
15431 if (!browser.canSelectImagesInContentEditable()) {
15432 // TODO: if I knew how to get dropped elements list from event I could limit it to only IMG element case
15433 setTimeout((function() {
15434 this.selection.getSelection().removeAllRanges();
15435 }).bind(this), 0);
15437 };
15439 var handleKeyDown = function(event) {
15440 var keyCode = event.keyCode,
15441 command = shortcuts[keyCode],
15442 target = this.selection.getSelectedNode(true),
15443 targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
15444 parent;
15446 // Select all (meta/ctrl + a)
15447 if ((event.ctrlKey || event.metaKey) && !event.altKey && keyCode === 65) {
15448 this.selection.selectAll();
15449 event.preventDefault();
15450 return;
15453 // Shortcut logic
15454 if ((event.ctrlKey || event.metaKey) && !event.altKey && command) {
15455 this.commands.exec(command);
15456 event.preventDefault();
15459 if (keyCode === wysihtml.BACKSPACE_KEY) {
15460 // Delete key override for special cases
15461 handleDeleteKeyPress(event, this);
15464 // Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor
15465 if (keyCode === wysihtml.BACKSPACE_KEY || keyCode === wysihtml.DELETE_KEY) {
15466 if (target && target.nodeName === "IMG") {
15467 event.preventDefault();
15468 parent = target.parentNode;
15469 parent.removeChild(target);// delete the <img>
15470 // And it's parent <a> too if it hasn't got any other child nodes
15471 if (parent.nodeName === "A" && !parent.firstChild) {
15472 parent.parentNode.removeChild(parent);
15474 setTimeout((function() {
15475 wysihtml.quirks.redraw(this.element);
15476 }).bind(this), 0);
15480 if (this.config.handleTabKey && keyCode === wysihtml.TAB_KEY) {
15481 // TAB key handling
15482 event.preventDefault();
15483 handleTabKeyDown(this, this.element, event.shiftKey);
15486 if (keyCode === wysihtml.ENTER_KEY) {
15487 handleEnterKeyPress(event, this);
15490 };
15492 var handleKeyPress = function(event) {
15494 // This block should run only if some character is inserted (nor command keys like delete, backspace, enter, etc.)
15495 if (event.which !== 0) {
15497 // Test if caret is last in a link in webkit and try to fix webkit problem,
15498 // that all inserted content is added outside of link.
15499 // This issue was added as a not thought through fix for getting caret after link in contenteditable if it is last in editable area.
15500 // Allthough it fixes this minor case it actually introduces a cascade of problems when editing links.
15501 // The standard approachi in other wysiwygs seems as a step backwards - introducing a separate modal for managing links content text.
15502 // I find it to be too big of a tradeoff in terms of expected simple UI flow, thus trying to fight against it.
15503 // Also adds link escaping by double space with caret at the end of link for all browsers
15505 if (this.selection.caretIsInTheEndOfNode()) {
15506 var target = this.selection.getSelectedNode(true),
15507 targetEl = (target && target.nodeType === 3) ? target.parentNode : target, // target guaranteed to be an Element
15508 invisibleSpace, space;
15510 if (targetEl && targetEl.closest('a') && target === targetEl.lastChild) {
15512 if (event.which !== 32 || this.selection.caretIsInTheEndOfNode(true) && browser.hasCaretAtLinkEndInsertionProblems()) {
15513 // Executed if there is no whitespace before caret in textnode in case of pressing space.
15514 // Whitespace before marks that user wants to escape the node by pressing double space.
15515 // Otherwise insert the character in the link not out as it would like to go natively
15517 invisibleSpace = this.doc.createTextNode(wysihtml.INVISIBLE_SPACE);
15518 this.selection.insertNode(invisibleSpace);
15519 this.selection.setBefore(invisibleSpace);
15520 setTimeout(function() {
15522 if (invisibleSpace.textContent.length > 1) {
15523 invisibleSpace.textContent = invisibleSpace.textContent.replace(wysihtml.INVISIBLE_SPACE_REG_EXP, '');
15524 this.selection.setAfter(invisibleSpace);
15525 } else {
15526 invisibleSpace.remove();
15529 }.bind(this), 0);
15530 } else if (event.which === 32) {
15531 // Seems like space was pressed and there was a space before the caret allready
15532 // This means user wants to escape the link now (caret is last in link node too) so we let the native browser do it-s job and escape.
15533 // But lets move the trailing space too out of link if present
15535 if (target.nodeType === 3 && (/[\u00A0 ]$/).test(target.textContent)) {
15537 target.textContent = target.textContent.replace(/[\u00A0 ]$/, '');
15538 space = this.doc.createTextNode(' ');
15539 targetEl.parentNode.insertBefore(space, targetEl.nextSibling);
15540 this.selection.setAfter(space, false);
15541 event.preventDefault();
15550 var handleIframeFocus = function(event) {
15551 setTimeout((function() {
15552 if (this.doc.querySelector(":focus") !== this.element) {
15553 this.focus();
15555 }).bind(this), 0);
15556 };
15558 var handleIframeBlur = function(event) {
15559 setTimeout((function() {
15560 this.selection.getSelection().removeAllRanges();
15561 }).bind(this), 0);
15562 };
15564 // Testing requires actions to be accessible from out of scope
15565 wysihtml.views.Composer.prototype.observeActions = actions;
15567 wysihtml.views.Composer.prototype.observe = function() {
15568 var that = this,
15569 container = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(),
15570 element = this.element,
15571 focusBlurElement = (browser.supportsEventsInIframeCorrectly() || this.sandbox.getContentEditable) ? this.element : this.sandbox.getWindow();
15573 this.focusState = this.getValue(false, false);
15574 this.actions = actions;
15576 // --------- destroy:composer event ---------
15577 container.addEventListener(["DOMNodeRemoved"], handleDomNodeRemoved.bind(this), false);
15579 // DOMNodeRemoved event is not supported in IE 8
15580 // TODO: try to figure out a polyfill style fix, so it could be transferred to polyfills and removed if ie8 is not needed
15581 if (!browser.supportsMutationEvents()) {
15582 this.domNodeRemovedInterval = setInterval(function() {
15583 if (!dom.contains(document.documentElement, container)) {
15584 handleDomNodeRemoved.call(this);
15586 }, 250);
15589 actions.addListeners(focusBlurElement, ['drop', 'paste', 'mouseup', 'focus', 'keyup'], handleUserInteraction.bind(this));
15590 focusBlurElement.addEventListener('focus', handleFocus.bind(this), false);
15591 focusBlurElement.addEventListener('blur', handleBlur.bind(this), false);
15593 actions.addListeners(this.element, ['drop', 'paste', 'beforepaste'], handlePaste.bind(this), false);
15594 this.element.addEventListener('copy', handleCopy.bind(this), false);
15595 this.element.addEventListener('mousedown', handleMouseDown.bind(this), false);
15596 this.element.addEventListener('click', handleClick.bind(this), false);
15597 this.element.addEventListener('drop', handleDrop.bind(this), false);
15598 this.element.addEventListener('keyup', handleKeyUp.bind(this), false);
15599 this.element.addEventListener('keydown', handleKeyDown.bind(this), false);
15600 this.element.addEventListener('keypress', handleKeyPress.bind(this), false);
15602 // IE controlselect madness fix
15603 if (wysihtml.browser.usesControlRanges()) {
15604 this.element.addEventListener('mscontrolselect', handleIEControlSelect.bind(this), false);
15607 this.element.addEventListener("dragenter", (function() {
15608 this.parent.fire("unset_placeholder");
15609 }).bind(this), false);
15611 };
15612 })(wysihtml);
15614 /**
15615 * Class that takes care that the value of the composer and the textarea is always in sync
15616 */
15617 (function(wysihtml) {
15618 var INTERVAL = 400;
15620 wysihtml.views.Synchronizer = Base.extend(
15621 /** @scope wysihtml.views.Synchronizer.prototype */ {
15623 constructor: function(editor, textarea, composer) {
15624 this.editor = editor;
15625 this.textarea = textarea;
15626 this.composer = composer;
15628 this._observe();
15629 },
15631 /**
15632 * Sync html from composer to textarea
15633 * Takes care of placeholders
15634 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the textarea
15635 */
15636 fromComposerToTextarea: function(shouldParseHtml) {
15637 this.textarea.setValue(wysihtml.lang.string(this.composer.getValue(false, false)).trim(), shouldParseHtml);
15638 },
15640 /**
15641 * Sync value of textarea to composer
15642 * Takes care of placeholders
15643 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer
15644 */
15645 fromTextareaToComposer: function(shouldParseHtml) {
15646 var textareaValue = this.textarea.getValue(false, false);
15647 if (textareaValue) {
15648 this.composer.setValue(textareaValue, shouldParseHtml);
15649 } else {
15650 this.composer.clear();
15651 this.editor.fire("set_placeholder");
15653 },
15655 /**
15656 * Invoke syncing based on view state
15657 * @param {Boolean} shouldParseHtml Whether the html should be sanitized before inserting it into the composer/textarea
15658 */
15659 sync: function(shouldParseHtml) {
15660 if (this.editor.currentView.name === "textarea") {
15661 this.fromTextareaToComposer(shouldParseHtml);
15662 } else {
15663 this.fromComposerToTextarea(shouldParseHtml);
15665 },
15667 /**
15668 * Initializes interval-based syncing
15669 * also makes sure that on-submit the composer's content is synced with the textarea
15670 * immediately when the form gets submitted
15671 */
15672 _observe: function() {
15673 var interval,
15674 that = this,
15675 form = this.textarea.element.form,
15676 startInterval = function() {
15677 interval = setInterval(function() { that.fromComposerToTextarea(); }, INTERVAL);
15678 },
15679 stopInterval = function() {
15680 clearInterval(interval);
15681 interval = null;
15682 };
15684 startInterval();
15686 if (form) {
15687 // If the textarea is in a form make sure that after onreset and onsubmit the composer
15688 // has the correct state
15689 wysihtml.dom.observe(form, "submit", function() {
15690 that.sync(true);
15691 });
15692 wysihtml.dom.observe(form, "reset", function() {
15693 setTimeout(function() { that.fromTextareaToComposer(); }, 0);
15694 });
15697 this.editor.on("change_view", function(view) {
15698 if (view === "composer" && !interval) {
15699 that.fromTextareaToComposer(true);
15700 startInterval();
15701 } else if (view === "textarea") {
15702 that.fromComposerToTextarea(true);
15703 stopInterval();
15705 });
15707 this.editor.on("destroy:composer", stopInterval);
15709 });
15710 })(wysihtml);
15712 (function(wysihtml) {
15714 wysihtml.views.SourceView = Base.extend(
15715 /** @scope wysihtml.views.SourceView.prototype */ {
15717 constructor: function(editor, composer) {
15718 this.editor = editor;
15719 this.composer = composer;
15721 this._observe();
15722 },
15724 switchToTextarea: function(shouldParseHtml) {
15725 var composerStyles = this.composer.win.getComputedStyle(this.composer.element),
15726 width = parseFloat(composerStyles.width),
15727 height = Math.max(parseFloat(composerStyles.height), 100);
15729 if (!this.textarea) {
15730 this.textarea = this.composer.doc.createElement('textarea');
15731 this.textarea.className = "wysihtml-source-view";
15733 this.textarea.style.width = width + 'px';
15734 this.textarea.style.height = height + 'px';
15735 this.textarea.value = this.editor.getValue(shouldParseHtml, true);
15736 this.composer.element.parentNode.insertBefore(this.textarea, this.composer.element);
15737 this.editor.currentView = "source";
15738 this.composer.element.style.display = 'none';
15739 },
15741 switchToComposer: function(shouldParseHtml) {
15742 var textareaValue = this.textarea.value;
15743 if (textareaValue) {
15744 this.composer.setValue(textareaValue, shouldParseHtml);
15745 } else {
15746 this.composer.clear();
15747 this.editor.fire("set_placeholder");
15749 this.textarea.parentNode.removeChild(this.textarea);
15750 this.editor.currentView = this.composer;
15751 this.composer.element.style.display = '';
15752 },
15754 _observe: function() {
15755 this.editor.on("change_view", function(view) {
15756 if (view === "composer") {
15757 this.switchToComposer(true);
15758 } else if (view === "textarea") {
15759 this.switchToTextarea(true);
15761 }.bind(this));
15764 });
15766 })(wysihtml);
15768 wysihtml.views.Textarea = wysihtml.views.View.extend(
15769 /** @scope wysihtml.views.Textarea.prototype */ {
15770 name: "textarea",
15772 constructor: function(parent, textareaElement, config) {
15773 this.base(parent, textareaElement, config);
15775 this._observe();
15776 },
15778 clear: function() {
15779 this.element.value = "";
15780 },
15782 getValue: function(parse) {
15783 var value = this.isEmpty() ? "" : this.element.value;
15784 if (parse !== false) {
15785 value = this.parent.parse(value);
15787 return value;
15788 },
15790 setValue: function(html, parse) {
15791 if (parse !== false) {
15792 html = this.parent.parse(html);
15794 this.element.value = html;
15795 },
15797 cleanUp: function(rules) {
15798 var html = this.parent.parse(this.element.value, undefined, rules);
15799 this.element.value = html;
15800 },
15802 hasPlaceholderSet: function() {
15803 var supportsPlaceholder = wysihtml.browser.supportsPlaceholderAttributeOn(this.element),
15804 placeholderText = this.element.getAttribute("placeholder") || null,
15805 value = this.element.value,
15806 isEmpty = !value;
15807 return (supportsPlaceholder && isEmpty) || (value === placeholderText);
15808 },
15810 isEmpty: function() {
15811 return !wysihtml.lang.string(this.element.value).trim() || this.hasPlaceholderSet();
15812 },
15814 _observe: function() {
15815 var element = this.element,
15816 parent = this.parent,
15817 eventMapping = {
15818 focusin: "focus",
15819 focusout: "blur"
15820 },
15821 /**
15822 * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events
15823 * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai
15824 */
15825 events = wysihtml.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"];
15827 parent.on("beforeload", function() {
15828 wysihtml.dom.observe(element, events, function(event) {
15829 var eventName = eventMapping[event.type] || event.type;
15830 parent.fire(eventName).fire(eventName + ":textarea");
15831 });
15833 wysihtml.dom.observe(element, ["paste", "drop"], function() {
15834 setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0);
15835 });
15836 });
15838 });
15840 /**
15841 * WYSIHTML Editor
15843 * @param {Element} editableElement Reference to the textarea which should be turned into a rich text interface
15844 * @param {Object} [config] See defaults object below for explanation of each individual config option
15846 * @events
15847 * load
15848 * beforeload (for internal use only)
15849 * focus
15850 * focus:composer
15851 * focus:textarea
15852 * blur
15853 * blur:composer
15854 * blur:textarea
15855 * change
15856 * change:composer
15857 * change:textarea
15858 * paste
15859 * paste:composer
15860 * paste:textarea
15861 * newword:composer
15862 * destroy:composer
15863 * undo:composer
15864 * redo:composer
15865 * beforecommand:composer
15866 * aftercommand:composer
15867 * enable:composer
15868 * disable:composer
15869 * change_view
15870 */
15871 (function(wysihtml) {
15872 var undef;
15874 wysihtml.Editor = wysihtml.lang.Dispatcher.extend({
15875 /** @scope wysihtml.Editor.prototype */
15876 defaults: {
15877 // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body
15878 name: undef,
15879 // Whether the editor should look like the textarea (by adopting styles)
15880 style: true,
15881 // Whether urls, entered by the user should automatically become clickable-links
15882 autoLink: true,
15883 // Tab key inserts tab into text as default behaviour. It can be disabled to regain keyboard navigation
15884 handleTabKey: true,
15885 // Object which includes parser rules to apply when html gets cleaned
15886 // See parser_rules/*.js for examples
15887 parserRules: { tags: { br: {}, span: {}, div: {}, p: {}, b: {}, i: {}, u: {} }, classes: {} },
15888 // Object which includes parser when the user inserts content via copy & paste. If null parserRules will be used instead
15889 pasteParserRulesets: null,
15890 // Parser method to use when the user inserts content
15891 parser: wysihtml.dom.parse,
15892 // By default wysihtml will insert a <br> for line breaks, set this to false to use <p>
15893 useLineBreaks: true,
15894 // Double enter (enter on blank line) exits block element in useLineBreaks mode.
15895 // It enables a way of escaping out of block elements and splitting block elements
15896 doubleLineBreakEscapesBlock: true,
15897 // Array (or single string) of stylesheet urls to be loaded in the editor's iframe
15898 stylesheets: [],
15899 // Placeholder text to use, defaults to the placeholder attribute on the textarea element
15900 placeholderText: undef,
15901 // Whether the rich text editor should be rendered on touch devices (wysihtml >= 0.3.0 comes with basic support for iOS 5)
15902 supportTouchDevices: true,
15903 // Whether senseless <span> elements (empty or without attributes) should be removed/replaced with their content
15904 cleanUp: true,
15905 // Whether to use div instead of secure iframe
15906 contentEditableMode: false,
15907 classNames: {
15908 // Class name which should be set on the contentEditable element in the created sandbox iframe, can be styled via the 'stylesheets' option
15909 composer: "wysihtml-editor",
15910 // Class name to add to the body when the wysihtml editor is supported
15911 body: "wysihtml-supported",
15912 // classname added to editable area element (iframe/div) on creation
15913 sandbox: "wysihtml-sandbox",
15914 // class on editable area with placeholder
15915 placeholder: "wysihtml-placeholder",
15916 // Classname of container that editor should not touch and pass through
15917 uneditableContainer: "wysihtml-uneditable-container"
15918 },
15919 // Browsers that support copied source handling will get a marking of the origin of the copied source (for determinig code cleanup rules on paste)
15920 // Also copied source is based directly on selection -
15921 // (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).
15922 // If falsy value is passed source override is also disabled
15923 copyedFromMarking: '<meta name="copied-from" content="wysihtml">'
15924 },
15926 constructor: function(editableElement, config) {
15927 this.editableElement = typeof(editableElement) === "string" ? document.getElementById(editableElement) : editableElement;
15928 this.config = wysihtml.lang.object({}).merge(this.defaults).merge(config).get();
15929 this._isCompatible = wysihtml.browser.supported();
15931 // merge classNames
15932 if (config && config.classNames) {
15933 wysihtml.lang.object(this.config.classNames).merge(config.classNames);
15936 if (this.editableElement.nodeName.toLowerCase() != "textarea") {
15937 this.config.contentEditableMode = true;
15938 this.config.noTextarea = true;
15940 if (!this.config.noTextarea) {
15941 this.textarea = new wysihtml.views.Textarea(this, this.editableElement, this.config);
15942 this.currentView = this.textarea;
15945 // Sort out unsupported/unwanted browsers here
15946 if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml.browser.isTouchDevice())) {
15947 var that = this;
15948 setTimeout(function() { that.fire("beforeload").fire("load"); }, 0);
15949 return;
15952 // Add class name to body, to indicate that the editor is supported
15953 wysihtml.dom.addClass(document.body, this.config.classNames.body);
15955 this.composer = new wysihtml.views.Composer(this, this.editableElement, this.config);
15956 this.currentView = this.composer;
15958 if (typeof(this.config.parser) === "function") {
15959 this._initParser();
15962 this.on("beforeload", this.handleBeforeLoad);
15963 },
15965 handleBeforeLoad: function() {
15966 if (!this.config.noTextarea) {
15967 this.synchronizer = new wysihtml.views.Synchronizer(this, this.textarea, this.composer);
15968 } else {
15969 this.sourceView = new wysihtml.views.SourceView(this, this.composer);
15971 this.runEditorExtenders();
15972 },
15974 runEditorExtenders: function() {
15975 wysihtml.editorExtenders.forEach(function(extender) {
15976 extender(this);
15977 }.bind(this));
15978 },
15980 isCompatible: function() {
15981 return this._isCompatible;
15982 },
15984 clear: function() {
15985 this.currentView.clear();
15986 return this;
15987 },
15989 getValue: function(parse, clearInternals) {
15990 return this.currentView.getValue(parse, clearInternals);
15991 },
15993 setValue: function(html, parse) {
15994 this.fire("unset_placeholder");
15996 if (!html) {
15997 return this.clear();
16000 this.currentView.setValue(html, parse);
16001 return this;
16002 },
16004 cleanUp: function(rules) {
16005 this.currentView.cleanUp(rules);
16006 },
16008 focus: function(setToEnd) {
16009 this.currentView.focus(setToEnd);
16010 return this;
16011 },
16013 /**
16014 * Deactivate editor (make it readonly)
16015 */
16016 disable: function() {
16017 this.currentView.disable();
16018 return this;
16019 },
16021 /**
16022 * Activate editor
16023 */
16024 enable: function() {
16025 this.currentView.enable();
16026 return this;
16027 },
16029 isEmpty: function() {
16030 return this.currentView.isEmpty();
16031 },
16033 hasPlaceholderSet: function() {
16034 return this.currentView.hasPlaceholderSet();
16035 },
16037 destroy: function() {
16038 if (this.composer && this.composer.sandbox) {
16039 this.composer.sandbox.destroy();
16041 this.fire("destroy:composer");
16042 this.off();
16043 },
16045 parse: function(htmlOrElement, clearInternals, customRules) {
16046 var parseContext = (this.config.contentEditableMode) ? document : ((this.composer) ? this.composer.sandbox.getDocument() : null);
16047 var returnValue = this.config.parser(htmlOrElement, {
16048 "rules": customRules || this.config.parserRules,
16049 "cleanUp": this.config.cleanUp,
16050 "context": parseContext,
16051 "uneditableClass": this.config.classNames.uneditableContainer,
16052 "clearInternals" : clearInternals
16053 });
16054 if (typeof(htmlOrElement) === "object") {
16055 wysihtml.quirks.redraw(htmlOrElement);
16057 return returnValue;
16058 },
16060 /**
16061 * Prepare html parser logic
16062 * - Observes for paste and drop
16063 */
16064 _initParser: function() {
16065 var oldHtml;
16067 if (wysihtml.browser.supportsModernPaste()) {
16068 this.on("paste:composer", function(event) {
16069 event.preventDefault();
16070 oldHtml = wysihtml.dom.getPastedHtml(event);
16071 if (oldHtml) {
16072 this._cleanAndPaste(oldHtml);
16074 }.bind(this));
16076 } else {
16077 this.on("beforepaste:composer", function(event) {
16078 event.preventDefault();
16079 var scrollPos = this.composer.getScrollPos();
16081 wysihtml.dom.getPastedHtmlWithDiv(this.composer, function(pastedHTML) {
16082 if (pastedHTML) {
16083 this._cleanAndPaste(pastedHTML);
16085 this.composer.setScrollPos(scrollPos);
16086 }.bind(this));
16088 }.bind(this));
16090 },
16092 _cleanAndPaste: function (oldHtml) {
16093 var cleanHtml = wysihtml.quirks.cleanPastedHTML(oldHtml, {
16094 "referenceNode": this.composer.element,
16095 "rules": this.config.pasteParserRulesets || [{"set": this.config.parserRules}],
16096 "uneditableClass": this.config.classNames.uneditableContainer
16097 });
16098 this.composer.selection.deleteContents();
16099 this.composer.selection.insertHTML(cleanHtml);
16101 });
16102 })(wysihtml);

Impressum / About Us