vendor/assets/javascripts/wysihtml5x.js in wysihtml5x-rails-0.4.0 vs vendor/assets/javascripts/wysihtml5x.js in wysihtml5x-rails-0.4.1

- old
+ new

@@ -1,41 +1,42 @@ /** - * @license wysihtml5x v0.4.0 + * @license wysihtml5x v0.4.1 * https://github.com/Edicy/wysihtml5 * * Author: Christopher Blum (https://github.com/tiff) * Secondary author of extended features: Oliver Pulges (https://github.com/pulges) * * Copyright (C) 2012 XING AG * Licensed under the MIT license (MIT) * */ var wysihtml5 = { - version: "0.4.0", - + version: "0.4.1", + // namespaces commands: {}, dom: {}, quirks: {}, toolbar: {}, lang: {}, selection: {}, views: {}, - + INVISIBLE_SPACE: "\uFEFF", - + EMPTY_FUNCTION: function() {}, - + ELEMENT_NODE: 1, TEXT_NODE: 3, - + BACKSPACE_KEY: 8, ENTER_KEY: 13, ESCAPE_KEY: 27, SPACE_KEY: 32, DELETE_KEY: 46 -};/* +}; +/* Rangy, a cross-browser JavaScript range and selection library http://code.google.com/p/rangy/ Copyright 2012, Tim Down Licensed under the MIT license. @@ -125,147 +126,147 @@ h=c.getBody(h).createControlRange();for(var D,G=false,P=0,X=d.length;P<X;++P){D=d.item(P);if(D!==b||G)h.add(d.item(P));else G=true}h.select();n(this)}else ba(this,b)}:function(b){ba(this,b)};var aa;if(!J&&W&&l.features.implementsDomRange){aa=function(b){var d=false;if(b.anchorNode)d=c.comparePoints(b.anchorNode,b.anchorOffset,b.focusNode,b.focusOffset)==1;return d};g.isBackwards=function(){return aa(this)}}else aa=g.isBackwards=function(){return false};g.toString=function(){for(var b=[],d=0,h=this.rangeCount;d< h;++d)b[d]=""+this._ranges[d];return b.join("")};g.collapse=function(b,d){q(this,b);var h=l.createRange(c.getDocument(b));h.collapseToPoint(b,d);this.removeAllRanges();this.addRange(h);this.isCollapsed=true};g.collapseToStart=function(){if(this.rangeCount){var b=this._ranges[0];this.collapse(b.startContainer,b.startOffset)}else throw new L("INVALID_STATE_ERR");};g.collapseToEnd=function(){if(this.rangeCount){var b=this._ranges[this.rangeCount-1];this.collapse(b.endContainer,b.endOffset)}else throw new L("INVALID_STATE_ERR"); };g.selectAllChildren=function(b){q(this,b);var d=l.createRange(c.getDocument(b));d.selectNodeContents(b);this.removeAllRanges();this.addRange(d)};g.deleteFromDocument=function(){if(M&&V&&this.docSelection.type=="Control"){for(var b=this.docSelection.createRange(),d;b.length;){d=b.item(0);b.remove(d);d.parentNode.removeChild(d)}this.refresh()}else if(this.rangeCount){b=this.getAllRanges();this.removeAllRanges();d=0;for(var h=b.length;d<h;++d)b[d].deleteContents();this.addRange(b[h-1])}};g.getAllRanges= function(){return this._ranges.slice(0)};g.setSingleRange=function(b){this.setRanges([b])};g.containsNode=function(b,d){for(var h=0,D=this._ranges.length;h<D;++h)if(this._ranges[h].containsNode(b,d))return true;return false};g.toHtml=function(){var b="";if(this.rangeCount){b=k.getRangeDocument(this._ranges[0]).createElement("div");for(var d=0,h=this._ranges.length;d<h;++d)b.appendChild(this._ranges[d].cloneContents());b=b.innerHTML}return b};g.getName=function(){return"WrappedSelection"};g.inspect= function(){return v(this)};g.detach=function(){this.win=this.anchorNode=this.focusNode=this.win._rangySelection=null};x.inspect=v;l.Selection=x;l.selectionPrototype=g;l.addCreateMissingNativeApiListener(function(b){if(typeof b.getSelection=="undefined")b.getSelection=function(){return l.getSelection(this)};b=null})});/* - Base.js, version 1.1a - Copyright 2006-2010, Dean Edwards - License: http://www.opensource.org/licenses/mit-license.php + Base.js, version 1.1a + Copyright 2006-2010, Dean Edwards + License: http://www.opensource.org/licenses/mit-license.php */ var Base = function() { - // dummy + // dummy }; Base.extend = function(_instance, _static) { // subclass - var extend = Base.prototype.extend; - - // build the prototype - Base._prototyping = true; - var proto = new this; - extend.call(proto, _instance); + var extend = Base.prototype.extend; + + // build the prototype + Base._prototyping = true; + var proto = new this; + extend.call(proto, _instance); proto.base = function() { // call this method from any other method to invoke that method's ancestor }; - delete Base._prototyping; - - // create the wrapper for the constructor function - //var constructor = proto.constructor.valueOf(); //-dean - var constructor = proto.constructor; - var klass = proto.constructor = function() { - if (!Base._prototyping) { - if (this._constructing || this.constructor == klass) { // instantiation - this._constructing = true; - constructor.apply(this, arguments); - delete this._constructing; - } else if (arguments[0] != null) { // casting - return (arguments[0].extend || extend).call(arguments[0], proto); - } - } - }; - - // build the class interface - klass.ancestor = this; - klass.extend = this.extend; - klass.forEach = this.forEach; - klass.implement = this.implement; - klass.prototype = proto; - klass.toString = this.toString; - klass.valueOf = function(type) { - //return (type == "object") ? klass : constructor; //-dean - return (type == "object") ? klass : constructor.valueOf(); - }; - extend.call(klass, _static); - // class initialisation - if (typeof klass.init == "function") klass.init(); - return klass; + delete Base._prototyping; + + // create the wrapper for the constructor function + //var constructor = proto.constructor.valueOf(); //-dean + var constructor = proto.constructor; + var klass = proto.constructor = function() { + if (!Base._prototyping) { + if (this._constructing || this.constructor == klass) { // instantiation + this._constructing = true; + constructor.apply(this, arguments); + delete this._constructing; + } else if (arguments[0] != null) { // casting + return (arguments[0].extend || extend).call(arguments[0], proto); + } + } + }; + + // build the class interface + klass.ancestor = this; + klass.extend = this.extend; + klass.forEach = this.forEach; + klass.implement = this.implement; + klass.prototype = proto; + klass.toString = this.toString; + klass.valueOf = function(type) { + //return (type == "object") ? klass : constructor; //-dean + return (type == "object") ? klass : constructor.valueOf(); + }; + extend.call(klass, _static); + // class initialisation + if (typeof klass.init == "function") klass.init(); + return klass; }; -Base.prototype = { - extend: function(source, value) { - if (arguments.length > 1) { // extending with a name/value pair - var ancestor = this[source]; - if (ancestor && (typeof value == "function") && // overriding a method? - // the valueOf() comparison is to avoid circular references - (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) && - /\bbase\b/.test(value)) { - // get the underlying method - var method = value.valueOf(); - // override - value = function() { - var previous = this.base || Base.prototype.base; - this.base = ancestor; - var returnValue = method.apply(this, arguments); - this.base = previous; - return returnValue; - }; - // point to the underlying method - value.valueOf = function(type) { - return (type == "object") ? value : method; - }; - value.toString = Base.toString; - } - this[source] = value; - } else if (source) { // extending with an object literal - var extend = Base.prototype.extend; - // if this object has a customised extend method then use it - if (!Base._prototyping && typeof this != "function") { - extend = this.extend || extend; - } - var proto = {toSource: null}; - // do the "toString" and other methods manually - var hidden = ["constructor", "toString", "valueOf"]; - // if we are prototyping then include the constructor - var i = Base._prototyping ? 0 : 1; - while (key = hidden[i++]) { - if (source[key] != proto[key]) { - extend.call(this, key, source[key]); +Base.prototype = { + extend: function(source, value) { + if (arguments.length > 1) { // extending with a name/value pair + var ancestor = this[source]; + if (ancestor && (typeof value == "function") && // overriding a method? + // the valueOf() comparison is to avoid circular references + (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) && + /\bbase\b/.test(value)) { + // get the underlying method + var method = value.valueOf(); + // override + value = function() { + var previous = this.base || Base.prototype.base; + this.base = ancestor; + var returnValue = method.apply(this, arguments); + this.base = previous; + return returnValue; + }; + // point to the underlying method + value.valueOf = function(type) { + return (type == "object") ? value : method; + }; + value.toString = Base.toString; + } + this[source] = value; + } else if (source) { // extending with an object literal + var extend = Base.prototype.extend; + // if this object has a customised extend method then use it + if (!Base._prototyping && typeof this != "function") { + extend = this.extend || extend; + } + var proto = {toSource: null}; + // do the "toString" and other methods manually + var hidden = ["constructor", "toString", "valueOf"]; + // if we are prototyping then include the constructor + var i = Base._prototyping ? 0 : 1; + while (key = hidden[i++]) { + if (source[key] != proto[key]) { + extend.call(this, key, source[key]); - } - } - // copy each of the source object's properties to this object - for (var key in source) { - if (!proto[key]) extend.call(this, key, source[key]); - } - } - return this; - } + } + } + // copy each of the source object's properties to this object + for (var key in source) { + if (!proto[key]) extend.call(this, key, source[key]); + } + } + return this; + } }; // initialise Base = Base.extend({ - constructor: function() { - this.extend(arguments[0]); - } + constructor: function() { + this.extend(arguments[0]); + } }, { - ancestor: Object, - version: "1.1", - - forEach: function(object, block, context) { - for (var key in object) { - if (this.prototype[key] === undefined) { - block.call(context, object[key], key, object); - } - } - }, - - implement: function() { - for (var i = 0; i < arguments.length; i++) { - if (typeof arguments[i] == "function") { - // if it's a function, call it - arguments[i](this.prototype); - } else { - // add the interface using the extend method - this.prototype.extend(arguments[i]); - } - } - return this; - }, - - toString: function() { - return String(this.valueOf()); - } + ancestor: Object, + version: "1.1", + + forEach: function(object, block, context) { + for (var key in object) { + if (this.prototype[key] === undefined) { + block.call(context, object[key], key, object); + } + } + }, + + implement: function() { + for (var i = 0; i < arguments.length; i++) { + if (typeof arguments[i] == "function") { + // if it's a function, call it + arguments[i](this.prototype); + } else { + // add the interface using the extend method + this.prototype.extend(arguments[i]); + } + } + return this; + }, + + toString: function() { + return String(this.valueOf()); + } });/** * Detect browser support for specific features */ wysihtml5.browser = (function() { var userAgent = navigator.userAgent, @@ -274,23 +275,23 @@ isIE = userAgent.indexOf("MSIE") !== -1 && userAgent.indexOf("Opera") === -1, isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1, isWebKit = userAgent.indexOf("AppleWebKit/") !== -1, isChrome = userAgent.indexOf("Chrome/") !== -1, isOpera = userAgent.indexOf("Opera/") !== -1; - + function iosVersion(userAgent) { return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1]; } - + function androidVersion(userAgent) { return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1]; } - + return { // Static variable needed, publicly accessible, to be able override it in unit tests USER_AGENT: userAgent, - + /** * Exclude browsers that are not capable of displaying and handling * contentEditable as desired: * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable * - IE < 8 create invalid markup and crash randomly from time to time @@ -310,23 +311,23 @@ return hasContentEditableSupport && hasEditingApiSupport && hasQuerySelectorSupport && !isIncompatibleMobileBrowser; }, - + isTouchDevice: function() { return this.supportsEvent("touchmove"); }, - + isIos: function() { return (/ipad|iphone|ipod/i).test(this.USER_AGENT); }, - + isAndroid: function() { return this.USER_AGENT.indexOf("Android") !== -1; }, - + /** * Whether the browser supports sandboxed iframes * Currently only IE 6+ offers such feature <iframe security="restricted"> * * http://msdn.microsoft.com/en-us/library/ms534622(v=vs.85).aspx @@ -361,11 +362,11 @@ * All other browsers provide the computed style in px via window.getComputedStyle */ hasCurrentStyleProperty: function() { return "currentStyle" in testElement; }, - + /** * Firefox on OSX navigates through history when hitting CMD + Arrow right/left */ hasHistoryIssue: function() { return isGecko && navigator.platform.substr(0, 3) === "Mac"; @@ -393,11 +394,11 @@ * Opera doesn't correctly fire focus/blur events when clicking in- and outside of iframe */ supportsEventsInIframeCorrectly: function() { return !isOpera; }, - + /** * Everything below IE9 doesn't know how to treat HTML5 tags * * @param {Object} context The document object on which to check HTML5 support * @@ -432,11 +433,11 @@ // converted into a list (<ul><li>...</li></ul>, <ol><li>...</li></ol>) // IE and Opera act a bit different here as they convert the entire content of the current block element into a list "insertUnorderedList": isIE || isWebKit, "insertOrderedList": isIE || isWebKit }; - + // Firefox throws errors for queryCommandSupported, so we have to build up our own object of supported commands var supported = { "insertHTML": isGecko }; @@ -541,23 +542,23 @@ * See https://developer.mozilla.org/en/DOM/Selection/modify */ supportsSelectionModify: function() { return "getSelection" in window && "modify" in window.getSelection(); }, - + // Returns if there is a way for setting selection to expand a line supportsSelectLine: function () { return (this.supportsSelectionModify() || document.selection) ? true : false; }, - + /** * Opera needs a white space after a <br> in order to position the caret correctly */ needsSpaceAfterLineBreak: function() { return isOpera; }, - + /** * Whether the browser supports the speech api on the given element * See http://mikepultz.com/2011/03/accessing-google-speech-api-chrome-11/ * * @example @@ -568,72 +569,73 @@ */ supportsSpeechApiOn: function(input) { var chromeVersion = userAgent.match(/Chrome\/(\d+)/) || [undefined, 0]; return chromeVersion[1] >= 11 && ("onwebkitspeechchange" in input || "speech" in input); }, - + /** * IE9 crashes when setting a getter via Object.defineProperty on XMLHttpRequest or XDomainRequest * See https://connect.microsoft.com/ie/feedback/details/650112 * or try the POC http://tifftiff.de/ie9_crash/ */ crashesWhenDefineProperty: function(property) { return isIE && (property === "XMLHttpRequest" || property === "XDomainRequest"); }, - + /** * IE is the only browser who fires the "focus" event not immediately when .focus() is called on an element */ doesAsyncFocus: function() { return isIE; }, - + /** * In IE it's impssible for the user and for the selection library to set the caret after an <img> when it's the lastChild in the document */ hasProblemsSettingCaretAfterImg: function() { return isIE; }, - + hasUndoInContextMenu: function() { return isGecko || isChrome || isOpera; }, - + /** * Opera sometimes doesn't insert the node at the right position when range.insertNode(someNode) * is used (regardless if rangy or native) * This especially happens when the caret is positioned right after a <br> because then * insertNode() will insert the node right before the <br> */ hasInsertNodeIssue: function() { return isOpera; }, - + /** * 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>) */ hasIframeFocusIssue: function() { return isIE; }, - + /** * Chrome + Safari create invalid nested markup after paste - * + * * <p> * foo * <p>bar</p> <!-- BOO! --> * </p> */ createsNestedInvalidMarkupAfterPaste: function() { return isWebKit; }, - + supportsMutationEvents: function() { return ("MutationEvent" in window); } }; -})();wysihtml5.lang.array = function(arr) { +})(); +wysihtml5.lang.array = function(arr) { return { /** * Check whether a given object exists in an array * * @example @@ -641,11 +643,11 @@ * // => true */ contains: function(needle) { return wysihtml5.lang.array(arr).indexOf(needle) !== -1; }, - + /** * Check whether a given object exists in an array and return index * If no elelemt found returns -1 * * @example @@ -660,11 +662,11 @@ if (arr[i] === needle) { return i; } } return -1; } }, - + /** * Substract one array from another * * @example * wysihtml5.lang.array([1, 2, 3, 4]).without([3, 4]); @@ -680,14 +682,14 @@ newArr.push(arr[i]); } } return newArr; }, - + /** * Return a clean native array - * + * * Following will convert a Live NodeList to a proper Array * @example * var childNodes = wysihtml5.lang.array(document.body.childNodes).get(); */ get: function() { @@ -697,11 +699,11 @@ for (; i<length; i++) { newArray.push(arr[i]); } return newArray; }, - + /** * Creates a new array with the results of calling a provided function on every element in this array. * optionally this can be provided as second argument * * @example @@ -722,11 +724,12 @@ } return A; } } }; -};wysihtml5.lang.Dispatcher = Base.extend( +}; +wysihtml5.lang.Dispatcher = Base.extend( /** @scope wysihtml5.lang.Dialog.prototype */ { on: function(eventName, handler) { this.events = this.events || {}; this.events[eventName] = this.events[eventName] || []; this.events[eventName].push(handler); @@ -751,31 +754,32 @@ // Clean up all events this.events = {}; } return this; }, - + fire: function(eventName, payload) { this.events = this.events || {}; var handlers = this.events[eventName] || [], i = 0; for (; i<handlers.length; i++) { handlers[i].call(this, payload); } return this; }, - + // deprecated, use .on() observe: function() { return this.on.apply(this, arguments); }, - + // deprecated, use .off() stopObserving: function() { return this.off.apply(this, arguments); } -});wysihtml5.lang.object = function(obj) { +}); +wysihtml5.lang.object = function(obj) { return { /** * @example * wysihtml5.lang.object({ foo: 1, bar: 1 }).merge({ bar: 2, baz: 3 }).get(); * // => { foo: 1, bar: 2, baz: 3 } @@ -784,15 +788,15 @@ for (var i in otherObj) { obj[i] = otherObj[i]; } return this; }, - + get: function() { return obj; }, - + /** * @example * wysihtml5.lang.object({ foo: 1 }).clone(); * // => { foo: 1 } */ @@ -802,21 +806,22 @@ for (i in obj) { newObj[i] = obj[i]; } return newObj; }, - + /** * @example * wysihtml5.lang.object([]).isArray(); * // => true */ isArray: function() { return Object.prototype.toString.call(obj) === "[object Array]"; } }; -};(function() { +}; +(function() { var WHITE_SPACE_START = /^\s+/, WHITE_SPACE_END = /\s+$/, ENTITY_REG_EXP = /[&<>"]/g, ENTITY_MAP = { '&': '&amp;', @@ -833,11 +838,11 @@ * // => "foo" */ trim: function() { return str.replace(WHITE_SPACE_START, "").replace(WHITE_SPACE_END, ""); }, - + /** * @example * wysihtml5.lang.string("Hello #{name}").interpolate({ name: "Christopher" }); * // => "Hello Christopher" */ @@ -845,11 +850,11 @@ for (var i in vars) { str = this.replace("#{" + i + "}").by(vars[i]); } return str; }, - + /** * @example * wysihtml5.lang.string("Hello Tom").replace("Tom").with("Hans"); * // => "Hello Hans" */ @@ -858,22 +863,23 @@ by: function(replace) { return str.split(search).join(replace); } }; }, - + /** * @example * wysihtml5.lang.string("hello<br>").escapeHTML(); * // => "hello&lt;br&gt;" */ escapeHTML: function() { return str.replace(ENTITY_REG_EXP, function(c) { return ENTITY_MAP[c]; }); } }; }; -})();/** +})(); +/** * Find urls in descendant text nodes of an element and auto-links them * Inspired by http://james.padolsey.com/javascript/find-and-replace-text-with-javascript/ * * @param {Element} element Container element in which to search for urls * @@ -898,11 +904,11 @@ */ URL_REG_EXP = /((https?:\/\/|www\.)[^\s<]{3,})/gi, TRAILING_CHAR_REG_EXP = /([^\w\/\-](,?))$/i, MAX_DISPLAY_LENGTH = 100, BRACKETS = { ")": "(", "]": "[", "}": "{" }; - + function autoLink(element) { if (_hasParentThatShouldBeIgnored(element)) { return element; } @@ -910,11 +916,11 @@ element = element.ownerDocument.body; } return _parseNode(element); } - + /** * This is basically a rebuild of * the rails auto_link_urls text helper */ function _convertUrlsToLinks(str) { @@ -934,15 +940,15 @@ } // Add http prefix if necessary if (realUrl.substr(0, 4) === "www.") { realUrl = "http://" + realUrl; } - + return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation; }); } - + /** * Creates or (if already cached) returns a temp element * for the given document object */ function _getTempElement(context) { @@ -950,31 +956,31 @@ if (!tempElement) { tempElement = context._wysihtml5_tempElement = context.createElement("div"); } return tempElement; } - + /** * Replaces the original text nodes with the newly auto-linked dom tree */ function _wrapMatchesInNode(textNode) { var parentNode = textNode.parentNode, nodeValue = wysihtml5.lang.string(textNode.data).escapeHTML(), tempElement = _getTempElement(parentNode.ownerDocument); - + // We need to insert an empty/temporary <span /> to fix IE quirks // Elsewise IE would strip white space in the beginning tempElement.innerHTML = "<span></span>" + _convertUrlsToLinks(nodeValue); tempElement.removeChild(tempElement.firstChild); - + while (tempElement.firstChild) { // inserts tempElement.firstChild before textNode parentNode.insertBefore(tempElement.firstChild, textNode); } parentNode.removeChild(textNode); } - + function _hasParentThatShouldBeIgnored(node) { var nodeName; while (node.parentNode) { node = node.parentNode; nodeName = node.nodeName; @@ -984,65 +990,66 @@ return false; } } return false; } - + function _parseNode(element) { if (IGNORE_URLS_IN.contains(element.nodeName)) { return; } - + if (element.nodeType === wysihtml5.TEXT_NODE && element.data.match(URL_REG_EXP)) { _wrapMatchesInNode(element); return; } - + var childNodes = wysihtml5.lang.array(element.childNodes).get(), childNodesLength = childNodes.length, i = 0; - + for (; i<childNodesLength; i++) { _parseNode(childNodes[i]); } - + return element; } - + wysihtml5.dom.autoLink = autoLink; - + // Reveal url reg exp to the outside wysihtml5.dom.autoLink.URL_REG_EXP = URL_REG_EXP; -})(wysihtml5);(function(wysihtml5) { +})(wysihtml5); +(function(wysihtml5) { var api = wysihtml5.dom; - + api.addClass = function(element, className) { var classList = element.classList; if (classList) { return classList.add(className); } if (api.hasClass(element, className)) { return; } element.className += " " + className; }; - + api.removeClass = function(element, className) { var classList = element.classList; if (classList) { return classList.remove(className); } - + element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), " "); }; - + api.hasClass = function(element, className) { var classList = element.classList; if (classList) { return classList.contains(className); } - + var elementClassName = element.className; return (elementClassName.length > 0 && (elementClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); }; })(wysihtml5); wysihtml5.dom.contains = (function() { @@ -1058,11 +1065,12 @@ return function(container, element) { // https://developer.mozilla.org/en/DOM/Node.compareDocumentPosition return !!(container.compareDocumentPosition(element) & 16); }; } -})();/** +})(); +/** * Converts an HTML fragment/element into a unordered/ordered list * * @param {Element} element The element which should be turned into a list * @param {String} listType The list type in which to convert the tree (either "ul" or "ol") * @return {Element} The created list @@ -1090,21 +1098,21 @@ function _createListItem(doc, list) { var listItem = doc.createElement("li"); list.appendChild(listItem); return listItem; } - + function _createList(doc, type) { return doc.createElement(type); } - + function convertToList(element, listType, uneditableClass) { if (element.nodeName === "UL" || element.nodeName === "OL" || element.nodeName === "MENU") { // Already a list return element; } - + var doc = element.ownerDocument, list = _createList(doc, listType), lineBreaks = element.querySelectorAll("br"), lineBreaksLength = lineBreaks.length, childNodes, @@ -1114,11 +1122,11 @@ parentNode, isBlockElement, isLineBreak, currentListItem, i; - + // First find <br> at the end of inline elements and move them behind them for (i=0; i<lineBreaksLength; i++) { lineBreak = lineBreaks[i]; while ((parentNode = lineBreak.parentNode) && parentNode !== element && parentNode.lastChild === lineBreak) { if (wysihtml5.dom.getStyle("display").from(parentNode) === "block") { @@ -1126,53 +1134,54 @@ break; } wysihtml5.dom.insert(lineBreak).after(lineBreak.parentNode); } } - + childNodes = wysihtml5.lang.array(element.childNodes).get(); childNodesLength = childNodes.length; - + for (i=0; i<childNodesLength; i++) { currentListItem = currentListItem || _createListItem(doc, list); childNode = childNodes[i]; isBlockElement = wysihtml5.dom.getStyle("display").from(childNode) === "block"; isLineBreak = childNode.nodeName === "BR"; - + // consider uneditable as an inline element if (isBlockElement && (!uneditableClass || !wysihtml5.dom.hasClass(childNode, uneditableClass))) { // Append blockElement to current <li> if empty, otherwise create a new one currentListItem = currentListItem.firstChild ? _createListItem(doc, list) : currentListItem; currentListItem.appendChild(childNode); currentListItem = null; continue; } - + if (isLineBreak) { // Only create a new list item in the next iteration when the current one has already content currentListItem = currentListItem.firstChild ? null : currentListItem; continue; } - + currentListItem.appendChild(childNode); } - + if (childNodes.length === 0) { _createListItem(doc, list); } - + element.parentNode.replaceChild(list, element); return list; } - + return convertToList; -})();/** +})(); +/** * Copy a set of attributes from one element to another * * @param {Array} attributesToCopy List of attributes which should be copied * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to - * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked + * copy the attributes from., this again returns an object which provides a method named "to" which can be invoked * with the element where to copy the attributes to (see example) * * @example * var textarea = document.querySelector("textarea"), * div = document.querySelector("div[contenteditable=true]"), @@ -1197,131 +1206,134 @@ return { andTo: arguments.callee }; } }; } }; -};/** +}; +/** * Copy a set of styles from one element to another * Please note that this only works properly across browsers when the element from which to copy the styles * is in the dom * * Interesting article on how to copy styles * * @param {Array} stylesToCopy List of styles which should be copied * @return {Object} Returns an object which offers the "from" method which can be invoked with the element where to - * copy the styles from., this again returns an object which provides a method named "to" which can be invoked + * copy the styles from., this again returns an object which provides a method named "to" which can be invoked * with the element where to copy the styles to (see example) * * @example * var textarea = document.querySelector("textarea"), * div = document.querySelector("div[contenteditable=true]"), * anotherDiv = document.querySelector("div.preview"); * wysihtml5.dom.copyStyles(["overflow-y", "width", "height"]).from(textarea).to(div).andTo(anotherDiv); * */ (function(dom) { - + /** * Mozilla, WebKit and Opera recalculate the computed width when box-sizing: boder-box; is set - * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then + * So if an element has "width: 200px; -moz-box-sizing: border-box; border: 1px;" then * its computed css width will be 198px * * See https://bugzilla.mozilla.org/show_bug.cgi?id=520992 */ var BOX_SIZING_PROPERTIES = ["-webkit-box-sizing", "-moz-box-sizing", "-ms-box-sizing", "box-sizing"]; - + var shouldIgnoreBoxSizingBorderBox = function(element) { if (hasBoxSizingBorderBox(element)) { return parseInt(dom.getStyle("width").from(element), 10) < element.offsetWidth; } return false; }; - + var hasBoxSizingBorderBox = function(element) { var i = 0, length = BOX_SIZING_PROPERTIES.length; for (; i<length; i++) { if (dom.getStyle(BOX_SIZING_PROPERTIES[i]).from(element) === "border-box") { return BOX_SIZING_PROPERTIES[i]; } } }; - + dom.copyStyles = function(stylesToCopy) { return { from: function(element) { if (shouldIgnoreBoxSizingBorderBox(element)) { stylesToCopy = wysihtml5.lang.array(stylesToCopy).without(BOX_SIZING_PROPERTIES); } - + var cssText = "", length = stylesToCopy.length, i = 0, property; for (; i<length; i++) { property = stylesToCopy[i]; cssText += property + ":" + dom.getStyle(property).from(element) + ";"; } - + return { to: function(element) { dom.setStyles(cssText).on(element); return { andTo: arguments.callee }; } }; } }; }; -})(wysihtml5.dom);/** +})(wysihtml5.dom); +/** * Event Delegation * * @example * wysihtml5.dom.delegate(document.body, "a", "click", function() { * // foo * }); */ (function(wysihtml5) { - + wysihtml5.dom.delegate = function(container, selector, eventName, handler) { return wysihtml5.dom.observe(container, eventName, function(event) { var target = event.target, match = wysihtml5.lang.array(container.querySelectorAll(selector)); - + while (target && target !== container) { if (match.contains(target)) { handler.call(target, event); break; } target = target.parentNode; } }); }; - -})(wysihtml5);/** + +})(wysihtml5); +/** * Returns the given html wrapped in a div element * * Fixing IE's inability to treat unknown elements (HTML5 section, article, ...) correctly * when inserted via innerHTML - * + * * @param {String} html The html which should be wrapped in a dom element * @param {Obejct} [context] Document object of the context the html belongs to * * @example * wysihtml5.dom.getAsDom("<article>foo</article>"); */ wysihtml5.dom.getAsDom = (function() { - + var _innerHTMLShiv = function(html, context) { var tempElement = context.createElement("div"); tempElement.style.display = "none"; context.body.appendChild(tempElement); // IE throws an exception when trying to insert <frameset></frameset> via innerHTML try { tempElement.innerHTML = html; } catch(e) {} context.body.removeChild(tempElement); return tempElement; }; - + /** * Make sure IE supports HTML5 tags, which is accomplished by simply creating one instance of each element */ var _ensureHTML5Compatibility = function(context) { if (context._wysihtml5_supportsHTML5Tags) { @@ -1330,22 +1342,22 @@ for (var i=0, length=HTML5_ELEMENTS.length; i<length; i++) { context.createElement(HTML5_ELEMENTS[i]); } context._wysihtml5_supportsHTML5Tags = true; }; - - + + /** * List of html5 tags * taken from http://simon.html5.org/html5-elements */ var HTML5_ELEMENTS = [ "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption", "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress", "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr" ]; - + return function(html, context) { context = context || document; var tempElement; if (typeof(html) === "object" && html.nodeType) { tempElement = context.createElement("div"); @@ -1357,11 +1369,12 @@ _ensureHTML5Compatibility(context); tempElement = _innerHTMLShiv(html, context); } return tempElement; }; -})();/** +})(); +/** * Walks the dom tree from the given node up until it finds a match * Designed for optimal performance. * * @param {Element} node The from which to check the parent nodes * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp) @@ -1373,45 +1386,45 @@ * var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" }); * // ... or ... * var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g }); */ wysihtml5.dom.getParentElement = (function() { - + function _isSameNodeName(nodeName, desiredNodeNames) { if (!desiredNodeNames || !desiredNodeNames.length) { return true; } - + if (typeof(desiredNodeNames) === "string") { return nodeName === desiredNodeNames; } else { return wysihtml5.lang.array(desiredNodeNames).contains(nodeName); } } - + function _isElement(node) { return node.nodeType === wysihtml5.ELEMENT_NODE; } - + function _hasClassName(element, className, classRegExp) { var classNames = (element.className || "").match(classRegExp) || []; if (!className) { return !!classNames.length; } return classNames[classNames.length - 1] === className; } - + function _getParentElementWithNodeName(node, nodeName, levels) { while (levels-- && node && node.nodeName !== "BODY") { if (_isSameNodeName(node.nodeName, nodeName)) { return node; } node = node.parentNode; } return null; } - + function _getParentElementWithNodeNameAndClassName(node, nodeName, className, classRegExp, levels) { while (levels-- && node && node.nodeName !== "BODY") { if (_isElement(node) && _isSameNodeName(node.nodeName, nodeName) && _hasClassName(node, className, classRegExp)) { @@ -1419,11 +1432,11 @@ } node = node.parentNode; } return null; } - + return function(node, matchingSet, levels) { levels = levels || 50; // Go max 50 nodes upwards from current node if (matchingSet.className || matchingSet.classRegExp) { return _getParentElementWithNodeNameAndClassName( node, matchingSet.nodeName, matchingSet.className, matchingSet.classRegExp, levels @@ -1448,33 +1461,33 @@ wysihtml5.dom.getStyle = (function() { var stylePropertyMapping = { "float": ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat" }, REG_EXP_CAMELIZE = /\-[a-z]/g; - + function camelize(str) { return str.replace(REG_EXP_CAMELIZE, function(match) { return match.charAt(1).toUpperCase(); }); } - + return function(property) { return { from: function(element) { if (element.nodeType !== wysihtml5.ELEMENT_NODE) { return; } - + var doc = element.ownerDocument, camelizedProperty = stylePropertyMapping[property] || camelize(property), style = element.style, currentStyle = element.currentStyle, styleValue = style[camelizedProperty]; if (styleValue) { return styleValue; } - + // currentStyle is no standard and only supported by Opera and IE but it has one important advantage over the standard-compliant // window.getComputedStyle, since it returns css property values in their original unit: // If you set an elements width to "50%", window.getComputedStyle will give you it's current width in px while currentStyle // gives you the original "50%". // Opera supports both, currentStyle and window.getComputedStyle, that's why checking for currentStyle should have higher prio @@ -1505,11 +1518,12 @@ return returnValue; } } }; }; -})();/** +})(); +/** * High performant way to check whether an element with a specific tag name is in the given document * Optimized for being heavily executed * Unleashes the power of live node lists * * @param {Object} doc The document object of the context where to check @@ -1518,25 +1532,26 @@ * wysihtml5.dom.hasElementWithTagName(document, "IMG"); */ wysihtml5.dom.hasElementWithTagName = (function() { var LIVE_CACHE = {}, DOCUMENT_IDENTIFIER = 1; - + function _getDocumentIdentifier(doc) { return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); } - + return function(doc, tagName) { var key = _getDocumentIdentifier(doc) + ":" + tagName, cacheEntry = LIVE_CACHE[key]; if (!cacheEntry) { cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tagName); } - + return cacheEntry.length > 0; }; -})();/** +})(); +/** * High performant way to check whether an element with a specific class name is in the given document * Optimized for being heavily executed * Unleashes the power of live node lists * * @param {Object} doc The document object of the context where to check @@ -1549,11 +1564,11 @@ DOCUMENT_IDENTIFIER = 1; function _getDocumentIdentifier(doc) { return doc._wysihtml5_identifier || (doc._wysihtml5_identifier = DOCUMENT_IDENTIFIER++); } - + wysihtml5.dom.hasElementWithClassName = function(doc, className) { // getElementsByClassName is not supported by IE<9 // but is sometimes mocked via library code (which then doesn't return live node lists) if (!wysihtml5.browser.supportsNativeGetElementsByClassName()) { return !!doc.querySelector("." + className); @@ -1571,33 +1586,34 @@ wysihtml5.dom.insert = function(elementToInsert) { return { after: function(element) { element.parentNode.insertBefore(elementToInsert, element.nextSibling); }, - + before: function(element) { element.parentNode.insertBefore(elementToInsert, element); }, - + into: function(element) { element.appendChild(elementToInsert); } }; -};wysihtml5.dom.insertCSS = function(rules) { +}; +wysihtml5.dom.insertCSS = function(rules) { rules = rules.join("\n"); - + return { into: function(doc) { var styleElement = doc.createElement("style"); styleElement.type = "text/css"; - + if (styleElement.styleSheet) { styleElement.styleSheet.cssText = rules; } else { styleElement.appendChild(doc.createTextNode(rules)); } - + var link = doc.querySelector("head link"); if (link) { link.parentNode.insertBefore(styleElement, link); return; } else { @@ -1606,24 +1622,25 @@ head.appendChild(styleElement); } } } }; -};/** +}; +/** * Method to set dom events * * @example * wysihtml5.dom.observe(iframe.contentWindow.document.body, ["focus", "blur"], function() { ... }); */ wysihtml5.dom.observe = function(element, eventNames, handler) { eventNames = typeof(eventNames) === "string" ? [eventNames] : eventNames; - + var handlerWrapper, eventName, i = 0, length = eventNames.length; - + for (; i<length; i++) { eventName = eventNames[i]; if (element.addEventListener) { element.addEventListener(eventName, handler, false); } else { @@ -1640,11 +1657,11 @@ handler.call(element, event); }; element.attachEvent("on" + eventName, handlerWrapper); } } - + return { stop: function() { var eventName, i = 0, length = eventNames.length; @@ -1710,11 +1727,11 @@ * }); * // => '<p class="red">foo</p><p>bar</p>' */ wysihtml5.dom.parse = (function() { - + /** * It's not possible to use a XMLParser/DOMParser as HTML5 is not always well-formed XML * new DOMParser().parseFromString('<img src="foo.gif">') will cause a parseError since the * node isn't closed * @@ -1728,111 +1745,112 @@ DEFAULT_NODE_NAME = "span", WHITE_SPACE_REG_EXP = /\s+/, defaultRules = { tags: {}, classes: {} }, currentRules = {}, uneditableClass = false; - + /** * Iterates over all childs of the element, recreates them, appends them into a document fragment * which later replaces the entire body content */ function parse(elementOrHtml, config) { wysihtml5.lang.object(currentRules).merge(defaultRules).merge(config.rules).get(); - + var context = config.context || elementOrHtml.ownerDocument || document, fragment = context.createDocumentFragment(), isString = typeof(elementOrHtml) === "string", element, newNode, firstChild; - + if (config.uneditableClass) { uneditableClass = config.uneditableClass; } - + if (isString) { element = wysihtml5.dom.getAsDom(elementOrHtml, context); } else { element = elementOrHtml; } - + while (element.firstChild) { firstChild = element.firstChild; newNode = _convert(firstChild, config.cleanUp); element.removeChild(firstChild); if (newNode) { fragment.appendChild(newNode); } } - + // Clear element contents element.innerHTML = ""; - + // Insert new DOM tree element.appendChild(fragment); - + return isString ? wysihtml5.quirks.getCorrectInnerHTML(element) : element; } - + function _convert(oldNode, cleanUp) { var oldNodeType = oldNode.nodeType, oldChilds = oldNode.childNodes, oldChildsLength = oldChilds.length, method = NODE_TYPE_MAPPING[oldNodeType], i = 0, fragment, newNode, newChild; - + if (uneditableClass && oldNodeType === 1 && wysihtml5.dom.hasClass(oldNode, uneditableClass)) { return oldNode; - } - + } + newNode = method && method(oldNode); - + if (!newNode) { if (newNode === false) { // false defines that tag should be removed but contents should remain (unwrap) - fragment = oldNode.ownerDocument.createDocumentFragment(); + + for (i = oldChildsLength; i--;) { + newChild = _convert(oldChilds[i], cleanUp); + if (newChild) { + fragment.insertBefore(newChild, fragment.firstChild); + } + } + // TODO: try to minimize surplus spaces if (wysihtml5.lang.array([ "div", "pre", "p", "table", "td", "th", "ul", "ol", "li", "dd", "dl", "footer", "header", "section", "h1", "h2", "h3", "h4", "h5", "h6" - ]).contains(oldNode.nodeName.toLowerCase()) && oldNode.parentNode.firstChild !== oldNode) { - - // add space as first when unwraping non-textflow elements - fragment.appendChild(oldNode.ownerDocument.createTextNode(" ")); + ]).contains(oldNode.nodeName.toLowerCase()) && oldNode.parentNode.lastChild !== oldNode) { + // add space at first when unwraping non-textflow elements + if (!oldNode.nextSibling || oldNode.nextSibling.nodeType !== 3 || !(/^\s/).test(oldNode.nextSibling.nodeValue)) { + fragment.appendChild(oldNode.ownerDocument.createTextNode(" ")); + } } - - - for (i=0; i<oldChildsLength; i++) { - newChild = _convert(oldChilds[i], cleanUp); - if (newChild) { - fragment.appendChild(newChild); - } - } + if (fragment.normalize) { fragment.normalize(); } return fragment; } else { return null; } } - + for (i=0; i<oldChildsLength; i++) { newChild = _convert(oldChilds[i], cleanUp); if (newChild) { newNode.appendChild(newChild); } } - + // Cleanup senseless <span> elements if (cleanUp && newNode.nodeName.toLowerCase() === DEFAULT_NODE_NAME && (!newNode.childNodes.length || ((/^\s*$/gi).test(newNode.innerHTML) && oldNode.className !== "_wysihtml5-temp-placeholder") || @@ -1845,38 +1863,38 @@ if (fragment.normalize) { fragment.normalize(); } return fragment; } - + if (newNode.normalize) { newNode.normalize(); } return newNode; } - + function _handleElement(oldNode) { var rule, newNode, tagRules = currentRules.tags, nodeName = oldNode.nodeName.toLowerCase(), scopeName = oldNode.scopeName; - - + + /** * We already parsed that element * ignore it! (yes, this sometimes happens in IE8 when the html is invalid) */ if (oldNode._wysihtml5) { return null; } oldNode._wysihtml5 = 1; - + if (oldNode.className === "wysihtml5-temp") { return null; } - + /** * IE is the only browser who doesn't include the namespace in the * nodeName, that's why we have to prepend it by ourselves * scopeName is a proprietary IE feature * read more here http://msdn.microsoft.com/en-us/library/ms534388(v=vs.85).aspx @@ -1894,25 +1912,25 @@ oldNode.nodeName === "P" && oldNode.outerHTML.slice(-4).toLowerCase() !== "</p>") { nodeName = "div"; } } - + if (nodeName in tagRules) { rule = tagRules[nodeName]; if (!rule || rule.remove) { return null; } else if (rule.unwrap) { return false; } - + // tests if type condition is met or node should be removed/unwrapped - + if (rule.one_of_type && !_testTypes(oldNode, currentRules, rule.one_of_type)) { return (rule.remove_action && rule.remove_action == "unwrap") ? false : null; } - + rule = typeof(rule) === "string" ? { rename_tag: rule } : rule; } else if (oldNode.firstChild) { rule = { rename_tag: DEFAULT_NODE_NAME }; } else { // Remove empty unknown elements @@ -1920,80 +1938,80 @@ } newNode = oldNode.ownerDocument.createElement(rule.rename_tag || nodeName); _handleAttributes(oldNode, newNode, rule); _handleStyles(oldNode, newNode, rule); oldNode = null; - + if (newNode.normalize) { newNode.normalize(); } return newNode; } - + function _testTypes(oldNode, rules, types) { var definition, type; - + // do not interfere with placeholder span or pasting caret position is not maintained if (oldNode.nodeName === "SPAN" && oldNode.className === "_wysihtml5-temp-placeholder") { return true; } - + for (type in types) { if (types.hasOwnProperty(type) && rules.type_definitions && rules.type_definitions[type]) { definition = rules.type_definitions[type]; if (_testType(oldNode, definition)) { - return true; + return true; } } } return false; } - + function array_contains(a, obj) { var i = a.length; while (i--) { if (a[i] === obj) { return true; } } return false; } - + function _testType(oldNode, definition) { - + var nodeClasses = oldNode.getAttribute("class"), nodeStyles = oldNode.getAttribute("style"), classesLength, s, s_corrected, a, attr, currentClass, styleProp; - + // test for classes, if one found return true if (nodeClasses && definition.classes) { nodeClasses = nodeClasses.replace(/^\s+/g, '').replace(/\s+$/g, '').split(WHITE_SPACE_REG_EXP); classesLength = nodeClasses.length; for (var i = 0; i < classesLength; i++) { if (definition.classes[nodeClasses[i]]) { return true; } } } - + // test for styles, if one found return true if (nodeStyles && definition.styles) { - + nodeStyles = nodeStyles.split(';'); for (s in definition.styles) { if (definition.styles.hasOwnProperty(s)) { for (var sp = nodeStyles.length; sp--;) { styleProp = nodeStyles[sp].split(':'); - + if (styleProp[0].replace(/\s/g, '').toLowerCase() === s) { if (definition.styles[s] === true || styleProp[1].replace(/\s/g, '').toLowerCase() === definition.styles[s]) { return true; } } } } } } - + // test for attributes in general against regex match if (definition.attrs) { for (a in definition.attrs) { if (definition.attrs.hasOwnProperty(a)) { attr = _getAttribute(oldNode, a); @@ -2005,11 +2023,11 @@ } } } return false; } - + function _handleStyles(oldNode, newNode, rule) { var s; if(rule && rule.keep_styles) { for (s in rule.keep_styles) { if (rule.keep_styles.hasOwnProperty(s)) { @@ -2026,11 +2044,11 @@ } } } } } - + function _handleAttributes(oldNode, newNode, rule) { var attributes = {}, // fresh new set of attributes to set on newNode setClass = rule.set_class, // classes to set addClass = rule.add_class, // add classes based on existing attributes setAttributes = rule.set_attributes, // attributes to set on the current node @@ -2046,15 +2064,15 @@ currentClass, newClass, attributeName, newAttributeValue, method; - + if (setAttributes) { attributes = wysihtml5.lang.object(setAttributes).clone(); } - + if (checkAttributes) { for (attributeName in checkAttributes) { method = attributeCheckMethods[checkAttributes[attributeName]]; if (!method) { continue; @@ -2066,15 +2084,15 @@ attributes[attributeName] = newAttributeValue; } } } } - + if (setClass) { classes.push(setClass); } - + if (addClass) { for (attributeName in addClass) { method = addClassMethods[addClass[attributeName]]; if (!method) { continue; @@ -2083,14 +2101,14 @@ if (typeof(newClass) === "string") { classes.push(newClass); } } } - + // make sure that wysihtml5 temp class doesn't get stripped out allowedClasses["_wysihtml5-temp-placeholder"] = 1; - + // add old classes last oldClasses = oldNode.getAttribute("class"); if (oldClasses) { classes = classes.concat(oldClasses.split(WHITE_SPACE_REG_EXP)); } @@ -2099,34 +2117,34 @@ currentClass = classes[i]; if (allowedClasses[currentClass]) { newClasses.push(currentClass); } } - + // remove duplicate entries and preserve class specificity newClassesLength = newClasses.length; while (newClassesLength--) { currentClass = newClasses[newClassesLength]; if (!wysihtml5.lang.array(newUniqueClasses).contains(currentClass)) { newUniqueClasses.unshift(currentClass); } } - + if (newUniqueClasses.length) { attributes["class"] = newUniqueClasses.join(" "); } - + // set attributes on newNode for (attributeName in attributes) { // Setting attributes can cause a js error in IE under certain circumstances // eg. on a <img> under https when it's new attribute value is non-https // TODO: Investigate this further and check for smarter handling try { newNode.setAttribute(attributeName, attributes[attributeName]); } catch(e) {} } - + // IE8 sometimes loses the width/height attributes when those are set before the "src" // so we make sure to set them again if (attributes.src) { if (typeof(attributes.width) !== "undefined") { newNode.setAttribute("width", attributes.width); @@ -2134,11 +2152,11 @@ if (typeof(attributes.height) !== "undefined") { newNode.setAttribute("height", attributes.height); } } } - + /** * IE gives wrong results for hasAttribute/getAttribute, for example: * var td = document.createElement("td"); * td.getAttribute("rowspan"); // => "1" in IE * @@ -2157,17 +2175,17 @@ } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) { // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML var outerHTML = node.outerHTML.toLowerCase(), // TODO: This might not work for attributes without value: <input disabled> hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1; - + return hasAttribute ? node.getAttribute(attributeName) : null; } else{ return node.getAttribute(attributeName); } } - + /** * Check whether the given node is a proper loaded image * FIXME: Returns undefined when unknown (Chrome, Safari) */ function _isLoadedImage(node) { @@ -2177,25 +2195,25 @@ if (node.complete && node.readyState === "complete") { return true; } } } - + var INVISIBLE_SPACE_REG_EXP = /\uFEFF/g; function _handleText(oldNode) { var nextSibling = oldNode.nextSibling; if (nextSibling && nextSibling.nodeType === wysihtml5.TEXT_NODE) { // Concatenate text nodes nextSibling.data = oldNode.data + nextSibling.data; } else { // \uFEFF = wysihtml5.INVISIBLE_SPACE (used as a hack in certain rich text editing situations) var data = oldNode.data.replace(INVISIBLE_SPACE_REG_EXP, ""); return oldNode.ownerDocument.createTextNode(data); - } + } } - - + + // ------------ attribute checks ------------ \\ var attributeCheckMethods = { url: (function() { var REG_EXP = /^https?:\/\//i; return function(attributeValue) { @@ -2229,30 +2247,36 @@ return attributeValue.replace(REG_EXP, function(match) { return match.toLowerCase(); }); }; })(), - + alt: (function() { var REG_EXP = /[^ a-z0-9_\-]/gi; return function(attributeValue) { if (!attributeValue) { return ""; } return attributeValue.replace(REG_EXP, ""); }; })(), - + numbers: (function() { var REG_EXP = /\D/g; return function(attributeValue) { attributeValue = (attributeValue || "").replace(REG_EXP, ""); return attributeValue || null; }; + })(), + + any: (function() { + return function(attributeValue) { + return attributeValue; + }; })() }; - + // ------------ class converter (converts an html attribute to a class name) ------------ \\ var addClassMethods = { align_img: (function() { var mapping = { left: "wysiwyg-float-left", @@ -2260,11 +2284,11 @@ }; return function(attributeValue) { return mapping[String(attributeValue).toLowerCase()]; }; })(), - + align_text: (function() { var mapping = { left: "wysiwyg-text-align-left", right: "wysiwyg-text-align-right", center: "wysiwyg-text-align-center", @@ -2272,11 +2296,11 @@ }; return function(attributeValue) { return mapping[String(attributeValue).toLowerCase()]; }; })(), - + clear_br: (function() { var mapping = { left: "wysiwyg-clear-left", right: "wysiwyg-clear-right", both: "wysiwyg-clear-both", @@ -2284,11 +2308,11 @@ }; return function(attributeValue) { return mapping[String(attributeValue).toLowerCase()]; }; })(), - + size_font: (function() { var mapping = { "1": "wysiwyg-font-size-xx-small", "2": "wysiwyg-font-size-small", "3": "wysiwyg-font-size-medium", @@ -2302,11 +2326,11 @@ return function(attributeValue) { return mapping[String(attributeValue).charAt(0)]; }; })() }; - + return parse; })(); /** * Checks for empty text node childs and removes them * @@ -2358,13 +2382,14 @@ newElement.appendChild(firstChild); } wysihtml5.dom.copyAttributes(["align", "className"]).from(element).to(newElement); element.parentNode.replaceChild(newElement, element); return newElement; -};/** +}; +/** * Takes an element, removes it and replaces it with it's childs - * + * * @param {Object} node The node which to replace with it's child nodes * @example * <div id="foo"> * <span>hello</span> * </div> @@ -2375,16 +2400,16 @@ */ wysihtml5.dom.replaceWithChildNodes = function(node) { if (!node.parentNode) { return; } - + if (!node.firstChild) { node.parentNode.removeChild(node); return; } - + var fragment = node.ownerDocument.createDocumentFragment(); while (node.firstChild) { fragment.appendChild(node.firstChild); } node.parentNode.replaceChild(fragment, node); @@ -2414,35 +2439,35 @@ */ (function(dom) { function _isBlockElement(node) { return dom.getStyle("display").from(node) === "block"; } - + function _isLineBreak(node) { return node.nodeName === "BR"; } - + function _appendLineBreak(element) { var lineBreak = element.ownerDocument.createElement("br"); element.appendChild(lineBreak); } - + function resolveList(list, useLineBreaks) { if (!list.nodeName.match(/^(MENU|UL|OL)$/)) { return; } - + var doc = list.ownerDocument, fragment = doc.createDocumentFragment(), previousSibling = list.previousElementSibling || list.previousSibling, firstChild, lastChild, isLastChild, shouldAppendLineBreak, paragraph, listItem; - + if (useLineBreaks) { // Insert line break if list is after a non-block element if (previousSibling && !_isBlockElement(previousSibling) && !_isLineBreak(previousSibling)) { _appendLineBreak(fragment); } @@ -2456,11 +2481,11 @@ fragment.appendChild(firstChild); if (shouldAppendLineBreak) { _appendLineBreak(fragment); } } - + listItem.parentNode.removeChild(listItem); } } else { while (listItem = (list.firstElementChild || list.firstChild)) { if (listItem.querySelector && listItem.querySelector("div, p, ul, ol, menu, blockquote, h1, h2, h3, h4, h5, h6")) { @@ -2478,13 +2503,14 @@ } } list.parentNode.replaceChild(fragment, list); } - + dom.resolveList = resolveList; -})(wysihtml5.dom);/** +})(wysihtml5.dom); +/** * Sandbox for executing javascript, parsing css styles and doing dom operations in a secure way * * Browser Compatibility: * - Secure in MSIE 6+, but only when the user hasn't made changes to his security level "restricted" * - Partially secure in other browsers (Firefox, Opera, Safari, Chrome, ...) @@ -2530,25 +2556,25 @@ */ documentProperties = [ "referrer", "write", "open", "close" ]; - + wysihtml5.dom.Sandbox = Base.extend( /** @scope wysihtml5.dom.Sandbox.prototype */ { constructor: function(readyCallback, config) { this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION; this.config = wysihtml5.lang.object({}).merge(config).get(); this.editableArea = this._createIframe(); }, - + insertInto: function(element) { if (typeof(element) === "string") { element = doc.getElementById(element); } - + element.appendChild(this.editableArea); }, getIframe: function() { return this.editableArea; @@ -2580,11 +2606,11 @@ * Therefore we need to set the "allow-same-origin" flag which enables accessing the iframe's dom * But then there's another problem, DOM events (focus, blur, change, keypress, ...) aren't fired. * In order to make this happen we need to set the "allow-scripts" flag. * A combination of allow-scripts and allow-same-origin is almost the same as setting no sandbox attribute at all. * - Chrome & Safari, doesn't seem to support sandboxing correctly when the iframe's html is inlined (no physical document) - * - IE needs to have the security="restricted" attribute set before the iframe is + * - IE needs to have the security="restricted" attribute set before the iframe is * inserted into the dom tree * - Believe it or not but in IE "security" in document.createElement("iframe") is false, even * though it supports it * - When an iframe has security="restricted", in IE eval() & execScript() don't work anymore * - IE doesn't fire the onload event when the content is inlined in the src attribute, therefore we rely @@ -2657,11 +2683,11 @@ throw new Error("wysihtml5.Sandbox: " + errorMessage, fileName, lineNumber); }; if (!wysihtml5.browser.supportsSandboxedIframes()) { // Unset a bunch of sensitive variables - // Please note: This isn't hack safe! + // Please note: This isn't hack safe! // It more or less just takes care of basic attacks and prevents accidental theft of sensitive information // IE is secure though, which is the most important thing, since IE is the only browser, who // takes over scripts & styles into contentEditable elements when copied from external websites // or applications (Microsoft Word, ...) var i, length; @@ -2672,11 +2698,11 @@ this._unset(iframeWindow, windowProperties2[i], wysihtml5.EMPTY_FUNCTION); } for (i=0, length=documentProperties.length; i<length; i++) { this._unset(iframeDocument, documentProperties[i]); } - // This doesn't work in Safari 5 + // This doesn't work in Safari 5 // See http://stackoverflow.com/questions/992461/is-it-possible-to-override-document-cookie-in-webkit this._unset(iframeDocument, "cookie", "", true); } this.loaded = true; @@ -2733,49 +2759,49 @@ } } }); })(wysihtml5); (function(wysihtml5) { - var doc = document; + var doc = document; wysihtml5.dom.ContentEditableArea = Base.extend({ getContentEditable: function() { return this.element; }, - + getWindow: function() { return this.element.ownerDocument.defaultView; }, getDocument: function() { return this.element.ownerDocument; }, - + constructor: function(readyCallback, config, contentEditable) { this.callback = readyCallback || wysihtml5.EMPTY_FUNCTION; this.config = wysihtml5.lang.object({}).merge(config).get(); if (contentEditable) { this.element = this._bindElement(contentEditable); - } else { + } else { this.element = this._createElement(); } }, - + // creates a new contenteditable and initiates it _createElement: function() { var element = doc.createElement("div"); element.className = "wysihtml5-sandbox"; this._loadElement(element); return element; }, - + // initiates an allready existent contenteditable _bindElement: function(contentEditable) { contentEditable.className = (contentEditable.className && contentEditable.className != '') ? contentEditable.className + " wysihtml5-sandbox" : "wysihtml5-sandbox"; this._loadElement(contentEditable, true); return contentEditable; }, - + _loadElement: function(element, contentExists) { var that = this; if (!contentExists) { var sandboxHtml = this._getHtml(); element.innerHTML = sandboxHtml; @@ -2794,15 +2820,15 @@ */ this.loaded = true; // Trigger the callback setTimeout(function() { that.callback(that); }, 0); }, - - _getHtml: function(templateVars) { + + _getHtml: function(templateVars) { return ''; } - + }); })(wysihtml5); (function() { var mapping = { "className": "class" @@ -2814,11 +2840,12 @@ element.setAttribute(mapping[i] || i, attributes[i]); } } }; }; -})();wysihtml5.dom.setStyles = function(styles) { +})(); +wysihtml5.dom.setStyles = function(styles) { return { on: function(element) { var style = element.style; if (typeof(styles) === "string") { style.cssText += ";" + styles; @@ -2832,11 +2859,12 @@ style[i] = styles[i]; } } } }; -};/** +}; +/** * Simulate HTML5 placeholder attribute * * Needed since * - div[contentEditable] elements don't support it * - older browsers (such as IE8 and Firefox 3.6) don't support it at all @@ -2928,17 +2956,18 @@ } else if (HAS_GET_ATTRIBUTE_BUG && "outerHTML" in node) { // Don't trust getAttribute/hasAttribute in IE 6-8, instead check the element's outerHTML var outerHTML = node.outerHTML.toLowerCase(), // TODO: This might not work for attributes without value: <input disabled> hasAttribute = outerHTML.indexOf(" " + attributeName + "=") != -1; - + return hasAttribute ? node.getAttribute(attributeName) : null; } else{ return node.getAttribute(attributeName); } -};(function(wysihtml5) { - +}; +(function(wysihtml5) { + var api = wysihtml5.dom; var MapCell = function(cell) { this.el = cell; this.isColspan= false; @@ -2959,11 +2988,11 @@ } else if (table) { this.table = table; this.cell = this.table.querySelectorAll('th, td')[0]; } }; - + function queryInList(list, query) { var ret = [], q; for (var e = 0, len = list.length; e < len; e++) { q = list[e].querySelectorAll(query); @@ -2971,19 +3000,19 @@ for(var i = q.length; i--; ret.unshift(q[i])); } } return ret; } - + function removeElement(el) { el.parentNode.removeChild(el); } - + function insertAfter(referenceNode, newNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } - + function nextNode(node, tag) { var element = node.nextSibling; while (element.nodeType !=1) { element = element.nextSibling; if (!tag || tag == element.tagName.toLowerCase()) { @@ -2992,16 +3021,16 @@ } return null; } TableModifyerByCell.prototype = { - + addSpannedCellToMap: function(cell, map, r, c, cspan, rspan) { var spanCollect = [], rmax = r + ((rspan) ? parseInt(rspan, 10) - 1 : 0), cmax = c + ((cspan) ? parseInt(cspan, 10) - 1 : 0); - + for (var rr = r; rr <= rmax; rr++) { if (typeof map[rr] == "undefined") { map[rr] = []; } for (var cc = c; cc <= cmax; cc++) { map[rr][cc] = new MapCell(cell); map[rr][cc].isColspan = (cspan && parseInt(cspan, 10) > 1); @@ -3010,27 +3039,27 @@ map[rr][cc].lastCol = cc == cmax; map[rr][cc].firstRow = rr == r; map[rr][cc].lastRow = rr == rmax; map[rr][cc].isReal = cc == c && rr == r; map[rr][cc].spanCollection = spanCollect; - + spanCollect.push(map[rr][cc]); } } }, - + setCellAsModified: function(cell) { cell.modified = true; if (cell.spanCollection.length > 0) { for (var s = 0, smax = cell.spanCollection.length; s < smax; s++) { cell.spanCollection[s].modified = true; } } }, setTableMap: function() { - var map = []; + var map = []; var tableRows = this.getTableRows(), ridx, row, cells, cidx, cell, c, cspan, rspan; @@ -3041,11 +3070,11 @@ if (typeof map[ridx] == "undefined") { map[ridx] = []; } for (cidx = 0; cidx < cells.length; cidx++) { cell = cells[cidx]; // If cell allready set means it is set by col or rowspan, - // so increase cols index until free col is found + // so increase cols index until free col is found while (typeof map[ridx][c] != "undefined") { c++; } cspan = api.getAttribute(cell, 'colspan'); rspan = api.getAttribute(cell, 'rowspan'); @@ -3055,61 +3084,61 @@ } else { map[ridx][c] = new MapCell(cell); c++; } } - } + } this.map = map; return map; }, - + getRowCells: function(row) { var inlineTables = this.table.querySelectorAll('table'), inlineCells = (inlineTables) ? queryInList(inlineTables, 'th, td') : [], allCells = row.querySelectorAll('th, td'), tableCells = (inlineCells.length > 0) ? wysihtml5.lang.array(allCells).without(inlineCells) : allCells; - + return tableCells; }, - + getTableRows: function() { var inlineTables = this.table.querySelectorAll('table'), inlineRows = (inlineTables) ? queryInList(inlineTables, 'tr') : [], allRows = this.table.querySelectorAll('tr'), tableRows = (inlineRows.length > 0) ? wysihtml5.lang.array(allRows).without(inlineRows) : allRows; - + return tableRows; }, - + getMapIndex: function(cell) { var r_length = this.map.length, c_length = (this.map && this.map[0]) ? this.map[0].length : 0; - + for (var r_idx = 0;r_idx < r_length; r_idx++) { for (var c_idx = 0;c_idx < c_length; c_idx++) { if (this.map[r_idx][c_idx].el === cell) { return {'row': r_idx, 'col': c_idx}; } } } return false; }, - + getElementAtIndex: function(idx) { this.setTableMap(); if (this.map[idx.row] && this.map[idx.row][idx.col] && this.map[idx.row][idx.col].el) { return this.map[idx.row][idx.col].el; } return null; }, - + getMapElsTo: function(to_cell) { var els = []; this.setTableMap(); this.idx_start = this.getMapIndex(this.cell); this.idx_end = this.getMapIndex(to_cell); - + // switch indexes if start is bigger than end if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) { var temp_idx = this.idx_start; this.idx_start = this.idx_end; this.idx_end = temp_idx; @@ -3117,26 +3146,26 @@ if (this.idx_start.col > this.idx_end.col) { var temp_cidx = this.idx_start.col; this.idx_start.col = this.idx_end.col; this.idx_end.col = temp_cidx; } - + if (this.idx_start != null && this.idx_end != null) { for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) { for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) { els.push(this.map[row][col].el); } } } return els; }, - + orderSelectionEnds: function(secondcell) { this.setTableMap(); this.idx_start = this.getMapIndex(this.cell); this.idx_end = this.getMapIndex(secondcell); - + // switch indexes if start is bigger than end if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) { var temp_idx = this.idx_start; this.idx_start = this.idx_end; this.idx_end = temp_idx; @@ -3144,40 +3173,40 @@ if (this.idx_start.col > this.idx_end.col) { var temp_cidx = this.idx_start.col; this.idx_start.col = this.idx_end.col; this.idx_end.col = temp_cidx; } - + return { "start": this.map[this.idx_start.row][this.idx_start.col].el, "end": this.map[this.idx_end.row][this.idx_end.col].el }; }, - + createCells: function(tag, nr, attrs) { var doc = this.table.ownerDocument, frag = doc.createDocumentFragment(), cell; for (var i = 0; i < nr; i++) { cell = doc.createElement(tag); - + if (attrs) { for (var attr in attrs) { if (attrs.hasOwnProperty(attr)) { cell.setAttribute(attr, attrs[attr]); } } } - + // add non breaking space cell.appendChild(document.createTextNode("\u00a0")); - + frag.appendChild(cell); } return frag; }, - + // Returns next real cell (not part of spanned cell unless first) on row if selected index is not real. I no real cells -1 will be returned correctColIndexForUnreals: function(col, row) { var r = this.map[row], corrIdx = -1; for (var i = 0, max = col; i < col; i++) { @@ -3185,35 +3214,35 @@ corrIdx++; } } return corrIdx; }, - + getLastNewCellOnRow: function(row, rowLimit) { var cells = this.getRowCells(row), cell, idx; - + for (var cidx = 0, cmax = cells.length; cidx < cmax; cidx++) { cell = cells[cidx]; idx = this.getMapIndex(cell); if (idx === false || (typeof rowLimit != "undefined" && idx.row != rowLimit)) { return cell; } } return null; }, - + removeEmptyTable: function() { var cells = this.table.querySelectorAll('td, th'); if (!cells || cells.length == 0) { removeElement(this.table); return true; } else { return false; } }, - + // Splits merged cell on row to unique cells splitRowToCells: function(cell) { if (cell.isColspan) { var colspan = parseInt(api.getAttribute(cell.el, 'colspan') || 1, 10), cType = cell.el.tagName.toLowerCase(); @@ -3222,86 +3251,103 @@ insertAfter(cell.el, newCells); } cell.el.removeAttribute('colspan'); } }, - + getRealRowEl: function(force, idx) { var r = null, c = null; - + idx = idx || this.idx; - + for (var cidx = 0, cmax = this.map[idx.row].length; cidx < cmax; cidx++) { c = this.map[idx.row][cidx]; if (c.isReal) { r = api.getParentElement(c.el, { nodeName: ["TR"] }); if (r) { return r; } } } - + if (r === null && force) { r = api.getParentElement(this.map[idx.row][idx.col].el, { nodeName: ["TR"] }) || null; } - + return r; }, - + injectRowAt: function(row, col, colspan, cType, c) { var r = this.getRealRowEl(false, {'row': row, 'col': col}), new_cells = this.createCells(cType, colspan); - + if (r) { var n_cidx = this.correctColIndexForUnreals(col, row); if (n_cidx >= 0) { insertAfter(this.getRowCells(r)[n_cidx], new_cells); } else { r.insertBefore(new_cells, r.firstChild); - } + } } else { var rr = this.table.ownerDocument.createElement('tr'); rr.appendChild(new_cells); insertAfter(api.getParentElement(c.el, { nodeName: ["TR"] }), rr); } }, - - canMerge: function() { + + canMerge: function(to) { + this.to = to; + this.setTableMap(); + this.idx_start = this.getMapIndex(this.cell); + this.idx_end = this.getMapIndex(this.to); + + // switch indexes if start is bigger than end + if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) { + var temp_idx = this.idx_start; + this.idx_start = this.idx_end; + this.idx_end = temp_idx; + } + if (this.idx_start.col > this.idx_end.col) { + var temp_cidx = this.idx_start.col; + this.idx_start.col = this.idx_end.col; + this.idx_end.col = temp_cidx; + } + for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) { for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) { if (this.map[row][col].isColspan || this.map[row][col].isRowspan) { return false; } } } return true; }, - + decreaseCellSpan: function(cell, span) { var nr = parseInt(api.getAttribute(cell.el, span), 10) - 1; - if (nr >= 1) { + if (nr >= 1) { cell.el.setAttribute(span, nr); } else { cell.el.removeAttribute(span); if (span == 'colspan') { cell.isColspan = false; } if (span == 'rowspan') { cell.isRowspan = false; - } + } cell.firstCol = true; cell.lastCol = true; cell.firstRow = true; cell.lastRow = true; - cell.isReal = true; + cell.isReal = true; } }, - + removeSurplusLines: function() { var row, cell, ridx, rmax, cidx, cmax, allRowspan; - + this.setTableMap(); if (this.map) { ridx = 0; rmax = this.map.length; for (;ridx < rmax; ridx++) { @@ -3321,11 +3367,11 @@ for (; cidx < cmax; cidx++) { this.decreaseCellSpan(row[cidx], 'rowspan'); } } } - + // remove rows without cells var tableRows = this.getTableRows(); ridx = 0; rmax = tableRows.length; for (;ridx < rmax; ridx++) { @@ -3334,25 +3380,25 @@ removeElement(row); } } } }, - + fillMissingCells: function() { var r_max = 0, c_max = 0, prevcell = null; - + this.setTableMap(); if (this.map) { - + // find maximal dimensions of broken table r_max = this.map.length; for (var ridx = 0; ridx < r_max; ridx++) { if (this.map[ridx].length > c_max) { c_max = this.map[ridx].length; } } - + for (var row = 0; row < r_max; row++) { for (var col = 0; col < c_max; col++) { if (this.map[row] && !this.map[row][col]) { if (col > 0) { this.map[row][col] = new MapCell(this.createCells('td', 1)); @@ -3361,34 +3407,34 @@ insertAfter(this.map[row][col-1].el, this.map[row][col].el); } } } } - } + } } }, - + rectify: function() { if (!this.removeEmptyTable()) { this.removeSurplusLines(); this.fillMissingCells(); return true; } else { return false; } }, - + unmerge: function() { if (this.rectify()) { this.setTableMap(); this.idx = this.getMapIndex(this.cell); - + if (this.idx) { var thisCell = this.map[this.idx.row][this.idx.col], colspan = (api.getAttribute(thisCell.el, "colspan")) ? parseInt(api.getAttribute(thisCell.el, "colspan"), 10) : 1, cType = thisCell.el.tagName.toLowerCase(); - + if (thisCell.isRowspan) { var rowspan = parseInt(api.getAttribute(thisCell.el, "rowspan"), 10); if (rowspan > 1) { for (var nr = 1, maxr = rowspan - 1; nr <= maxr; nr++){ this.injectRowAt(this.idx.row + nr, this.idx.col, colspan, cType, thisCell); @@ -3398,42 +3444,26 @@ } this.splitRowToCells(thisCell); } } }, - + // merges cells from start cell (defined in creating obj) to "to" cell merge: function(to) { if (this.rectify()) { - this.to = to; - this.setTableMap(); - this.idx_start = this.getMapIndex(this.cell); - this.idx_end = this.getMapIndex(this.to); - - // switch indexes if start is bigger than end - if (this.idx_start.row > this.idx_end.row || (this.idx_start.row == this.idx_end.row && this.idx_start.col > this.idx_end.col)) { - var temp_idx = this.idx_start; - this.idx_start = this.idx_end; - this.idx_end = temp_idx; - } - if (this.idx_start.col > this.idx_end.col) { - var temp_cidx = this.idx_start.col; - this.idx_start.col = this.idx_end.col; - this.idx_end.col = temp_cidx; - } - if (this.canMerge()) { + if (this.canMerge(to)) { var rowspan = this.idx_end.row - this.idx_start.row + 1, colspan = this.idx_end.col - this.idx_start.col + 1; - + for (var row = this.idx_start.row, maxr = this.idx_end.row; row <= maxr; row++) { for (var col = this.idx_start.col, maxc = this.idx_end.col; col <= maxc; col++) { - + if (row == this.idx_start.row && col == this.idx_start.col) { - if (rowspan > 1) { + if (rowspan > 1) { this.map[row][col].el.setAttribute('rowspan', rowspan); } - if (colspan > 1) { + if (colspan > 1) { this.map[row][col].el.setAttribute('colspan', colspan); } } else { // transfer content if (!(/^\s*<br\/?>\s*$/.test(this.map[row][col].el.innerHTML.toLowerCase()))) { @@ -3449,20 +3479,20 @@ console.log('Do not know how to merge allready merged cells.'); } } } }, - + // Decreases rowspan of a cell if it is done on first cell of rowspan row (real cell) // Cell is moved to next row (if it is real) collapseCellToNextRow: function(cell) { var cellIdx = this.getMapIndex(cell.el), newRowIdx = cellIdx.row + 1, newIdx = {'row': newRowIdx, 'col': cellIdx.col}; - + if (newRowIdx < this.map.length) { - + var row = this.getRealRowEl(false, newIdx); if (row !== null) { var n_cidx = this.correctColIndexForUnreals(newIdx.col, newIdx.row); if (n_cidx >= 0) { insertAfter(this.getRowCells(row)[n_cidx], cell.el); @@ -3480,11 +3510,11 @@ cell.el.removeAttribute('rowspan'); } } } }, - + // Removes a cell when removing a row // If is rowspan cell then decreases the rowspan // and moves cell to next row if needed (is first cell of rowspan) removeRowCell: function(cell) { if (cell.isReal) { @@ -3499,11 +3529,40 @@ } else { cell.el.removeAttribute('rowspan'); } } }, - + + getRowElementsByCell: function() { + var cells = []; + this.setTableMap(); + this.idx = this.getMapIndex(this.cell); + if (this.idx !== false) { + var modRow = this.map[this.idx.row]; + for (var cidx = 0, cmax = modRow.length; cidx < cmax; cidx++) { + if (modRow[cidx].isReal) { + cells.push(modRow[cidx].el); + } + } + } + return cells; + }, + + getColumnElementsByCell: function() { + var cells = []; + this.setTableMap(); + this.idx = this.getMapIndex(this.cell); + if (this.idx !== false) { + for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++) { + if (this.map[ridx][this.idx.col] && this.map[ridx][this.idx.col].isReal) { + cells.push(this.map[ridx][this.idx.col].el); + } + } + } + return cells; + }, + // Removes the row of selected cell removeRow: function() { var oldRow = api.getParentElement(this.cell, { nodeName: ["TR"] }); if (oldRow) { this.setTableMap(); @@ -3518,11 +3577,11 @@ } } removeElement(oldRow); } }, - + removeColCell: function(cell) { if (cell.isColspan) { if (parseInt(api.getAttribute(cell.el, 'colspan'), 10) > 2) { cell.el.setAttribute('colspan', parseInt(api.getAttribute(cell.el, 'colspan'), 10) - 1); } else { @@ -3530,11 +3589,11 @@ } } else if (cell.isReal) { removeElement(cell.el); } }, - + removeColumn: function() { this.setTableMap(); this.idx = this.getMapIndex(this.cell); if (this.idx !== false) { for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++) { @@ -3543,11 +3602,11 @@ this.removeColCell(this.map[ridx][this.idx.col]); } } } }, - + // removes row or column by selected cell element remove: function(what) { if (this.rectify()) { switch (what) { case 'row': @@ -3558,45 +3617,45 @@ break; } this.rectify(); } }, - + addRow: function(where) { var doc = this.table.ownerDocument; - + this.setTableMap(); this.idx = this.getMapIndex(this.cell); if (where == "below" && api.getAttribute(this.cell, 'rowspan')) { this.idx.row = this.idx.row + parseInt(api.getAttribute(this.cell, 'rowspan'), 10) - 1; } - + if (this.idx !== false) { var modRow = this.map[this.idx.row], - newRow = doc.createElement('tr'); - + newRow = doc.createElement('tr'); + for (var ridx = 0, rmax = modRow.length; ridx < rmax; ridx++) { if (!modRow[ridx].modified) { this.setCellAsModified(modRow[ridx]); this.addRowCell(modRow[ridx], newRow, where); } } - + switch (where) { - case 'below': + case 'below': insertAfter(this.getRealRowEl(true), newRow); break; - case 'above': + case 'above': var cr = api.getParentElement(this.map[this.idx.row][this.idx.col].el, { nodeName: ["TR"] }); if (cr) { cr.parentNode.insertBefore(newRow, cr); } break; } } }, - + addRowCell: function(cell, row, where) { var colSpanAttr = (cell.isColspan) ? {"colspan" : api.getAttribute(cell.el, 'colspan')} : null; if (cell.isReal) { if (where != 'above' && cell.isRowspan) { cell.el.setAttribute('rowspan', parseInt(api.getAttribute(cell.el,'rowspan'), 10) + 1); @@ -3606,71 +3665,71 @@ } else { if (where != 'above' && cell.isRowspan && cell.lastRow) { row.appendChild(this.createCells('td', 1, colSpanAttr)); } else if (c.isRowspan) { cell.el.attr('rowspan', parseInt(api.getAttribute(cell.el, 'rowspan'), 10) + 1); - } + } } }, - + add: function(where) { if (this.rectify()) { if (where == 'below' || where == 'above') { this.addRow(where); } if (where == 'before' || where == 'after') { this.addColumn(where); } } }, - + addColCell: function (cell, ridx, where) { var doAdd, cType = cell.el.tagName.toLowerCase(); - + // defines add cell vs expand cell conditions // true means add switch (where) { - case "before": + case "before": doAdd = (!cell.isColspan || cell.firstCol); break; case "after": doAdd = (!cell.isColspan || cell.lastCol || (cell.isColspan && c.el == this.cell)); break; } - + if (doAdd){ - // adds a cell before or after current cell element + // adds a cell before or after current cell element switch (where) { case "before": cell.el.parentNode.insertBefore(this.createCells(cType, 1), cell.el); break; case "after": insertAfter(cell.el, this.createCells(cType, 1)); break; } - - // handles if cell has rowspan + + // handles if cell has rowspan if (cell.isRowspan) { this.handleCellAddWithRowspan(cell, ridx+1, where); } - + } else { // expands cell cell.el.setAttribute('colspan', parseInt(api.getAttribute(cell.el, 'colspan'), 10) + 1); } }, - + addColumn: function(where) { var row, modCell; - + this.setTableMap(); this.idx = this.getMapIndex(this.cell); if (where == "after" && api.getAttribute(this.cell, 'colspan')) { this.idx.col = this.idx.col + parseInt(api.getAttribute(this.cell, 'colspan'), 10) - 1; } - + if (this.idx !== false) { for (var ridx = 0, rmax = this.map.length; ridx < rmax; ridx++ ) { row = this.map[ridx]; if (row[this.idx.col]) { modCell = row[this.idx.col]; @@ -3680,33 +3739,33 @@ } } } } }, - + handleCellAddWithRowspan: function (cell, ridx, where) { var addRowsNr = parseInt(api.getAttribute(this.cell, 'rowspan'), 10) - 1, crow = api.getParentElement(cell.el, { nodeName: ["TR"] }), cType = cell.el.tagName.toLowerCase(), cidx, temp_r_cells, doc = this.table.ownerDocument, nrow; - + for (var i = 0; i < addRowsNr; i++) { cidx = this.correctColIndexForUnreals(this.idx.col, (ridx + i)); crow = nextNode(crow, 'tr'); if (crow) { if (cidx > 0) { switch (where) { - case "before": + case "before": temp_r_cells = this.getRowCells(crow); if (cidx > 0 && this.map[ridx + i][this.idx.col].el != temp_r_cells[cidx] && cidx == temp_r_cells.length - 1) { insertAfter(temp_r_cells[cidx], this.createCells(cType, 1)); } else { temp_r_cells[cidx].parentNode.insertBefore(this.createCells(cType, 1), temp_r_cells[cidx]); } - + break; case "after": insertAfter(this.getRowCells(crow)[cidx], this.createCells(cType, 1)); break; } @@ -3719,75 +3778,91 @@ this.table.appendChild(nrow); } } } }; - + api.table = { getCellsBetween: function(cell1, cell2) { var c1 = new TableModifyerByCell(cell1); return c1.getMapElsTo(cell2); }, - + addCells: function(cell, where) { var c = new TableModifyerByCell(cell); c.add(where); }, - + removeCells: function(cell, what) { var c = new TableModifyerByCell(cell); c.remove(what); }, - + mergeCellsBetween: function(cell1, cell2) { var c1 = new TableModifyerByCell(cell1); c1.merge(cell2); }, - + unmergeCell: function(cell) { var c = new TableModifyerByCell(cell); c.unmerge(); }, - + orderSelectionEnds: function(cell, cell2) { var c = new TableModifyerByCell(cell); return c.orderSelectionEnds(cell2); }, - + indexOf: function(cell) { var c = new TableModifyerByCell(cell); c.setTableMap(); return c.getMapIndex(cell); }, - + findCell: function(table, idx) { var c = new TableModifyerByCell(null, table); return c.getElementAtIndex(idx); + }, + + findRowByCell: function(cell) { + var c = new TableModifyerByCell(cell); + return c.getRowElementsByCell(); + }, + + findColumnByCell: function(cell) { + var c = new TableModifyerByCell(cell); + return c.getColumnElementsByCell(); + }, + + canMerge: function(cell1, cell2) { + var c = new TableModifyerByCell(cell1); + return c.canMerge(cell2); } }; - - - + + + })(wysihtml5); // does a selector query on element or array of elements wysihtml5.dom.query = function(elements, query) { var ret = [], q; - + if (elements.nodeType) { elements = [elements]; } - + for (var e = 0, len = elements.length; e < len; e++) { q = elements[e].querySelectorAll(query); if (q) { for(var i = q.length; i--; ret.unshift(q[i])); } } - return ret; -};/** + return ret; +}; +/** * Fix most common html formatting misbehaviors of browsers implementation when inserting * content via copy & paste contentEditable * * @author Christopher Blum */ @@ -3795,15 +3870,15 @@ // TODO: We probably need more rules here var defaultRules = { // When pasting underlined links <a> into a contentEditable, IE thinks, it has to insert <u> to keep the styling "a u": wysihtml5.dom.replaceWithChildNodes }; - + function cleanPastedHTML(elementOrHtml, rules, context) { rules = rules || defaultRules; context = context || elementOrHtml.ownerDocument || document; - + var element, isString = typeof(elementOrHtml) === "string", method, matches, matchesLength, @@ -3812,27 +3887,28 @@ if (isString) { element = wysihtml5.dom.getAsDom(elementOrHtml, context); } else { element = elementOrHtml; } - + for (i in rules) { matches = element.querySelectorAll(i); method = rules[i]; matchesLength = matches.length; for (; j<matchesLength; j++) { method(matches[j]); } } - + matches = elementOrHtml = rules = null; - + return isString ? element.innerHTML : element; } - + return cleanPastedHTML; -})();/** +})(); +/** * IE and Opera leave an empty paragraph in the contentEditable element after clearing it * * @param {Object} contentEditableElement The contentEditable element to observe for clearing events * @exaple * wysihtml5.quirks.ensureProperClearing(myContentEditableElement); @@ -3867,11 +3943,11 @@ wysihtml5.quirks.getCorrectInnerHTML = function(element) { var innerHTML = element.innerHTML; if (innerHTML.indexOf(TILDE_ESCAPED) === -1) { return innerHTML; } - + var elementsWithTilde = element.querySelectorAll("[href*='~'], [src*='~']"), url, urlToSearch, length, i; @@ -3880,170 +3956,169 @@ urlToSearch = wysihtml5.lang.string(url).replace("~").by(TILDE_ESCAPED); innerHTML = wysihtml5.lang.string(innerHTML).replace(urlToSearch).by(url); } return innerHTML; }; -})(wysihtml5);/** +})(wysihtml5); +/** * Force rerendering of a given element * Needed to fix display misbehaviors of IE * * @param {Element} element The element object which needs to be rerendered * @example * wysihtml5.quirks.redraw(document.body); */ (function(wysihtml5) { var CLASS_NAME = "wysihtml5-quirks-redraw"; - + wysihtml5.quirks.redraw = function(element) { wysihtml5.dom.addClass(element, CLASS_NAME); wysihtml5.dom.removeClass(element, CLASS_NAME); - + // Following hack is needed for firefox to make sure that image resize handles are properly removed try { var doc = element.ownerDocument; doc.execCommand("italic", false, null); doc.execCommand("italic", false, null); } catch(e) {} }; -})(wysihtml5);wysihtml5.quirks.tableCellsSelection = (function() { - - var dom = wysihtml5.dom, - select = { - table: null, - start: null, - end: null, - select: selectCells - }, - editable = null, - selection_class = "wysiwyg-tmp-selected-cell", - moveHandler = null, - upHandler = null, - editor = null; - - function init (element, edit) { - editable = element; - editor = edit; - - dom.observe(editable, "mousedown", function(event) { - var target = event.target, - nodeName = target.nodeName; - if (nodeName == "TD" || nodeName == "TH") { - handleSelectionMousedown(target); - } - - }); - - return select; - } - - function handleSelectionMousedown (target) { - select.start = target; - select.end = target; - select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] }); - - if (select.table) { - removeCellSelections(); - dom.addClass(target, selection_class); - moveHandler = dom.observe(editable, "mousemove", handleMouseMove); - upHandler = dom.observe(editable, "mouseup", handleMouseUp); - } - } - - // remove all selection classes - function removeCellSelections () { - if (editable) { - var selectedCells = editable.querySelectorAll('.' + selection_class); - if (selectedCells.length > 0) { - for (var i = 0; i < selectedCells.length; i++) { - dom.removeClass(selectedCells[i], selection_class); - } +})(wysihtml5); +wysihtml5.quirks.tableCellsSelection = function(editable, editor) { + + var dom = wysihtml5.dom, + select = { + table: null, + start: null, + end: null, + cells: null, + select: selectCells + }, + selection_class = "wysiwyg-tmp-selected-cell", + moveHandler = null, + upHandler = null; + + function init () { + + dom.observe(editable, "mousedown", function(event) { + var target = wysihtml5.dom.getParentElement(event.target, { nodeName: ["TD", "TH"] }); + if (target) { + handleSelectionMousedown(target); } - } - } - - function addSelections (cells) { - for (var i = 0; i < cells.length; i++) { - dom.addClass(cells[i], selection_class); + }); + + return select; } - } - - function handleMouseMove (event) { - var curTable = null, - cell = dom.getParentElement(event.target, { nodeName: ["TD","TH"] }), - selectedCells; - - if (cell && select.table && select.start) { - curTable = dom.getParentElement(cell, { nodeName: ["TABLE"] }); - if (curTable && curTable === select.table) { + + function handleSelectionMousedown (target) { + select.start = target; + select.end = target; + select.cells = [target]; + select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] }); + + if (select.table) { removeCellSelections(); - select.end = cell; - selectedCells = dom.table.getCellsBetween(select.start, cell); - addSelections(selectedCells); + dom.addClass(target, selection_class); + moveHandler = dom.observe(editable, "mousemove", handleMouseMove); + upHandler = dom.observe(editable, "mouseup", handleMouseUp); } } - } - - function handleMouseUp (event) { - moveHandler.stop(); - upHandler.stop(); - editor.fire("tableselect").fire("tableselect:composer"); - setTimeout(function() { - bindSideclick(); - },0); - } - - function bindSideclick () { - var sideClickHandler = dom.observe(editable.ownerDocument, "click", function(event) { - sideClickHandler.stop(); - if (dom.getParentElement(event.target, { nodeName: ["TABLE"] }) != select.table) { - removeCellSelections(); - select.table = null; - select.start = null; - select.end = null; - editor.fire("tableunselect").fire("tableunselect:composer"); + + // remove all selection classes + function removeCellSelections () { + if (editable) { + var selectedCells = editable.querySelectorAll('.' + selection_class); + if (selectedCells.length > 0) { + for (var i = 0; i < selectedCells.length; i++) { + dom.removeClass(selectedCells[i], selection_class); + } + } } - }); - } - - function selectCells (start, end) { - select.start = start; - select.end = end; - select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] }); - selectedCells = dom.table.getCellsBetween(select.start, select.end); - addSelections(selectedCells); - bindSideclick(); + } + + function addSelections (cells) { + for (var i = 0; i < cells.length; i++) { + dom.addClass(cells[i], selection_class); + } + } + + function handleMouseMove (event) { + var curTable = null, + cell = dom.getParentElement(event.target, { nodeName: ["TD","TH"] }); + + if (cell && select.table && select.start) { + curTable = dom.getParentElement(cell, { nodeName: ["TABLE"] }); + if (curTable && curTable === select.table) { + removeCellSelections(); + select.end = cell; + select.cells = dom.table.getCellsBetween(select.start, cell); + if (select.cells.length > 1) { + editor.composer.selection.deselect(); + } + addSelections(select.cells); + } + } + } + + function handleMouseUp (event) { + moveHandler.stop(); + upHandler.stop(); editor.fire("tableselect").fire("tableselect:composer"); - } - - return init; + setTimeout(function() { + bindSideclick(); + },0); + } -})(); + function bindSideclick () { + var sideClickHandler = dom.observe(editable.ownerDocument, "click", function(event) { + sideClickHandler.stop(); + if (dom.getParentElement(event.target, { nodeName: ["TABLE"] }) != select.table) { + removeCellSelections(); + select.table = null; + select.start = null; + select.end = null; + editor.fire("tableunselect").fire("tableunselect:composer"); + } + }); + } + function selectCells (start, end) { + select.start = start; + select.end = end; + select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] }); + selectedCells = dom.table.getCellsBetween(select.start, select.end); + addSelections(selectedCells); + bindSideclick(); + editor.fire("tableselect").fire("tableselect:composer"); + } + + return init(); + +}; (function(wysihtml5) { var RGBA_REGEX = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d\.]+)\s*\)/i, RGB_REGEX = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/i, HEX6_REGEX = /^#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])/i, HEX3_REGEX = /^#([0-9a-f])([0-9a-f])([0-9a-f])/i; - + var param_REGX = function (p) { return new RegExp("(^|\\s|;)" + p + "\\s*:\\s*[^;$]+" , "gi"); }; - + wysihtml5.quirks.styleParser = { - + parseColor: function(stylesStr, paramName) { var paramRegex = param_REGX(paramName), params = stylesStr.match(paramRegex), radix = 10, str, colorMatch; - - if (params) { + + if (params) { for (var i = params.length; i--;) { params[i] = wysihtml5.lang.string(params[i].split(':')[1]).trim(); - } + } str = params[params.length-1]; - + if (RGBA_REGEX.test(str)) { colorMatch = str.match(RGBA_REGEX); } else if (RGB_REGEX.test(str)) { colorMatch = str.match(RGB_REGEX); } else if (HEX6_REGEX.test(str)) { @@ -4055,11 +4130,11 @@ colorMatch.push(1); return wysihtml5.lang.array(colorMatch).map(function(d, idx) { return (idx < 3) ? (parseInt(d, 16) * 16) + parseInt(d, 16): parseFloat(d); }); } - + if (colorMatch) { colorMatch.shift(); if (!colorMatch[3]) { colorMatch.push(1); } @@ -4068,11 +4143,11 @@ }); } } return false; }, - + unparseColor: function(val, props) { if (props) { if (props == "hex") { return (val[0].toString(16).toUpperCase()) + (val[1].toString(16).toUpperCase()) + (val[2].toString(16).toUpperCase()); } else if (props == "hash") { @@ -4083,60 +4158,61 @@ return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")"; } else if (props == "csv") { return val[0] + "," + val[1] + "," + val[2] + "," + val[3]; } } - + if (val[3] && val[3] !== 1) { return "rgba(" + val[0] + "," + val[1] + "," + val[2] + "," + val[3] + ")"; } else { return "rgb(" + val[0] + "," + val[1] + "," + val[2] + ")"; } }, - + parseFontSize: function(stylesStr) { var params = stylesStr.match(param_REGX('font-size')); - if (params) { + if (params) { return wysihtml5.lang.string(params[params.length - 1].split(':')[1]).trim(); } return false; } }; - -})(wysihtml5);/** + +})(wysihtml5); +/** * Selection API * * @example * var selection = new wysihtml5.Selection(editor); */ (function(wysihtml5) { var dom = wysihtml5.dom; - + function _getCumulativeOffsetTop(element) { var top = 0; if (element.parentNode) { do { top += element.offsetTop || 0; element = element.offsetParent; } while (element); } return top; } - + wysihtml5.Selection = Base.extend( /** @scope wysihtml5.Selection.prototype */ { constructor: function(editor, contain, unselectableClass) { // Make sure that our external range library is initialized window.rangy.init(); - + this.editor = editor; this.composer = editor.composer; this.doc = this.composer.doc; this.contain = contain; this.unselectableClass = unselectableClass || false; }, - + /** * Get the current selection as a bookmark to be able to later restore it * * @return {Object} An object that represents the current selection */ @@ -4179,11 +4255,11 @@ * @example * selection.setBefore(myElement); */ setAfter: function(node) { var range = rangy.createRange(this.doc); - + range.setStartAfter(node); range.setEndAfter(node); return this.setSelection(range); }, @@ -4249,22 +4325,22 @@ } else { range = this.getRange(this.doc); return range ? range.commonAncestorContainer : this.doc.body; } }, - + getSelectedOwnNodes: function(controlRange) { var selection, ranges = this.getOwnRanges(), ownNodes = []; - + for (var i = 0, maxi = ranges.length; i < maxi; i++) { ownNodes.push(ranges[i].commonAncestorContainer || this.doc.body); } return ownNodes; }, - + findNodesInSelection: function(nodeTypes) { var ranges = this.getOwnRanges(), nodes = [], curNodes; for (var i = 0, maxi = ranges.length; i < maxi; i++) { curNodes = ranges[i].getNodes([1], function(node) { @@ -4272,49 +4348,49 @@ }); nodes = nodes.concat(curNodes); } return nodes; }, - + containsUneditable: function() { var uneditables = this.getOwnUneditables(), selection = this.getSelection(); - + for (var i = 0, maxi = uneditables.length; i < maxi; i++) { if (selection.containsNode(uneditables[i])) { return true; } } - + return false; }, - + deleteContents: function() { var ranges = this.getOwnRanges(); for (var i = ranges.length; i--;) { ranges[i].deleteContents(); } this.setSelection(ranges[0]); }, - + getPreviousNode: function(node, ignoreEmpty) { if (!node) { var selection = this.getSelection(); node = selection.anchorNode; } - + if (node === this.contain) { return false; } - + var ret = node.previousSibling, parent; - + if (ret === this.contain) { return false; } - + if (ret && ret.nodeType !== 3 && ret.nodeType !== 1) { // do not count comments and other node types ret = this.getPreviousNode(ret, ignoreEmpty); } else if (ret && ret.nodeType === 3 && (/^\s*$/).test(ret.textContent)) { // do not count empty textnodes as previus nodes @@ -4327,112 +4403,108 @@ parent = node.parentNode; if (parent !== this.contain) { ret = this.getPreviousNode(parent, ignoreEmpty); } } - + return (ret !== this.contain) ? ret : false; }, - - - + + + caretIsInTheBeginnig: function() { var selection = this.getSelection(), node = selection.anchorNode, offset = selection.anchorOffset; - + return (offset === 0 && !this.getPreviousNode(node, true)); }, - + caretIsBeforeUneditable: function() { var selection = this.getSelection(), node = selection.anchorNode, offset = selection.anchorOffset; - + if (offset === 0) { var prevNode = this.getPreviousNode(node, true); if (prevNode) { var uneditables = this.getOwnUneditables(); for (var i = 0, maxi = uneditables.length; i < maxi; i++) { if (prevNode === uneditables[i]) { return uneditables[i]; } } } - } + } return false; }, // TODO: has problems in chrome 12. investigate block level and uneditable area inbetween executeAndRestore: function(method, restoreScrollPosition) { var body = this.doc.body, oldScrollTop = restoreScrollPosition && body.scrollTop, oldScrollLeft = restoreScrollPosition && body.scrollLeft, className = "_wysihtml5-temp-placeholder", placeholderHtml = '<span class="' + className + '">' + wysihtml5.INVISIBLE_SPACE + '</span>', - range = this.getRange(this.doc), + range = this.getRange(true), caretPlaceholder, newCaretPlaceholder, nextSibling, prevSibling, node, node2, range2, newRange; - + // Nothing selected, execute and say goodbye if (!range) { method(body, body); return; } - + if (!range.collapsed) { range2 = range.cloneRange(); node2 = range2.createContextualFragment(placeholderHtml); range2.collapse(false); range2.insertNode(node2); range2.detach(); } + node = range.createContextualFragment(placeholderHtml); range.insertNode(node); - + if (node2) { caretPlaceholder = this.contain.querySelectorAll("." + className); range.setStartBefore(caretPlaceholder[0]); range.setEndAfter(caretPlaceholder[caretPlaceholder.length -1]); } this.setSelection(range); - + // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder try { method(range.startContainer, range.endContainer); } catch(e) { setTimeout(function() { throw e; }, 0); } caretPlaceholder = this.contain.querySelectorAll("." + className); if (caretPlaceholder && caretPlaceholder.length) { - newRange = rangy.createRange(this.doc); nextSibling = caretPlaceholder[0].nextSibling; if (caretPlaceholder.length > 1) { prevSibling = caretPlaceholder[caretPlaceholder.length -1].previousSibling; } if (prevSibling && nextSibling) { newRange.setStartBefore(nextSibling); newRange.setEndAfter(prevSibling); - } else { newCaretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE); dom.insert(newCaretPlaceholder).before(caretPlaceholder[0]); newRange.setStartAfter(newCaretPlaceholder); newRange.setEndAfter(newCaretPlaceholder); } this.setSelection(newRange); for (var i = caretPlaceholder.length; i--;) { caretPlaceholder[i].parentNode.removeChild(caretPlaceholder[i]); } - - - - + } else { // fallback for when all hell breaks loose this.contain.focus(); } @@ -4487,17 +4559,17 @@ newRange = rangy.createRange(this.doc); try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {} try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {} try { this.setSelection(newRange); } catch(e3) {} }, - + set: function(node, offset) { var newRange = rangy.createRange(this.doc); newRange.setStart(node, offset || 0); this.setSelection(newRange); }, - + /** * Insert html at the caret position and move the cursor after the inserted html * * @param {String} html HTML string to insert * @example @@ -4505,11 +4577,11 @@ */ insertHTML: function(html) { var range = rangy.createRange(this.doc), node = range.createContextualFragment(html), lastChild = node.lastChild; - + this.insertNode(node); if (lastChild) { this.setAfter(lastChild); } }, @@ -4537,11 +4609,11 @@ var ranges = this.getOwnRanges(), node, nodes = []; if (ranges.length == 0) { return nodes; } - + for (var i = ranges.length; i--;) { node = this.doc.createElement(nodeOptions.nodeName); nodes.push(node); if (nodeOptions.className) { node.className = nodeOptions.className; @@ -4556,30 +4628,30 @@ ranges[i].insertNode(node); } } return nodes; }, - + deblockAndSurround: function(nodeOptions) { var tempElement = this.doc.createElement('div'), range = rangy.createRange(this.doc), tempDivElements, tempElements, firstChild; - + tempElement.className = nodeOptions.className; - + this.composer.commands.exec("formatBlock", nodeOptions.nodeName, nodeOptions.className); tempDivElements = this.contain.querySelectorAll("." + nodeOptions.className); if (tempDivElements[0]) { tempDivElements[0].parentNode.insertBefore(tempElement, tempDivElements[0]); - + range.setStartBefore(tempDivElements[0]); range.setEndAfter(tempDivElements[tempDivElements.length - 1]); tempElements = range.extractContents(); - - while (tempElements.firstChild) { + + while (tempElements.firstChild) { firstChild = tempElements.firstChild; if (firstChild.nodeType == 1 && wysihtml5.dom.hasClass(firstChild, nodeOptions.className)) { while (firstChild.firstChild) { tempElement.appendChild(firstChild.firstChild); } @@ -4590,11 +4662,11 @@ } } } else { tempElement = null; } - + return tempElement; }, /** * Scroll the current caret position into the view @@ -4704,76 +4776,94 @@ return range.getNodes([nodeType], filter); } else { return []; } }, - + fixRangeOverflow: function(range) { - - if (this.contain && range) { - var containment = range.compareNode(this.contain); - - if (containment !== 2) { - if (containment === 1) { - range.setStartBefore(this.contain.firstChild); - } - if (containment === 0) { - range.setEndAfter(this.contain.lastChild); - } - if (containment === 3) { - range.setStartBefore(this.contain.firstChild); - range.setEndAfter(this.contain.lastChild); - } - - } + if (this.contain && this.contain.firstChild && range) { + var containment = range.compareNode(this.contain); + if (containment !== 2) { + if (containment === 1) { + range.setStartBefore(this.contain.firstChild); + } + if (containment === 0) { + range.setEndAfter(this.contain.lastChild); + } + if (containment === 3) { + range.setStartBefore(this.contain.firstChild); + range.setEndAfter(this.contain.lastChild); + } + } else if (this._detectInlineRangeProblems(range)) { + var previousElementSibling = range.endContainer.previousElementSibling; + if (previousElementSibling) { + range.setEnd(previousElementSibling, this._endOffsetForNode(previousElementSibling)); + } } - + } }, - - getRange: function() { + + _endOffsetForNode: function(node) { + var range = document.createRange() + range.selectNodeContents(node) + return range.endOffset; + }, + + _detectInlineRangeProblems: function(range) { + position = range.startContainer.compareDocumentPosition(range.endContainer); + return ( + range.endOffset == 0 && + position & Node.DOCUMENT_POSITION_FOLLOWING + ); + }, + + getRange: function(dontFix) { var selection = this.getSelection(), range = selection && selection.rangeCount && selection.getRangeAt(0); - this.fixRangeOverflow(range); - + + if (dontFix !== true) { + this.fixRangeOverflow(range); + } + return range; }, - + getOwnUneditables: function() { var allUneditables = dom.query(this.contain, '.' + this.unselectableClass), deepUneditables = dom.query(allUneditables, '.' + this.unselectableClass); - + return wysihtml5.lang.array(allUneditables).without(deepUneditables); }, - + // Returns an array of ranges that belong only to this editable // Needed as uneditable block in contenteditabel can split range into pieces // If manipulating content reverse loop is usually needed as manipulation can shift subsequent ranges getOwnRanges: function() { var ranges = [], r = this.getRange(), tmpRanges; - - if (r) { ranges.push(r); } - + + if (r) { ranges.push(r); } + if (this.unselectableClass && this.contain && r) { var uneditables = this.getOwnUneditables(), tmpRange; if (uneditables.length > 0) { for (var i = 0, imax = uneditables.length; i < imax; i++) { tmpRanges = []; for (var j = 0, jmax = ranges.length; j < jmax; j++) { if (ranges[j]) { switch (ranges[j].compareNode(uneditables[i])) { - case 2: + case 2: // all selection inside uneditable. remove break; case 3: //section begins before and ends after uneditable. spilt tmpRange = ranges[j].cloneRange(); tmpRange.setEndBefore(uneditables[i]); tmpRanges.push(tmpRange); - + tmpRange = ranges[j].cloneRange(); tmpRange.setStartAfter(uneditables[i]); tmpRanges.push(tmpRange); break; default: @@ -4796,21 +4886,25 @@ setSelection: function(range) { var win = this.doc.defaultView || this.doc.parentWindow, selection = rangy.getSelection(win); return selection.setSingleRange(range); }, - + createRange: function() { return rangy.createRange(this.doc); }, - + isCollapsed: function() { return this.getSelection().isCollapsed; + }, + + deselect: function() { + var sel = this.getSelection(); + sel && sel.removeAllRanges(); } - }); - + })(wysihtml5); /** * Inspired by the rangy CSS Applier module written by Tim Down and licensed under the MIT license. * http://code.google.com/p/rangy/ * @@ -4818,43 +4912,43 @@ * - to use custom tags * - to detect and replace similar css classes via reg exp */ (function(wysihtml5, rangy) { var defaultTagName = "span"; - + var REG_EXP_WHITE_SPACE = /\s+/g; - + function hasClass(el, cssClass, regExp) { if (!el.className) { return false; } - + var matchingClassNames = el.className.match(regExp) || []; return matchingClassNames[matchingClassNames.length - 1] === cssClass; } - + function hasStyleAttr(el, regExp) { if (!el.getAttribute || !el.getAttribute('style')) { return false; } var matchingStyles = el.getAttribute('style').match(regExp); return (el.getAttribute('style').match(regExp)) ? true : false; } - + function addStyle(el, cssStyle, regExp) { if (el.getAttribute('style')) { removeStyle(el, regExp); if (el.getAttribute('style') && !(/\s+/).test(el.getAttribute('style'))) { el.setAttribute('style', cssStyle + ";" + el.getAttribute('style')); } else { - + el.setAttribute('style', cssStyle); } } else { el.setAttribute('style', cssStyle); } - } + } function addClass(el, cssClass, regExp) { if (el.className) { removeClass(el, regExp); el.className += " " + cssClass; @@ -4866,11 +4960,11 @@ function removeClass(el, regExp) { if (el.className) { el.className = el.className.replace(regExp, ""); } } - + function removeStyle(el, regExp) { var s, s2 = []; if (el.getAttribute('style')) { s = el.getAttribute('style').split(';'); @@ -4884,20 +4978,20 @@ } else { el.removeAttribute('style'); } } } - + function getMatchingStyleRegexp(el, style) { var regexes = [], sSplit = style.split(';'), elStyle = el.getAttribute('style'); - + if (elStyle) { - elStyle = elStyle.replace(/\s/gi, '').toLowerCase(); + elStyle = elStyle.replace(/\s/gi, '').toLowerCase(); regexes.push(new RegExp("(^|\\s|;)" + style.replace(/\s/gi, '').replace(/([\(\)])/gi, "\\$1").toLowerCase().replace(";", ";?"), "gi")); - + for (var i = sSplit.length; i-- > 0;) { if (!(/^\s*$/).test(sSplit[i])) { regexes.push(new RegExp("(^|\\s|;)" + sSplit[i].replace(/\s/gi, '').replace(/([\(\)])/gi, "\\$1").toLowerCase().replace(";", ";?"), "gi")); } } @@ -4905,32 +4999,32 @@ if (elStyle.match(regexes[j])) { return regexes[j]; } } } - + return false; }; - + function removeOrChangeStyle(el, style, regExp) { - + var exactRegex = getMatchingStyleRegexp(el, style); - + /*new RegExp("(^|\\s|;)" + style.replace(/\s/gi, '').replace(/([\(\)])/gi, "\\$1").toLowerCase().replace(";", ";?"), "gi"), elStyle = el.getAttribute('style');*/ - + if (exactRegex) { // adding same style value on property again removes style removeStyle(el, exactRegex); return "remove"; } else { // adding new style value changes value addStyle(el, style, regExp); return "change"; } }; - + function hasSameClasses(el1, el2) { return el1.className.replace(REG_EXP_WHITE_SPACE, " ") == el2.className.replace(REG_EXP_WHITE_SPACE, " "); } function replaceWithOwnChildren(el) { @@ -5002,11 +5096,11 @@ } rangy.dom.insertAfter(newNode, descendantNode); } return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode)); } - + function Merge(firstNode) { this.isElementMerge = (firstNode.nodeType == wysihtml5.ELEMENT_NODE); this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; this.textNodes = [this.firstTextNode]; } @@ -5059,24 +5153,25 @@ HTMLApplier.prototype = { getAncestorWithClass: function(node) { var cssClassMatch; while (node) { cssClassMatch = this.cssClass ? hasClass(node, this.cssClass, this.similarClassRegExp) : (this.cssStyle !== "") ? false : true; - if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) { + if (node.nodeType == wysihtml5.ELEMENT_NODE && node.getAttribute("contenteditable") != "false" && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssClassMatch) { return node; } node = node.parentNode; } return false; }, - + // returns parents of node with given style attribute getAncestorWithStyle: function(node) { var cssStyleMatch; while (node) { cssStyleMatch = this.cssStyle ? hasStyleAttr(node, this.similarStyleRegExp) : false; - if (node.nodeType == wysihtml5.ELEMENT_NODE && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssStyleMatch) { + + if (node.nodeType == wysihtml5.ELEMENT_NODE && node.getAttribute("contenteditable") != "false" && rangy.dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && cssStyleMatch) { return node; } node = node.parentNode; } return false; @@ -5136,11 +5231,11 @@ // Set the range boundaries range.setStart(rangeStartNode, rangeStartOffset); range.setEnd(rangeEndNode, rangeEndOffset); } }, - + getAdjacentMergeableTextNode: function(node, forward) { var isTextNode = (node.nodeType == wysihtml5.TEXT_NODE); var el = isTextNode ? node.parentNode : node; var adjacentNode; var propName = forward ? "nextSibling" : "previousSibling"; @@ -5157,11 +5252,11 @@ return adjacentNode[forward ? "firstChild" : "lastChild"]; } } return null; }, - + areElementsMergeable: function(el1, el2) { return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase()) && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase()) && hasSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2); @@ -5179,11 +5274,11 @@ }, applyToTextNode: function(textNode) { var parent = textNode.parentNode; if (parent.childNodes.length == 1 && rangy.dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) { - + if (this.cssClass) { addClass(parent, this.cssClass, this.similarClassRegExp); } } else { var el = this.createContainer(rangy.dom.getDocument(textNode)); @@ -5202,47 +5297,47 @@ styleChanged = false; if (!range.containsNode(ancestor)) { // Split out the portion of the ancestor from which we can remove the CSS class var ancestorRange = range.cloneRange(); ancestorRange.selectNode(ancestor); - + if (ancestorRange.isPointInRange(range.endContainer, range.endOffset) && isSplitPoint(range.endContainer, range.endOffset)) { splitNodeAt(ancestor, range.endContainer, range.endOffset); range.setEndAfter(ancestor); } if (ancestorRange.isPointInRange(range.startContainer, range.startOffset) && isSplitPoint(range.startContainer, range.startOffset)) { ancestor = splitNodeAt(ancestor, range.startContainer, range.startOffset); } } - + if (!styleMode && this.similarClassRegExp) { removeClass(ancestor, this.similarClassRegExp); } - + if (styleMode && this.similarStyleRegExp) { styleChanged = (removeOrChangeStyle(ancestor, this.cssStyle, this.similarStyleRegExp) === "change"); } - + if (this.isRemovable(ancestor) && !styleChanged) { replaceWithOwnChildren(ancestor); } }, applyToRange: function(range) { var textNodes; - for (var ri = range.length; ri--;) { + for (var ri = range.length; ri--;) { textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); - + if (!textNodes.length) { try { var node = this.createContainer(range[ri].endContainer.ownerDocument); range[ri].surroundContents(node); this.selectNode(range[ri], node); return; } catch(e) {} } - + range[ri].splitBoundaries(); textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); if (textNodes.length) { var textNode; @@ -5250,26 +5345,26 @@ textNode = textNodes[i]; if (!this.getAncestorWithClass(textNode)) { this.applyToTextNode(textNode); } } - + range[ri].setStart(textNodes[0], 0); textNode = textNodes[textNodes.length - 1]; range[ri].setEnd(textNode, textNode.length); - + if (this.normalize) { this.postApply(textNodes, range[ri]); } } - + } }, undoToRange: function(range) { var textNodes, textNode, ancestorWithClass, ancestorWithStyle; - + for (var ri = range.length; ri--;) { textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); if (textNodes.length) { range[ri].splitBoundaries(); textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); @@ -5278,12 +5373,12 @@ node = doc.createTextNode(wysihtml5.INVISIBLE_SPACE); range[ri].insertNode(node); range[ri].selectNode(node); textNodes = [node]; } - - + + for (var i = 0, len = textNodes.length; i < len; ++i) { if (range[ri].isValid()) { textNode = textNodes[i]; ancestorWithClass = this.getAncestorWithClass(textNode); ancestorWithStyle = this.getAncestorWithStyle(textNode); @@ -5292,11 +5387,11 @@ } else if (ancestorWithStyle) { this.undoToTextNode(textNode, range[ri], false, ancestorWithStyle); } } } - + if (len == 1) { this.selectNode(range[ri], textNodes[0]); } else { range[ri].setStart(textNodes[0], 0); textNode = textNodes[textNodes.length - 1]; @@ -5304,14 +5399,14 @@ if (this.normalize) { this.postApply(textNodes, range[ri]); } } - + } }, - + selectNode: function(range, node) { var isElement = node.nodeType === wysihtml5.ELEMENT_NODE, canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true, content = isElement ? node.innerHTML : node.data, isEmpty = (content === "" || content === wysihtml5.INVISIBLE_SPACE); @@ -5326,11 +5421,11 @@ } else if (isEmpty) { range.setStartAfter(node); range.setEndAfter(node); } }, - + getTextSelectedByRange: function(textNode, range) { var textRange = range.cloneRange(); textRange.selectNodeContents(textNode); var intersectionRange = textRange.intersection(range); @@ -5341,21 +5436,21 @@ }, isAppliedToRange: function(range) { var ancestors = [], ancestor, styleAncestor, textNodes; - + for (var ri = range.length; ri--;) { textNodes = range[ri].getNodes([wysihtml5.TEXT_NODE]); if (!textNodes.length) { ancestor = this.getAncestorWithClass(range[ri].startContainer); if (!ancestor) { ancestor = this.getAncestorWithStyle(range[ri].startContainer); } return ancestor ? [ancestor] : false; } - + for (var i = 0, len = textNodes.length, selectedText; i < len; ++i) { selectedText = this.getTextSelectedByRange(textNodes[i], range[ri]); ancestor = this.getAncestorWithClass(textNodes[i]); if (!ancestor) { ancestor = this.getAncestorWithStyle(textNodes[i]); @@ -5363,11 +5458,11 @@ if (!(selectedText != "" && !ancestor)) { ancestors.push(ancestor); } } } - + return (ancestors.length) ? ancestors : false; }, toggleRange: function(range) { if (this.isAppliedToRange(range)) { @@ -5377,36 +5472,37 @@ } } }; wysihtml5.selection.HTMLApplier = HTMLApplier; - -})(wysihtml5, rangy);/** + +})(wysihtml5, rangy); +/** * Rich Text Query/Formatting Commands - * + * * @example * var commands = new wysihtml5.Commands(editor); */ wysihtml5.Commands = Base.extend( /** @scope wysihtml5.Commands.prototype */ { constructor: function(editor) { this.editor = editor; this.composer = editor.composer; this.doc = this.composer.doc; }, - + /** * Check whether the browser supports the given command * * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") * @example * commands.supports("createLink"); */ support: function(command) { return wysihtml5.browser.supportsCommand(this.doc, command); }, - + /** * Check whether the browser supports the given command * * @param {String} command The command string which to execute (eg. "bold", "italic", "insertUnorderedList") * @param {String} [value] The command value parameter, needed for some commands ("createLink", "insertImage", ...), optional for commands that don't require one ("bold", "underline", ...) @@ -5416,27 +5512,27 @@ exec: function(command, value) { var obj = wysihtml5.commands[command], args = wysihtml5.lang.array(arguments).get(), method = obj && obj.exec, result = null; - + this.editor.fire("beforecommand:composer"); - + if (method) { args.unshift(this.composer); result = method.apply(obj, args); } else { try { // try/catch for buggy firefox result = this.doc.execCommand(command, false, value); } catch(e) {} } - + this.editor.fire("aftercommand:composer"); return result; }, - + /** * Check whether the current command is active * If the caret is within a bold text, then calling this with command "bold" should return true * * @param {String} command The command string which to check (eg. "bold", "italic", "insertUnorderedList") @@ -5459,11 +5555,11 @@ } catch(e) { return false; } } }, - + /* Get command state parsed value if command has stateValue parsing function */ stateValue: function(command) { var obj = wysihtml5.commands[command], args = wysihtml5.lang.array(arguments).get(), method = obj && obj.stateValue; @@ -5471,11 +5567,11 @@ args.unshift(this.composer); return method.apply(obj, args); } else { return false; } - } + } }); wysihtml5.commands.bold = { exec: function(composer, command) { wysihtml5.commands.formatInline.execWithToggle(composer, command, "b"); }, @@ -5536,39 +5632,39 @@ elementToSetCaretAfter = whiteSpace; } } composer.selection.setAfter(elementToSetCaretAfter); } - + // Changes attributes of links function _changeLinks(composer, anchors, attributes) { var oldAttrs; for (var a = anchors.length; a--;) { - + // Remove all old attributes oldAttrs = anchors[a].attributes; for (var oa = oldAttrs.length; oa--;) { anchors[a].removeAttribute(oldAttrs.item(oa).name); } - + // Set new attributes for (var j in attributes) { if (attributes.hasOwnProperty(j)) { anchors[a].setAttribute(j, attributes[j]); - } + } } - + } } - + wysihtml5.commands.createLink = { /** * TODO: Use HTMLApplier or formatInline here * * Turns selection into a link * If selection is already a link, it just changes the attributes - * + * * @example * // either ... * wysihtml5.commands.createLink.exec(composer, "createLink", "http://www.google.de"); * // ... or ... * wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" }); @@ -5589,13 +5685,14 @@ state: function(composer, command) { return wysihtml5.commands.formatInline.state(composer, command, "A"); } }; -})(wysihtml5);(function(wysihtml5) { +})(wysihtml5); +(function(wysihtml5) { var dom = wysihtml5.dom; - + function _removeFormat(composer, anchors) { var length = anchors.length, i = 0, anchor, codeElement, @@ -5613,20 +5710,20 @@ } else { dom.replaceWithChildNodes(anchor); } } } - + wysihtml5.commands.removeLink = { /* * If selection is a link, it removes the link and wraps it with a <code> element * The <code> element is needed to avoid auto linking - * + * * @example * wysihtml5.commands.createLink.exec(composer, "removeLink"); */ - + exec: function(composer, command) { var anchors = this.state(composer, command); if (anchors) { composer.selection.executeAndRestore(function() { _removeFormat(composer, anchors); @@ -5636,18 +5733,19 @@ state: function(composer, command) { return wysihtml5.commands.formatInline.state(composer, command, "A"); } }; -})(wysihtml5);/** +})(wysihtml5); +/** * document.execCommand("fontSize") will create either inline styles (firefox, chrome) or use font tags * which we don't want * Instead we set a css class */ (function(wysihtml5) { var REG_EXP = /wysiwyg-font-size-[0-9a-z\-]+/g; - + wysihtml5.commands.fontSize = { exec: function(composer, command, size) { wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", "wysiwyg-font-size-" + size, REG_EXP); }, @@ -5657,11 +5755,11 @@ }; })(wysihtml5); /* In case font size adjustment to any number defined by user is preferred, we cannot use classes and must use inline styles. */ (function(wysihtml5) { var REG_EXP = /(\s|^)font-size\s*:\s*[^;\s]+;?/gi; - + wysihtml5.commands.fontSizeStyle = { exec: function(composer, command, size) { size = (typeof(size) == "object") ? size.size : size; if (!(/^\s*$/).test(size)) { wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, "font-size:" + size, REG_EXP); @@ -5669,16 +5767,16 @@ }, state: function(composer, command, size) { return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "font-size", REG_EXP); }, - + stateValue: function(composer, command) { var st = this.state(composer, command), styleStr, fontsizeMatches, val = false; - + if (st && wysihtml5.lang.object(st).isArray()) { st = st[0]; } if (st) { styleStr = st.getAttribute('style'); @@ -5687,40 +5785,42 @@ } } return false; } }; -})(wysihtml5);/** +})(wysihtml5); +/** * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags * which we don't want * Instead we set a css class */ (function(wysihtml5) { var REG_EXP = /wysiwyg-color-[0-9a-z]+/g; - + wysihtml5.commands.foreColor = { exec: function(composer, command, color) { wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", "wysiwyg-color-" + color, REG_EXP); }, state: function(composer, command, color) { return wysihtml5.commands.formatInline.state(composer, command, "span", "wysiwyg-color-" + color, REG_EXP); } }; -})(wysihtml5);/** +})(wysihtml5); +/** * document.execCommand("foreColor") will create either inline styles (firefox, chrome) or use font tags * which we don't want * Instead we set a css class */ (function(wysihtml5) { var REG_EXP = /(\s|^)color\s*:\s*[^;\s]+;?/gi; - + wysihtml5.commands.foreColorStyle = { exec: function(composer, command, color) { var colorVals = wysihtml5.quirks.styleParser.parseColor((typeof(color) == "object") ? "color:" + color.color : "color:" + color, "color"), colString; - + if (colorVals) { colString = "color: rgb(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ');'; if (colorVals[3] !== 1) { colString += "color: rgba(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ',' + colorVals[3] + ');'; } @@ -5729,19 +5829,19 @@ }, state: function(composer, command) { return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "color", REG_EXP); }, - + stateValue: function(composer, command, props) { var st = this.state(composer, command), colorStr; - + if (st && wysihtml5.lang.object(st).isArray()) { st = st[0]; } - + if (st) { colorStr = st.getAttribute('style'); if (colorStr) { if (colorStr) { val = wysihtml5.quirks.styleParser.parseColor(colorStr, "color"); @@ -5749,61 +5849,63 @@ } } } return false; } - + }; -})(wysihtml5);/* In case background adjustment to any color defined by user is preferred, we cannot use classes and must use inline styles. */ +})(wysihtml5); +/* In case background adjustment to any color defined by user is preferred, we cannot use classes and must use inline styles. */ (function(wysihtml5) { var REG_EXP = /(\s|^)background-color\s*:\s*[^;\s]+;?/gi; - + wysihtml5.commands.bgColorStyle = { exec: function(composer, command, color) { var colorVals = wysihtml5.quirks.styleParser.parseColor((typeof(color) == "object") ? "background-color:" + color.color : "background-color:" + color, "background-color"), colString; - + if (colorVals) { colString = "background-color: rgb(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ');'; if (colorVals[3] !== 1) { colString += "background-color: rgba(" + colorVals[0] + ',' + colorVals[1] + ',' + colorVals[2] + ',' + colorVals[3] + ');'; } wysihtml5.commands.formatInline.execWithToggle(composer, command, "span", false, false, colString, REG_EXP); } }, - + state: function(composer, command) { return wysihtml5.commands.formatInline.state(composer, command, "span", false, false, "background-color", REG_EXP); }, - + stateValue: function(composer, command, props) { var st = this.state(composer, command), - colorStr, + colorStr, val = false; - + if (st && wysihtml5.lang.object(st).isArray()) { st = st[0]; } - + if (st) { colorStr = st.getAttribute('style'); if (colorStr) { val = wysihtml5.quirks.styleParser.parseColor(colorStr, "background-color"); return wysihtml5.quirks.styleParser.unparseColor(val, props); } } return false; } - + }; -})(wysihtml5);(function(wysihtml5) { +})(wysihtml5); +(function(wysihtml5) { var dom = wysihtml5.dom, // Following elements are grouped // when the caret is within a H1 and the H4 is invoked, the H1 should turn into H4 // instead of creating a H4 within a H1 which would result in semantically invalid html BLOCK_ELEMENTS_GROUP = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "PRE", "BLOCKQUOTE", "DIV"]; - + /** * Remove similiar classes (based on classRegExp) * and add the desired class name */ function _addClass(element, className, classRegExp) { @@ -5934,60 +6036,60 @@ target.className += " " + className; } }); } doc.execCommand(command, false, nodeName); - + if (eventListener) { eventListener.stop(); } } } function _selectionWrap(composer, options) { if (composer.selection.isCollapsed()) { composer.selection.selectLine(); } - + var surroundedNodes = composer.selection.surround(options); for (var i = 0, imax = surroundedNodes.length; i < imax; i++) { _removeLineBreakBeforeAndAfter(surroundedNodes[i]); _removeLastChildIfLineBreak(surroundedNodes[i]); } - + // rethink restoring selection // composer.selection.selectNode(element, wysihtml5.browser.displaysCaretInEmptyContentEditableCorrectly()); } function _hasClasses(element) { return !!wysihtml5.lang.string(element.className).trim(); } - + wysihtml5.commands.formatBlock = { exec: function(composer, command, nodeName, className, classRegExp) { var doc = composer.doc, blockElements = this.state(composer, command, nodeName, className, classRegExp), useLineBreaks = composer.config.useLineBreaks, defaultNodeName = useLineBreaks ? "DIV" : "P", selectedNodes, classRemoveAction, blockRenameFound; nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; - + if (blockElements.length) { composer.selection.executeAndRestoreSimple(function() { for (var b = blockElements.length; b--;) { if (classRegExp) { classRemoveAction = _removeClass(blockElements[b], classRegExp); } - + if (classRemoveAction && nodeName === null && blockElements[b].nodeName != defaultNodeName) { // dont rename or remove element when just setting block formating class return; } - + var hasClasses = _hasClasses(blockElements[b]); - + if (!hasClasses && (useLineBreaks || nodeName === "P")) { // Insert a line break afterwards and beforewards when there are siblings // that are not of type line break or block element _addLineBreakBeforeAndAfter(blockElements[b]); dom.replaceWithChildNodes(blockElements[b]); @@ -5995,14 +6097,14 @@ // Make sure that styling is kept by renaming the element to a <div> or <p> and copying over the class name dom.renameElement(blockElements[b], nodeName === "P" ? "DIV" : defaultNodeName); } } }); - + return; } - + // Find similiar block element and rename it (<h2 class="foo"></h2> => <h1 class="foo"></h1>) if (nodeName === null || wysihtml5.lang.array(BLOCK_ELEMENTS_GROUP).contains(nodeName)) { selectedNodes = composer.selection.findNodesInSelection(BLOCK_ELEMENTS_GROUP).concat(composer.selection.getSelectedOwnNodes()); composer.selection.executeAndRestoreSimple(function() { for (var n = selectedNodes.length; n--;) { @@ -6018,17 +6120,17 @@ blockElement = dom.renameElement(blockElement, nodeName); } if (className) { _addClass(blockElement, className, classRegExp); } - + blockRenameFound = true; } } - + }); - + if (blockRenameFound) { return; } } @@ -6042,23 +6144,23 @@ // Native command does not create elements from selecton boundaries. // Not quite user expected behaviour if (composer.commands.support(command)) { _execCommand(doc, composer, command, nodeName || defaultNodeName, className); return; - } + } } - + }, state: function(composer, command, nodeName, className, classRegExp) { var nodes = composer.selection.getSelectedOwnNodes(), parents = [], parent; - + nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; - + //var selectedNode = composer.selection.getSelectedNode(); for (var i = 0, maxi = nodes.length; i < maxi; i++) { parent = dom.getParentElement(nodes[i], { nodeName: nodeName, className: className, @@ -6071,26 +6173,27 @@ if (parents.length == 0) { return false; } return parents; } - - + + }; -})(wysihtml5);/** +})(wysihtml5); +/** * formatInline scenarios for tag "B" (| = caret, |foo| = selected text) * * #1 caret in unformatted text: * abcdefg| * output: * abcdefg<b>|</b> - * + * * #2 unformatted text selected: * abc|deg|h * output: * abc<b>|deg|</b>h - * + * * #3 unformatted text selected across boundaries: * ab|c <span>defg|h</span> * output: * ab<b>|c </b><span><b>defg</b>|h</span> * @@ -6116,32 +6219,32 @@ "em": "i", "b": "strong", "i": "em" }, htmlApplier = {}; - + function _getTagNames(tagName) { var alias = ALIAS_MAPPING[tagName]; return alias ? [tagName.toLowerCase(), alias.toLowerCase()] : [tagName.toLowerCase()]; } - + function _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp) { var identifier = tagName + ":" + className; if (cssStyle) { - identifier += ":" + cssStyle + identifier += ":" + cssStyle; } if (!htmlApplier[identifier]) { htmlApplier[identifier] = new wysihtml5.selection.HTMLApplier(_getTagNames(tagName), className, classRegExp, true, cssStyle, styleRegExp); } return htmlApplier[identifier]; } - + wysihtml5.commands.formatInline = { exec: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, dontRestoreSelect, noCleanup) { var range = composer.selection.createRange(); ownRanges = composer.selection.getOwnRanges(); - + if (!ownRanges || ownRanges.length == 0) { return false; } composer.selection.getSelection().removeAllRanges(); _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp).toggleRange(ownRanges); @@ -6159,22 +6262,22 @@ } else { composer.cleanUp(); } } }, - + // Executes so that if collapsed caret is in a state and executing that state it should unformat that state // It is achieved by selecting the entire state element before executing. // This works on built in contenteditable inline format commands execWithToggle: function(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) { var that = this; if (this.state(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp) && composer.selection.isCollapsed()) { var state_element = that.state(composer, command, tagName, className, classRegExp)[0]; composer.selection.executeAndRestoreSimple(function() { var parent = state_element.parentNode; composer.selection.selectNode(state_element, true); - wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, true); + wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp, true, true); }); } else { wysihtml5.commands.formatInline.exec(composer, command, tagName, className, classRegExp, cssStyle, styleRegExp); } }, @@ -6194,19 +6297,20 @@ if (className && !wysihtml5.dom.hasElementWithClassName(doc, className)) { return false; } ownRanges = composer.selection.getOwnRanges(); - + if (ownRanges.length == 0) { return false; } - + return _getApplier(tagName, className, classRegExp, cssStyle, styleRegExp).isAppliedToRange(ownRanges); } }; -})(wysihtml5);wysihtml5.commands.insertHTML = { +})(wysihtml5); +wysihtml5.commands.insertHTML = { exec: function(composer, command, html) { if (composer.commands.support(command)) { composer.doc.execCommand(command, false, html); } else { composer.selection.insertHTML(html); @@ -6217,16 +6321,16 @@ return false; } }; (function(wysihtml5) { var NODE_NAME = "IMG"; - + wysihtml5.commands.insertImage = { /** * Inserts an <img> * If selection is already an image link, it removes it - * + * * @example * // either ... * wysihtml5.commands.insertImage.exec(composer, "insertImage", "http://www.google.de/logo.jpg"); * // ... or ... * wysihtml5.commands.insertImage.exec(composer, "insertImage", { src: "http://www.google.de/logo.jpg", title: "foo" }); @@ -6256,11 +6360,11 @@ wysihtml5.quirks.redraw(composer.element); return; } image = doc.createElement(NODE_NAME); - + for (var i in value) { image.setAttribute(i === "className" ? "class" : i, value[i]); } composer.selection.insertNode(image); @@ -6312,13 +6416,14 @@ } return imagesInSelection[0]; } }; -})(wysihtml5);(function(wysihtml5) { +})(wysihtml5); +(function(wysihtml5) { var LINE_BREAK = "<br>" + (wysihtml5.browser.needsSpaceAfterLineBreak() ? " " : ""); - + wysihtml5.commands.insertLineBreak = { exec: function(composer, command) { if (composer.commands.support(command)) { composer.doc.execCommand(command, false, null); if (!wysihtml5.browser.autoScrollsToCaret()) { @@ -6331,25 +6436,34 @@ state: function() { return false; } }; -})(wysihtml5);wysihtml5.commands.insertOrderedList = { +})(wysihtml5); +wysihtml5.commands.insertOrderedList = { exec: function(composer, command) { var doc = composer.doc, selectedNode = composer.selection.getSelectedNode(), list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), tempClassName = "_wysihtml5-temp-" + new Date().getTime(), isEmpty, tempElement; - + + // do not count list elements outside of composer + if (list && !composer.element.contains(list)) { + list = null + } + if (otherList && !composer.element.contains(otherList)) { + otherList = null + } + if (!list && !otherList && composer.commands.support(command)) { doc.execCommand(command, false, null); return; } - + if (list) { // Unwrap list // <ol><li>foo</li><li>bar</li></ol> // becomes: // foo<br>bar<br> @@ -6379,30 +6493,41 @@ composer.selection.selectNode(list.querySelector("li"), true); } } } }, - + state: function(composer) { - var selectedNode = composer.selection.getSelectedNode(); - return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }); + var selectedNode = composer.selection.getSelectedNode(), + node = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }); + + return (composer.element.contains(node) ? node : false); } -};wysihtml5.commands.insertUnorderedList = { +}; +wysihtml5.commands.insertUnorderedList = { exec: function(composer, command) { var doc = composer.doc, selectedNode = composer.selection.getSelectedNode(), list = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }), otherList = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "OL" }), tempClassName = "_wysihtml5-temp-" + new Date().getTime(), isEmpty, tempElement; - + + // do not count list elements outside of composer + if (list && !composer.element.contains(list)) { + list = null + } + if (otherList && !composer.element.contains(otherList)) { + otherList = null + } + if (!list && !otherList && composer.commands.support(command)) { doc.execCommand(command, false, null); return; } - + if (list) { // Unwrap list // <ul><li>foo</li><li>bar</li></ul> // becomes: // foo<br>bar<br> @@ -6432,16 +6557,19 @@ composer.selection.selectNode(list.querySelector("li"), true); } } } }, - + state: function(composer) { - var selectedNode = composer.selection.getSelectedNode(); - return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }); + var selectedNode = composer.selection.getSelectedNode(), + node = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "UL" }); + + return (composer.element.contains(node) ? node : false); } -};wysihtml5.commands.italic = { +}; +wysihtml5.commands.italic = { exec: function(composer, command) { wysihtml5.commands.formatInline.execWithToggle(composer, command, "i"); }, state: function(composer, command) { @@ -6450,53 +6578,57 @@ // chrome: <i>, <em>, <blockquote>, ... // ie: <i>, <em> // opera: only <i> return wysihtml5.commands.formatInline.state(composer, command, "i"); } -};(function(wysihtml5) { +}; +(function(wysihtml5) { var CLASS_NAME = "wysiwyg-text-align-center", REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; - + wysihtml5.commands.justifyCenter = { exec: function(composer, command) { return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); }, state: function(composer, command) { return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); } }; -})(wysihtml5);(function(wysihtml5) { +})(wysihtml5); +(function(wysihtml5) { var CLASS_NAME = "wysiwyg-text-align-left", REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; - + wysihtml5.commands.justifyLeft = { exec: function(composer, command) { return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); }, state: function(composer, command) { return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); } }; -})(wysihtml5);(function(wysihtml5) { +})(wysihtml5); +(function(wysihtml5) { var CLASS_NAME = "wysiwyg-text-align-right", REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; - + wysihtml5.commands.justifyRight = { exec: function(composer, command) { return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); }, state: function(composer, command) { return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); } }; -})(wysihtml5);(function(wysihtml5) { +})(wysihtml5); +(function(wysihtml5) { var CLASS_NAME = "wysiwyg-text-align-justify", REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; - + wysihtml5.commands.justifyFull = { exec: function(composer, command) { return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); }, @@ -6511,50 +6643,59 @@ }, state: function(composer) { return false; } -};wysihtml5.commands.underline = { +}; +wysihtml5.commands.underline = { exec: function(composer, command) { wysihtml5.commands.formatInline.execWithToggle(composer, command, "u"); }, state: function(composer, command) { return wysihtml5.commands.formatInline.state(composer, command, "u"); } -};wysihtml5.commands.undo = { +}; +wysihtml5.commands.undo = { exec: function(composer) { return composer.undoManager.undo(); }, state: function(composer) { return false; } -};wysihtml5.commands.createTable = { +}; +wysihtml5.commands.createTable = { exec: function(composer, command, value) { var col, row, html; if (value && value.cols && value.rows && parseInt(value.cols, 10) > 0 && parseInt(value.rows, 10) > 0) { - html = "<table><tbody>"; + if (value.tableStyle) { + html = "<table style=\"" + value.tableStyle + "\">"; + } else { + html = "<table>"; + } + html += "<tbody>"; for (row = 0; row < value.rows; row ++) { html += '<tr>'; for (col = 0; col < value.cols; col ++) { html += "<td>&nbsp;</td>"; } html += '</tr>'; } html += "</tbody></table>"; composer.commands.exec("insertHTML", html); //composer.selection.insertHTML(html); - } - - + } + + }, state: function(composer, command) { return false; } -};wysihtml5.commands.mergeTableCells = { +}; +wysihtml5.commands.mergeTableCells = { exec: function(composer, command) { if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) { if (this.state(composer, command)) { wysihtml5.dom.table.unmergeCell(composer.tableSelection.start); } else { @@ -6579,14 +6720,15 @@ return [start]; } } return false; } -};wysihtml5.commands.addTableCells = { +}; +wysihtml5.commands.addTableCells = { exec: function(composer, command, value) { if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) { - + // switches start and end if start is bigger than end (reverse selection) var tableSelect = wysihtml5.dom.table.orderSelectionEnds(composer.tableSelection.start, composer.tableSelection.end); if (value == "before" || value == "above") { wysihtml5.dom.table.addCells(tableSelect.start, value); } else if (value == "after" || value == "below") { @@ -6599,38 +6741,39 @@ }, state: function(composer, command) { return false; } -};wysihtml5.commands.deleteTableCells = { +}; +wysihtml5.commands.deleteTableCells = { exec: function(composer, command, value) { if (composer.tableSelection && composer.tableSelection.start && composer.tableSelection.end) { var tableSelect = wysihtml5.dom.table.orderSelectionEnds(composer.tableSelection.start, composer.tableSelection.end), idx = wysihtml5.dom.table.indexOf(tableSelect.start), selCell, table = composer.tableSelection.table; - + wysihtml5.dom.table.removeCells(tableSelect.start, value); setTimeout(function() { // move selection to next or previous if not present selCell = wysihtml5.dom.table.findCell(table, idx); - + if (!selCell){ if (value == "row") { selCell = wysihtml5.dom.table.findCell(table, { "row": idx.row - 1, "col": idx.col }); } - + if (value == "column") { selCell = wysihtml5.dom.table.findCell(table, { "row": idx.row, "col": idx.col - 1 }); } - } + } if (selCell) { composer.tableSelection.select(selCell, selCell); } }, 0); @@ -6638,11 +6781,12 @@ }, state: function(composer, command) { return false; } -};/** +}; +/** * Undo Manager for wysihtml5 * slightly inspired by http://rniwa.com/editing/undomanager.html#the-undomanager-interface */ (function(wysihtml5) { var Z_KEY = 90, @@ -6653,87 +6797,87 @@ DATA_ATTR_NODE = "data-wysihtml5-selection-node", DATA_ATTR_OFFSET = "data-wysihtml5-selection-offset", UNDO_HTML = '<span id="_wysihtml5-undo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>', REDO_HTML = '<span id="_wysihtml5-redo" class="_wysihtml5-temp">' + wysihtml5.INVISIBLE_SPACE + '</span>', dom = wysihtml5.dom; - + function cleanTempElements(doc) { var tempElement; while (tempElement = doc.querySelector("._wysihtml5-temp")) { tempElement.parentNode.removeChild(tempElement); } } - + wysihtml5.UndoManager = wysihtml5.lang.Dispatcher.extend( /** @scope wysihtml5.UndoManager.prototype */ { constructor: function(editor) { this.editor = editor; this.composer = editor.composer; this.element = this.composer.element; - + this.position = 0; this.historyStr = []; this.historyDom = []; - + this.transact(); - + this._observe(); }, - + _observe: function() { var that = this, doc = this.composer.sandbox.getDocument(), lastKey; - + // Catch CTRL+Z and CTRL+Y dom.observe(this.element, "keydown", function(event) { if (event.altKey || (!event.ctrlKey && !event.metaKey)) { return; } - + var keyCode = event.keyCode, isUndo = keyCode === Z_KEY && !event.shiftKey, isRedo = (keyCode === Z_KEY && event.shiftKey) || (keyCode === Y_KEY); - + if (isUndo) { that.undo(); event.preventDefault(); } else if (isRedo) { that.redo(); event.preventDefault(); } }); - + // Catch delete and backspace dom.observe(this.element, "keydown", function(event) { var keyCode = event.keyCode; if (keyCode === lastKey) { return; } - + lastKey = keyCode; - + if (keyCode === BACKSPACE_KEY || keyCode === DELETE_KEY) { that.transact(); } }); - + // Now this is very hacky: // These days browsers don't offer a undo/redo event which we could hook into // to be notified when the user hits undo/redo in the contextmenu. // Therefore we simply insert two elements as soon as the contextmenu gets opened. // The last element being inserted will be immediately be removed again by a exexCommand("undo") // => When the second element appears in the dom tree then we know the user clicked "redo" in the context menu // => When the first element disappears from the dom tree then we know the user clicked "undo" in the context menu - + // TODO: unexpected behaviour. Tends to undo on contextmenu showing in chrome on newly inserted blocks /*if (wysihtml5.browser.hasUndoInContextMenu()) { var interval, observed, cleanUp = function() { cleanTempElements(doc); clearInterval(interval); }; - + dom.observe(this.element, "contextmenu", function() { cleanUp(); that.composer.selection.executeAndRestoreSimple(function() { if (that.element.lastChild) { that.composer.selection.setAfter(that.element.lastChild); @@ -6761,108 +6905,108 @@ dom.observe(document, "mousedown", cleanUp); dom.observe(doc, ["mousedown", "paste", "cut", "copy"], cleanUp); } }); }*/ - + this.editor .on("newword:composer", function() { that.transact(); }) - + .on("beforecommand:composer", function() { that.transact(); }); }, - + transact: function() { var previousHtml = this.historyStr[this.position - 1], currentHtml = this.composer.getValue(); - + if (currentHtml === previousHtml) { return; } - + var length = this.historyStr.length = this.historyDom.length = this.position; if (length > MAX_HISTORY_ENTRIES) { this.historyStr.shift(); this.historyDom.shift(); this.position--; } - + this.position++; - + var range = this.composer.selection.getRange(), node = (range && range.startContainer) ? range.startContainer : this.element, offset = (range && range.startOffset) ? range.startOffset : 0, element, position; - + if (node.nodeType === wysihtml5.ELEMENT_NODE) { element = node; } else { element = node.parentNode; position = this.getChildNodeIndex(element, node); } - + element.setAttribute(DATA_ATTR_OFFSET, offset); if (typeof(position) !== "undefined") { element.setAttribute(DATA_ATTR_NODE, position); } - + var clone = this.element.cloneNode(!!currentHtml); this.historyDom.push(clone); this.historyStr.push(currentHtml); - + element.removeAttribute(DATA_ATTR_OFFSET); element.removeAttribute(DATA_ATTR_NODE); }, - + undo: function() { this.transact(); - + if (!this.undoPossible()) { return; } - + this.set(this.historyDom[--this.position - 1]); this.editor.fire("undo:composer"); }, - + redo: function() { if (!this.redoPossible()) { return; } - + this.set(this.historyDom[++this.position - 1]); this.editor.fire("redo:composer"); }, - + undoPossible: function() { return this.position > 1; }, - + redoPossible: function() { return this.position < this.historyStr.length; }, - + set: function(historyEntry) { this.element.innerHTML = ""; - + var i = 0, childNodes = historyEntry.childNodes, length = historyEntry.childNodes.length; - + for (; i<length; i++) { this.element.appendChild(childNodes[i].cloneNode(true)); } - + // Restore selection var offset, node, position; - + if (historyEntry.hasAttribute(DATA_ATTR_OFFSET)) { offset = historyEntry.getAttribute(DATA_ATTR_OFFSET); position = historyEntry.getAttribute(DATA_ATTR_NODE); node = this.element; } else { @@ -6870,29 +7014,29 @@ offset = node.getAttribute(DATA_ATTR_OFFSET); position = node.getAttribute(DATA_ATTR_NODE); node.removeAttribute(DATA_ATTR_OFFSET); node.removeAttribute(DATA_ATTR_NODE); } - + if (position !== null) { node = this.getChildNodeByIndex(node, +position); } - + this.composer.selection.set(node, offset); }, - + getChildNodeIndex: function(parent, child) { var i = 0, childNodes = parent.childNodes, length = childNodes.length; for (; i<length; i++) { if (childNodes[i] === child) { return i; } } }, - + getChildNodeByIndex: function(parent, index) { return parent.childNodes[index]; } }); })(wysihtml5); @@ -6907,11 +7051,11 @@ this.config = config; if (!this.config.noTextarea) { this._observeViewChange(); } }, - + _observeViewChange: function() { var that = this; this.parent.on("beforeload", function() { that.parent.on("change_view", function(view) { if (view === that.name) { @@ -6923,38 +7067,39 @@ that.hide(); } }); }); }, - + focus: function() { if (this.element.ownerDocument.querySelector(":focus") === this.element) { return; } - + try { this.element.focus(); } catch(e) {} }, - + hide: function() { this.element.style.display = "none"; }, - + show: function() { this.element.style.display = ""; }, - + disable: function() { this.element.setAttribute("disabled", "disabled"); }, - + enable: function() { this.element.removeAttribute("disabled"); } -});(function(wysihtml5) { +}); +(function(wysihtml5) { var dom = wysihtml5.dom, browser = wysihtml5.browser; - + wysihtml5.views.Composer = wysihtml5.views.View.extend( /** @scope wysihtml5.views.Composer.prototype */ { name: "composer", // Needed for firefox in order to display a proper caret in an empty contentEditable @@ -6978,11 +7123,11 @@ this.element.innerHTML = browser.displaysCaretInEmptyContentEditableCorrectly() ? "" : this.CARET_HACK; }, getValue: function(parse) { var value = this.isEmpty() ? "" : wysihtml5.quirks.getCorrectInnerHTML(this.element); - + if (parse) { value = this.parent.parse(value); } return value; @@ -6990,25 +7135,25 @@ setValue: function(html, parse) { if (parse) { html = this.parent.parse(html); } - + try { this.element.innerHTML = html; } catch (e) { this.element.innerText = html; } }, - + cleanUp: function() { this.parent.parse(this.element); }, show: function() { this.editableArea.style.display = this._displayStyle || ""; - + if (!this.config.noTextarea && !this.textarea.element.disabled) { // Firefox needs this, otherwise contentEditable becomes uneditable this.disable(); this.enable(); } @@ -7037,13 +7182,13 @@ // This is needed by our simulate_placeholder.js to work // therefore we clear it ourselves this time if (wysihtml5.browser.doesAsyncFocus() && this.hasPlaceholderSet()) { this.clear(); } - + this.base(); - + var lastChild = this.element.lastChild; if (setToEnd && lastChild && this.selection) { if (lastChild.nodeName === "BR") { this.selection.setBefore(this.element.lastChild); } else { @@ -7066,14 +7211,14 @@ innerHTML === "<br>" || innerHTML === "<p></p>" || innerHTML === "<p><br></p>" || this.hasPlaceholderSet(); }, - + _initContentEditableArea: function() { var that = this; - + if (this.config.noTextarea) { this.sandbox = new dom.ContentEditableArea(function() { that._create(); }, {}, this.editableArea); } else { @@ -7086,24 +7231,24 @@ } }, _initSandbox: function() { var that = this; - + this.sandbox = new dom.Sandbox(function() { that._create(); }, { stylesheets: this.config.stylesheets }); this.editableArea = this.sandbox.getIframe(); - + var textareaElement = this.textarea.element; dom.insert(this.editableArea).after(textareaElement); - + this._createWysiwygFormField(); }, - + // Creates hidden field which tells the server after submit, that the user used an wysiwyg editor _createWysiwygFormField: function() { if (this.textarea.element.form) { var hiddenField = document.createElement("input"); hiddenField.type = "hidden"; @@ -7121,79 +7266,79 @@ this.textarea = this.parent.textarea; this.element.innerHTML = this.textarea.getValue(true); } else { this.cleanUp(); // cleans contenteditable on initiation as it may contain html } - + // Make sure our selection handler is ready this.selection = new wysihtml5.Selection(this.parent, this.element, this.config.uneditableContainerClassname); - + // Make sure commands dispatcher is ready this.commands = new wysihtml5.Commands(this.parent); - + if (!this.config.noTextarea) { dom.copyAttributes([ "className", "spellcheck", "title", "lang", "dir", "accessKey" ]).from(this.textarea.element).to(this.element); } - + dom.addClass(this.element, this.config.composerClassName); - // + // // Make the editor look like the original textarea, by syncing styles if (this.config.style && !this.config.contentEditableMode) { this.style(); } - + this.observe(); - + var name = this.config.name; if (name) { dom.addClass(this.element, name); if (!this.config.contentEditableMode) { dom.addClass(this.editableArea, name); } } - + this.enable(); - + if (!this.config.noTextarea && this.textarea.element.disabled) { this.disable(); } - + // Simulate html5 placeholder attribute on contentEditable element var placeholderText = typeof(this.config.placeholder) === "string" ? this.config.placeholder : ((this.config.noTextarea) ? this.editableArea.getAttribute("data-placeholder") : this.textarea.element.getAttribute("placeholder")); if (placeholderText) { dom.simulatePlaceholder(this.parent, this, placeholderText); } - + // Make sure that the browser avoids using inline styles whenever possible this.commands.exec("styleWithCSS", false); - + this._initAutoLinking(); this._initObjectResizing(); this._initUndoManager(); this._initLineBreaking(); - + // Simulate html5 autofocus on contentEditable element // This doesn't work on IOS (5.1.1) if (!this.config.noTextarea && (this.textarea.element.hasAttribute("autofocus") || document.querySelector(":focus") == this.textarea.element) && !browser.isIos()) { setTimeout(function() { that.focus(true); }, 100); } - + // IE sometimes leaves a single paragraph, which can't be removed by the user if (!browser.clearsContentEditableCorrectly()) { wysihtml5.quirks.ensureProperClearing(this); } - + // Set up a sync that makes sure that textarea and editor have the same content if (this.initSync && this.config.sync) { this.initSync(); } - + // Okay hide the textarea, we are ready to go if (!this.config.noTextarea) { this.textarea.hide(); } - + // Fire global (before-)load event this.parent.fire("beforeload").fire("load"); }, _initAutoLinking: function() { @@ -7216,11 +7361,11 @@ that.selection.executeAndRestore(function(startContainer, endContainer) { dom.autoLink(endContainer.parentNode); }); } }); - + dom.observe(this.element, "blur", function() { dom.autoLink(that.element); }); } @@ -7270,65 +7415,65 @@ }); }, _initObjectResizing: function() { this.commands.exec("enableObjectResizing", true); - + // IE sets inline styles after resizing objects // The following lines make sure that the width/height css properties // are copied over to the width/height attributes if (browser.supportsEvent("resizeend")) { var properties = ["width", "height"], propertiesLength = properties.length, element = this.element; - + dom.observe(element, "resizeend", function(event) { var target = event.target || event.srcElement, style = target.style, i = 0, property; - + if (target.nodeName !== "IMG") { return; } - + for (; i<propertiesLength; i++) { property = properties[i]; if (style[property]) { target.setAttribute(property, parseInt(style[property], 10)); style[property] = ""; } } - + // After resizing IE sometimes forgets to remove the old resize handles wysihtml5.quirks.redraw(element); }); } }, - + _initUndoManager: function() { this.undoManager = new wysihtml5.UndoManager(this.parent); }, - + _initLineBreaking: function() { var that = this, USE_NATIVE_LINE_BREAK_INSIDE_TAGS = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"], LIST_TAGS = ["UL", "OL", "MENU"]; - + function adjust(selectedNode) { var parentElement = dom.getParentElement(selectedNode, { nodeName: ["P", "DIV"] }, 2); - if (parentElement) { + if (parentElement && dom.contains(that.element, parentElement)) { that.selection.executeAndRestore(function() { if (that.config.useLineBreaks) { dom.replaceWithChildNodes(parentElement); } else if (parentElement.nodeName !== "P") { dom.renameElement(parentElement, "p"); } }); } } - + if (!this.config.useLineBreaks) { dom.observe(this.element, ["focus", "keydown"], function() { if (that.isEmpty()) { var paragraph = that.doc.createElement("P"); that.element.innerHTML = ""; @@ -7340,39 +7485,39 @@ that.selection.selectNode(paragraph, true); } } }); } - + // Under certain circumstances Chrome + Safari create nested <p> or <hX> tags after paste // Inserting an invisible white space in front of it fixes the issue // This is too hacky and causes selection not to replace content on paste in chrome /* if (browser.createsNestedInvalidMarkupAfterPaste()) { dom.observe(this.element, "paste", function(event) { var invisibleSpace = that.doc.createTextNode(wysihtml5.INVISIBLE_SPACE); that.selection.insertNode(invisibleSpace); }); }*/ - + dom.observe(this.element, "keydown", function(event) { var keyCode = event.keyCode; - + if (event.shiftKey) { return; } - + if (keyCode !== wysihtml5.ENTER_KEY && keyCode !== wysihtml5.BACKSPACE_KEY) { return; } var blockElement = dom.getParentElement(that.selection.getSelectedNode(), { nodeName: USE_NATIVE_LINE_BREAK_INSIDE_TAGS }, 4); if (blockElement) { setTimeout(function() { // Unwrap paragraph after leaving a list or a H1-6 var selectedNode = that.selection.getSelectedNode(), list; - + if (blockElement.nodeName === "LI") { if (!selectedNode) { return; } @@ -7387,15 +7532,15 @@ adjust(selectedNode); } }, 0); return; } - + if (that.config.useLineBreaks && keyCode === wysihtml5.ENTER_KEY && !wysihtml5.browser.insertsLineBreaksOnReturn()) { event.preventDefault(); that.commands.exec("insertLineBreak"); - + } }); } }); })(wysihtml5); @@ -7443,16 +7588,16 @@ "html { height: 100%; }", "body { height: 100%; padding: 1px 0 0 0; margin: -1px 0 0 0; }", "body > p:first-child { margin-top: 0; }", "._wysihtml5-temp { display: none; }", wysihtml5.browser.isGecko ? - "body.placeholder { color: graytext !important; }" : + "body.placeholder { color: graytext !important; }" : "body.placeholder { color: #a9a9a9 !important; }", // Ensure that user see's broken images and can delete them "img:-moz-broken { -moz-force-broken-image-icon: 1; height: 24px; width: 24px; }" ]; - + /** * With "setActive" IE offers a smart way of focusing elements without scrolling them into view: * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx * * Other browsers need a more hacky way: (pssst don't tell my mama) @@ -7472,136 +7617,137 @@ position: elementStyle.position, top: elementStyle.top, left: elementStyle.left, WebkitUserSelect: elementStyle.WebkitUserSelect }; - + dom.setStyles({ position: "absolute", top: "-99999px", left: "-99999px", // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother WebkitUserSelect: "none" }).on(element); - + element.focus(); - + dom.setStyles(originalStyles).on(element); - + if (win.scrollTo) { // Some browser extensions unset this method to prevent annoyances // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100 // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1 win.scrollTo(originalScrollLeft, originalScrollTop); } } }; - - + + wysihtml5.views.Composer.prototype.style = function() { var that = this, originalActiveElement = doc.querySelector(":focus"), textareaElement = this.textarea.element, hasPlaceholder = textareaElement.hasAttribute("placeholder"), originalPlaceholder = hasPlaceholder && textareaElement.getAttribute("placeholder"), originalDisplayValue = textareaElement.style.display, originalDisabled = textareaElement.disabled, displayValueForCopying; - + this.focusStylesHost = HOST_TEMPLATE.cloneNode(false); this.blurStylesHost = HOST_TEMPLATE.cloneNode(false); this.disabledStylesHost = HOST_TEMPLATE.cloneNode(false); - + // Remove placeholder before copying (as the placeholder has an affect on the computed style) if (hasPlaceholder) { textareaElement.removeAttribute("placeholder"); } - + if (textareaElement === originalActiveElement) { textareaElement.blur(); } - + // enable for copying styles textareaElement.disabled = false; - + // set textarea to display="none" to get cascaded styles via getComputedStyle textareaElement.style.display = displayValueForCopying = "none"; - + if ((textareaElement.getAttribute("rows") && dom.getStyle("height").from(textareaElement) === "auto") || (textareaElement.getAttribute("cols") && dom.getStyle("width").from(textareaElement) === "auto")) { textareaElement.style.display = displayValueForCopying = originalDisplayValue; } - + // --------- iframe styles (has to be set before editor styles, otherwise IE9 sets wrong fontFamily on blurStylesHost) --------- dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.editableArea).andTo(this.blurStylesHost); - + // --------- editor styles --------- dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.element).andTo(this.blurStylesHost); - + // --------- apply standard rules --------- dom.insertCSS(ADDITIONAL_CSS_RULES).into(this.element.ownerDocument); - + // --------- :disabled styles --------- textareaElement.disabled = true; dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.disabledStylesHost); dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.disabledStylesHost); textareaElement.disabled = originalDisabled; - + // --------- :focus styles --------- textareaElement.style.display = originalDisplayValue; focusWithoutScrolling(textareaElement); textareaElement.style.display = displayValueForCopying; - + dom.copyStyles(BOX_FORMATTING).from(textareaElement).to(this.focusStylesHost); dom.copyStyles(TEXT_FORMATTING).from(textareaElement).to(this.focusStylesHost); - + // reset textarea textareaElement.style.display = originalDisplayValue; - + dom.copyStyles(["display"]).from(textareaElement).to(this.editableArea); - + // Make sure that we don't change the display style of the iframe when copying styles oblur/onfocus // this is needed for when the change_view event is fired where the iframe is hidden and then // the blur event fires and re-displays it var boxFormattingStyles = wysihtml5.lang.array(BOX_FORMATTING).without(["display"]); - + // --------- restore focus --------- if (originalActiveElement) { originalActiveElement.focus(); } else { textareaElement.blur(); } - + // --------- restore placeholder --------- if (hasPlaceholder) { textareaElement.setAttribute("placeholder", originalPlaceholder); } - + // --------- Sync focus/blur styles --------- this.parent.on("focus:composer", function() { dom.copyStyles(boxFormattingStyles) .from(that.focusStylesHost).to(that.editableArea); dom.copyStyles(TEXT_FORMATTING) .from(that.focusStylesHost).to(that.element); }); - + this.parent.on("blur:composer", function() { dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea); dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); }); - + this.parent.observe("disable:composer", function() { dom.copyStyles(boxFormattingStyles) .from(that.disabledStylesHost).to(that.editableArea); dom.copyStyles(TEXT_FORMATTING) .from(that.disabledStylesHost).to(that.element); }); - + this.parent.observe("enable:composer", function() { dom.copyStyles(boxFormattingStyles) .from(that.blurStylesHost).to(that.editableArea); dom.copyStyles(TEXT_FORMATTING) .from(that.blurStylesHost).to(that.element); }); - + return this; }; -})(wysihtml5);/** +})(wysihtml5); +/** * Taking care of events * - Simulating 'change' event on contentEditable element * - Handling drag & drop logic * - Catch paste events * - Dispatch proprietary newword:composer event @@ -7616,11 +7762,11 @@ shortcuts = { "66": "bold", // B "73": "italic", // I "85": "underline" // U }; - + wysihtml5.views.Composer.prototype.observe = function() { var that = this, state = this.getValue(), container = (this.sandbox.getIframe) ? this.sandbox.getIframe() : this.sandbox.getContentEditable(), element = this.element, @@ -7641,20 +7787,20 @@ clearInterval(domNodeRemovedInterval); that.parent.fire("destroy:composer"); } }, 250); } - + // --------- User interaction tracking -- - + dom.observe(focusBlurElement, interactionEvents, function() { setTimeout(function() { that.parent.fire("interaction").fire("interaction:composer"); }, 0); }); - + if (this.config.handleTables) { this.tableSelection = wysihtml5.quirks.tableCellsSelection(element, that.parent); } // --------- Focus & blur logic --------- @@ -7701,36 +7847,36 @@ dom.observe(element, "mousedown", function(event) { var target = event.target; var allImages = element.querySelectorAll('img'), notMyImages = element.querySelectorAll('.' + that.config.uneditableContainerClassname + ' img'), myImages = wysihtml5.lang.array(allImages).without(notMyImages); - + if (target.nodeName === "IMG" && wysihtml5.lang.array(myImages).contains(target)) { that.selection.selectNode(target); } }); } - + if (!browser.canSelectImagesInContentEditable()) { dom.observe(element, "drop", function(event) { // TODO: if I knew how to get dropped elements list from event I could limit it to only IMG element case setTimeout(function() { that.selection.getSelection().removeAllRanges(); }, 0); }); } - + if (browser.hasHistoryIssue() && browser.supportsSelectionModify()) { dom.observe(element, "keydown", function(event) { if (!event.metaKey && !event.ctrlKey) { return; } - + var keyCode = event.keyCode, win = element.ownerDocument.defaultView, selection = win.getSelection(); - + if (keyCode === 37 || keyCode === 39) { if (keyCode === 37) { selection.modify("extend", "left", "lineboundary"); if (!event.shiftKey) { selection.collapseToStart(); @@ -7744,39 +7890,39 @@ } event.preventDefault(); } }); } - + // --------- Shortcut logic --------- dom.observe(element, "keydown", function(event) { var keyCode = event.keyCode, command = shortcuts[keyCode]; if ((event.ctrlKey || event.metaKey) && !event.altKey && command) { that.commands.exec(command); event.preventDefault(); } if (keyCode == 8) { - + if (that.selection.isCollapsed()) { if (that.selection.caretIsInTheBeginnig()) { event.preventDefault(); } else { var beforeUneditable = that.selection.caretIsBeforeUneditable(); if (beforeUneditable) { event.preventDefault(); - + // TODO: take the how to delete around uneditable out of here // merge node with previous node from uneditable var prevNode = that.selection.getPreviousNode(beforeUneditable, true), curNode = that.selection.getSelectedNode(); - - if (curNode.nodeType !== 1 && curNode.parentNode !== element) { curNode = curNode.parentNode; } + + if (curNode.nodeType !== 1 && curNode.parentNode !== element) { curNode = curNode.parentNode; } if (prevNode) { if (curNode.nodeType == 1) { var first = curNode.firstChild; - + if (prevNode.nodeType == 1) { while (curNode.firstChild) { prevNode.appendChild(curNode.firstChild); } } else { @@ -7802,11 +7948,11 @@ } } else if (that.selection.containsUneditable()) { event.preventDefault(); that.selection.deleteContents(); } - + } }); // --------- Make sure that when pressing backspace/delete on selected images deletes the image and it's anchor --------- dom.observe(element, "keydown", function(event) { @@ -7824,11 +7970,11 @@ setTimeout(function() { wysihtml5.quirks.redraw(element); }, 0); event.preventDefault(); } }); - + // --------- IE 8+9 focus the editor when the iframe is clicked (without actually firing the 'focus' event on the <body>) --------- if (!this.config.contentEditableMode && browser.hasIframeFocusIssue()) { dom.observe(this.iframe, "focus", function() { setTimeout(function() { if (that.doc.querySelector(":focus") !== that.element) { @@ -7841,17 +7987,17 @@ setTimeout(function() { that.selection.getSelection().removeAllRanges(); }, 0); }); } - + // --------- Show url in tooltip when hovering links or images --------- var titlePrefixes = { IMG: "Image: ", A: "Link: " }; - + dom.observe(element, "mouseover", function(event) { var target = event.target, nodeName = target.nodeName, title; if (nodeName !== "A" && nodeName !== "IMG") { @@ -7862,16 +8008,17 @@ title = titlePrefixes[nodeName] + (target.getAttribute("href") || target.getAttribute("src")); target.setAttribute("title", title); } }); }; -})(wysihtml5);/** +})(wysihtml5); +/** * Class that takes care that the value of the composer and the textarea is always in sync */ (function(wysihtml5) { var INTERVAL = 400; - + wysihtml5.views.Synchronizer = Base.extend( /** @scope wysihtml5.views.Synchronizer.prototype */ { constructor: function(editor, textarea, composer) { this.editor = editor; @@ -7962,53 +8109,53 @@ }); })(wysihtml5); wysihtml5.views.Textarea = wysihtml5.views.View.extend( /** @scope wysihtml5.views.Textarea.prototype */ { name: "textarea", - + constructor: function(parent, textareaElement, config) { this.base(parent, textareaElement, config); - + this._observe(); }, - + clear: function() { this.element.value = ""; }, - + getValue: function(parse) { var value = this.isEmpty() ? "" : this.element.value; if (parse) { value = this.parent.parse(value); } return value; }, - + setValue: function(html, parse) { if (parse) { html = this.parent.parse(html); } this.element.value = html; }, - + cleanUp: function() { var html = this.parent.parse(this.element.value); this.element.value = html; }, - + hasPlaceholderSet: function() { var supportsPlaceholder = wysihtml5.browser.supportsPlaceholderAttributeOn(this.element), placeholderText = this.element.getAttribute("placeholder") || null, value = this.element.value, isEmpty = !value; return (supportsPlaceholder && isEmpty) || (value === placeholderText); }, - + isEmpty: function() { return !wysihtml5.lang.string(this.element.value).trim() || this.hasPlaceholderSet(); }, - + _observe: function() { var element = this.element, parent = this.parent, eventMapping = { focusin: "focus", @@ -8017,23 +8164,24 @@ /** * Calling focus() or blur() on an element doesn't synchronously trigger the attached focus/blur events * This is the case for focusin and focusout, so let's use them whenever possible, kkthxbai */ events = wysihtml5.browser.supportsEvent("focusin") ? ["focusin", "focusout", "change"] : ["focus", "blur", "change"]; - + parent.on("beforeload", function() { wysihtml5.dom.observe(element, events, function(event) { var eventName = eventMapping[event.type] || event.type; parent.fire(eventName).fire(eventName + ":textarea"); }); - + wysihtml5.dom.observe(element, ["paste", "drop"], function() { setTimeout(function() { parent.fire("paste").fire("paste:textarea"); }, 0); }); }); } -});/** +}); +/** * WYSIHTML5 Editor * * @param {Element} editableElement Reference to the textarea which should be turned into a rich text interface * @param {Object} [config] See defaultConfig object below for explanation of each individual config option * @@ -8062,24 +8210,24 @@ * disable:composer * change_view */ (function(wysihtml5) { var undef; - + var defaultConfig = { - // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body + // Give the editor a name, the name will also be set as class name on the iframe and on the iframe's body name: undef, // Whether the editor should look like the textarea (by adopting styles) style: true, // Id of the toolbar element, pass falsey value if you don't want any toolbar logic toolbar: undef, // Whether toolbar is displayed after init by script automatically. // Can be set to false if toolobar is set to display only on editable area focus showToolbarAfterInit: true, // Whether urls, entered by the user should automatically become clickable-links autoLink: true, - // Includes table editing events and cell selection tracking + // Includes table editing events and cell selection tracking handleTables: true, // Object which includes parser rules to apply when html gets inserted via copy & paste // See parser_rules/*.js for examples parserRules: { tags: { br: {}, span: {}, div: {}, p: {} }, classes: {} }, // Parser method to use when the user inserts content via copy & paste @@ -8100,64 +8248,64 @@ cleanUp: true, // Whether to use div instead of secure iframe contentEditableMode: false, xingAlert: false, // Classname of container that editor should not touch and pass through - // Pass false to disable + // Pass false to disable uneditableContainerClassname: "wysihtml5-uneditable-container" }; - + wysihtml5.Editor = wysihtml5.lang.Dispatcher.extend( /** @scope wysihtml5.Editor.prototype */ { constructor: function(editableElement, config) { this.editableElement = typeof(editableElement) === "string" ? document.getElementById(editableElement) : editableElement; this.config = wysihtml5.lang.object({}).merge(defaultConfig).merge(config).get(); this._isCompatible = wysihtml5.browser.supported(); - + if (this.editableElement.nodeName.toLowerCase() != "textarea") { this.config.contentEditableMode = true; this.config.noTextarea = true; } if (!this.config.noTextarea) { this.textarea = new wysihtml5.views.Textarea(this, this.editableElement, this.config); this.currentView = this.textarea; } - + // Sort out unsupported/unwanted browsers here if (!this._isCompatible || (!this.config.supportTouchDevices && wysihtml5.browser.isTouchDevice())) { var that = this; setTimeout(function() { that.fire("beforeload").fire("load"); }, 0); return; } - + // Add class name to body, to indicate that the editor is supported wysihtml5.dom.addClass(document.body, this.config.bodyClassName); - + this.composer = new wysihtml5.views.Composer(this, this.editableElement, this.config); this.currentView = this.composer; - + if (typeof(this.config.parser) === "function") { this._initParser(); } - + this.on("beforeload", this.handleBeforeLoad); - - + + if (this.config.xingAlert) { try { console.log("Heya! This page is using wysihtml5 for rich text editing. Check out https://github.com/xing/wysihtml5");} catch(e) {} } }, - + handleBeforeLoad: function() { if (!this.config.noTextarea) { this.synchronizer = new wysihtml5.views.Synchronizer(this, this.textarea, this.composer); } if (this.config.toolbar) { this.toolbar = new wysihtml5.toolbar.Toolbar(this, this.config.toolbar, this.config.showToolbarAfterInit); } }, - + isCompatible: function() { return this._isCompatible; }, clear: function() { @@ -8169,19 +8317,19 @@ return this.currentView.getValue(parse); }, setValue: function(html, parse) { this.fire("unset_placeholder"); - + if (!html) { return this.clear(); } - + this.currentView.setValue(html, parse); return this; }, - + cleanUp: function() { this.currentView.cleanUp(); }, focus: function(setToEnd) { @@ -8194,27 +8342,27 @@ */ disable: function() { this.currentView.disable(); return this; }, - + /** * Activate editor */ enable: function() { this.currentView.enable(); return this; }, - + isEmpty: function() { return this.currentView.isEmpty(); }, - + hasPlaceholderSet: function() { return this.currentView.hasPlaceholderSet(); }, - + parse: function(htmlOrElement) { var parseContext = (this.config.contentEditableMode) ? document : this.composer.sandbox.getDocument(); var returnValue = this.config.parser(htmlOrElement, { "rules": this.config.parserRules, "cleanUp": this.config.cleanUp, @@ -8224,10 +8372,10 @@ if (typeof(htmlOrElement) === "object") { wysihtml5.quirks.redraw(htmlOrElement); } return returnValue; }, - + /** * Prepare html parser logic * - Observes for paste and drop */ _initParser: function() {