vendor/assets/javascripts/wysihtml5x.js in wysihtml5x-rails-0.4.17 vs vendor/assets/javascripts/wysihtml5x.js in wysihtml5x-rails-0.5.0.beta1

- old
+ new

@@ -1,10 +1,21 @@ // TODO: in future try to replace most inline compability checks with polyfills for code readability // IE8 SUPPORT BLOCK // You can compile wuthout all this if IE8 is not needed +// String trim for ie8 +if (!String.prototype.trim) { + (function() { + // Make sure we trim BOM and NBSP + var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; + String.prototype.trim = function() { + return this.replace(rtrim, ''); + }; + })(); +} + // addEventListener, removeEventListener // TODO: make usage of wysihtml5.dom.observe obsolete (function() { if (!Event.prototype.preventDefault) { Event.prototype.preventDefault=function() { @@ -100,10 +111,18 @@ Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]'; }; } +// Array indexOf for ie8 +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(a,f) { + for(var c=this.length,r=-1,d=f>>>0; ~(c-d); r=this[--c]===a?c:r); + return r; + }; +} + // Function.prototype.bind() // TODO: clean the code from variable 'that' as it can be confusing if (!Function.prototype.bind) { Function.prototype.bind = function(oThis) { if (typeof this !== 'function') { @@ -125,23 +144,252 @@ fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }; -};/** - * @license wysihtml5x v0.4.17 +} + +// Element.matches Adds ie8 support and unifies nonstandard function names in other browsers +this.Element && function(ElementPrototype) { + ElementPrototype.matches = ElementPrototype.matches || + ElementPrototype.matchesSelector || + ElementPrototype.mozMatchesSelector || + ElementPrototype.msMatchesSelector || + ElementPrototype.oMatchesSelector || + ElementPrototype.webkitMatchesSelector || + function (selector) { + var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1; + while (nodes[++i] && nodes[i] != node); + return !!nodes[i]; + }; +}(Element.prototype); + +// Element.classList for ie8-9 (toggle all IE) +// source http://purl.eligrey.com/github/classList.js/blob/master/classList.js + +if ("document" in self) { + // Full polyfill for browsers with no classList support + if (!("classList" in document.createElement("_"))) { + (function(view) { + "use strict"; + if (!('Element' in view)) return; + + var + classListProp = "classList", + protoProp = "prototype", + elemCtrProto = view.Element[protoProp], + objCtr = Object, + strTrim = String[protoProp].trim || function() { + return this.replace(/^\s+|\s+$/g, ""); + }, + arrIndexOf = Array[protoProp].indexOf || function(item) { + var + i = 0, + len = this.length; + for (; i < len; i++) { + if (i in this && this[i] === item) { + return i; + } + } + return -1; + }, // Vendors: please allow content code to instantiate DOMExceptions + DOMEx = function(type, message) { + this.name = type; + this.code = DOMException[type]; + this.message = message; + }, + checkTokenAndGetIndex = function(classList, token) { + if (token === "") { + throw new DOMEx( + "SYNTAX_ERR", "An invalid or illegal string was specified" + ); + } + if (/\s/.test(token)) { + throw new DOMEx( + "INVALID_CHARACTER_ERR", "String contains an invalid character" + ); + } + return arrIndexOf.call(classList, token); + }, + ClassList = function(elem) { + var + trimmedClasses = strTrim.call(elem.getAttribute("class") || ""), + classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [], + i = 0, + len = classes.length; + for (; i < len; i++) { + this.push(classes[i]); + } + this._updateClassName = function() { + elem.setAttribute("class", this.toString()); + }; + }, + classListProto = ClassList[protoProp] = [], + classListGetter = function() { + return new ClassList(this); + }; + // Most DOMException implementations don't allow calling DOMException's toString() + // on non-DOMExceptions. Error's toString() is sufficient here. + DOMEx[protoProp] = Error[protoProp]; + classListProto.item = function(i) { + return this[i] || null; + }; + classListProto.contains = function(token) { + token += ""; + return checkTokenAndGetIndex(this, token) !== -1; + }; + classListProto.add = function() { + var + tokens = arguments, + i = 0, + l = tokens.length, + token, updated = false; + do { + token = tokens[i] + ""; + if (checkTokenAndGetIndex(this, token) === -1) { + this.push(token); + updated = true; + } + } + while (++i < l); + + if (updated) { + this._updateClassName(); + } + }; + classListProto.remove = function() { + var + tokens = arguments, + i = 0, + l = tokens.length, + token, updated = false, + index; + do { + token = tokens[i] + ""; + index = checkTokenAndGetIndex(this, token); + while (index !== -1) { + this.splice(index, 1); + updated = true; + index = checkTokenAndGetIndex(this, token); + } + } + while (++i < l); + + if (updated) { + this._updateClassName(); + } + }; + classListProto.toggle = function(token, force) { + token += ""; + + var + result = this.contains(token), + method = result ? + force !== true && "remove" : + force !== false && "add"; + + if (method) { + this[method](token); + } + + if (force === true || force === false) { + return force; + } else { + return !result; + } + }; + classListProto.toString = function() { + return this.join(" "); + }; + + if (objCtr.defineProperty) { + var classListPropDesc = { + get: classListGetter, + enumerable: true, + configurable: true + }; + try { + objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); + } catch (ex) { // IE 8 doesn't support enumerable:true + if (ex.number === -0x7FF5EC54) { + classListPropDesc.enumerable = false; + objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); + } + } + } else if (objCtr[protoProp].__defineGetter__) { + elemCtrProto.__defineGetter__(classListProp, classListGetter); + } + + }(self)); + + } else { + // There is full or partial native classList support, so just check if we need + // to normalize the add/remove and toggle APIs. + + (function() { + "use strict"; + + var testElement = document.createElement("_"); + + testElement.classList.add("c1", "c2"); + + // Polyfill for IE 10/11 and Firefox <26, where classList.add and + // classList.remove exist but support only one argument at a time. + if (!testElement.classList.contains("c2")) { + var createMethod = function(method) { + var original = DOMTokenList.prototype[method]; + + DOMTokenList.prototype[method] = function(token) { + var i, len = arguments.length; + + for (i = 0; i < len; i++) { + token = arguments[i]; + original.call(this, token); + } + }; + }; + createMethod('add'); + createMethod('remove'); + } + + testElement.classList.toggle("c3", false); + + // Polyfill for IE 10 and Firefox <24, where classList.toggle does not + // support the second argument. + if (testElement.classList.contains("c3")) { + var _toggle = DOMTokenList.prototype.toggle; + + DOMTokenList.prototype.toggle = function(token, force) { + if (1 in arguments && !this.contains(token) === !force) { + return force; + } else { + return _toggle.call(this, token); + } + }; + + } + + testElement = null; + }()); + + } + +} + +;/** + * @license wysihtml5x v0.5.0-beta1 * 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.17", + version: "0.5.0-beta1", // namespaces commands: {}, dom: {}, quirks: {}, @@ -4686,10 +4934,19 @@ It is on window but cannot return text/html Should actually check for clipboardData on paste event, but cannot in firefox */ supportsModenPaste: function () { return !("clipboardData" in window); + }, + + // Unifies the property names of element.style by returning the suitable property name for current browser + // Input property key must be the standard + fixStyleKey: function(key) { + if (key === "cssFloat") { + return ("styleFloat" in document.createElement("div").style) ? "styleFloat" : "cssFloat"; + } + return key; } }; })(); ;wysihtml5.lang.array = function(arr) { return { @@ -5420,26 +5677,30 @@ * 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)); + var callback = function(event) { + var target = event.target, + element = (target.nodeType === 3) ? target.parentNode : target, // IE has .contains only seeing elements not textnodes + matches = container.querySelectorAll(selector); - while (target && target !== container) { - if (match.contains(target)) { - handler.call(target, event); - break; + for (var i = 0, max = matches.length; i < max; i++) { + if (matches[i].contains(element)) { + handler.call(matches[i], event); } - target = target.parentNode; } - }); - }; + }; + container.addEventListener(eventName, callback, false); + return { + stop: function() { + container.removeEventListener(eventName, callback, false); + } + }; + }; })(wysihtml5); ;// TODO: Refactor dom tree traversing here (function(wysihtml5) { wysihtml5.dom.domNode = function(node) { var defaultNodeTypes = [wysihtml5.ELEMENT_NODE, wysihtml5.TEXT_NODE]; @@ -5513,10 +5774,107 @@ } } } return wysihtml5.dom.domNode(lastChild).lastLeafNode(options); + }, + + /* + Tests a node against properties, and returns true if matches. + Tests on principle that all properties defined must have at least one match. + styleValue parameter works in context of styleProperty and has no effect otherwise. + Returns true if element matches and false if it does not. + + Properties for filtering element: + { + query: selector string, + nodeName: string (uppercase), + className: string, + classRegExp: regex, + styleProperty: string or [], + styleValue: string, [] or regex + } + + Example: + var node = wysihtml5.dom.domNode(element).test({}) + */ + test: function(properties) { + var prop; + + // retuern false if properties object is not defined + if (!properties) { + return false; + } + + // Only element nodes can be tested for these properties + if (node.nodeType !== 1) { + return false; + } + + if (properties.query) { + if (!node.matches(properties.query)) { + return false; + } + } + + if (properties.nodeName && node.nodeName !== properties.nodeName) { + return false; + } + + if (properties.className && !node.classList.contains(properties.className)) { + return false; + } + + // classRegExp check (useful for classname begins with logic) + if (properties.classRegExp) { + var matches = (node.className || "").match(properties.classRegExp) || []; + if (matches.length === 0) { + return false; + } + } + + // styleProperty check + if (properties.styleProperty && properties.styleProperty.length > 0) { + var hasOneStyle = false, + styles = (Array.isArray(properties.styleProperty)) ? properties.styleProperty : [properties.styleProperty]; + for (var j = 0, maxStyleP = styles.length; j < maxStyleP; j++) { + // Some old IE-s have different property name for cssFloat + prop = wysihtml5.browser.fixStyleKey(styles[j]); + if (node.style[prop]) { + if (properties.styleValue) { + // Style value as additional parameter + if (properties.styleValue instanceof RegExp) { + // style value as Regexp + if (node.style[prop].trim().match(properties.styleValue).length > 0) { + hasOneStyle = true; + break; + } + } else if (Array.isArray(properties.styleValue)) { + // style value as array + if (properties.styleValue.indexOf(node.style[prop].trim())) { + hasOneStyle = true; + break; + } + } else { + // style value as string + if (properties.styleValue === node.style[prop].trim()) { + hasOneStyle = true; + break; + } + } + } else { + hasOneStyle = true; + break; + } + } + if (!hasOneStyle) { + return false; + } + } + } + + return true; } }; }; })(wysihtml5);;/** @@ -5583,81 +5941,39 @@ return tempElement; }; })(); ;/** * Walks the dom tree from the given node up until it finds a match - * Designed for optimal performance. * * @param {Element} node The from which to check the parent nodes - * @param {Object} matchingSet Object to match against (possible properties: nodeName, className, classRegExp) + * @param {Object} matchingSet Object to match against, Properties for filtering element: + * { + * query: selector string, + * classRegExp: regex, + * styleProperty: string or [], + * styleValue: string, [] or regex + * } * @param {Number} [levels] How many parents should the function check up from the current node (defaults to 50) + * @param {Element} Optional, defines the container that limits the search + * * @return {null|Element} Returns the first element that matched the desiredNodeName(s) - * @example - * var listElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: ["MENU", "UL", "OL"] }); - * // ... or ... - * var unorderedListElement = wysihtml5.dom.getParentElement(document.querySelector("li"), { nodeName: "UL" }); - * // ... or ... - * var coloredElement = wysihtml5.dom.getParentElement(myTextNode, { nodeName: "SPAN", className: "wysiwyg-color-red", classRegExp: /wysiwyg-color-[a-z]/g }); - */ +*/ + wysihtml5.dom.getParentElement = (function() { - function _isSameNodeName(nodeName, desiredNodeNames) { - if (!desiredNodeNames || !desiredNodeNames.length) { - return true; - } - - if (typeof(desiredNodeNames) === "string") { - return nodeName === desiredNodeNames; - } else { - return wysihtml5.lang.array(desiredNodeNames).contains(nodeName); - } - } - - function _isElement(node) { - return node.nodeType === wysihtml5.ELEMENT_NODE; - } - - function _hasClassName(element, className, classRegExp) { - var classNames = (element.className || "").match(classRegExp) || []; - if (!className) { - return !!classNames.length; - } - return classNames[classNames.length - 1] === className; - } - - function _hasStyle(element, cssStyle, styleRegExp) { - var styles = (element.getAttribute('style') || "").match(styleRegExp) || []; - if (!cssStyle) { - return !!styles.length; - } - return styles[styles.length - 1] === cssStyle; - } - - return function(node, matchingSet, levels, container) { - var findByStyle = (matchingSet.cssStyle || matchingSet.styleRegExp), - findByClass = (matchingSet.className || matchingSet.classRegExp); - - levels = levels || 50; // Go max 50 nodes upwards from current node - - // make the matching class regex from class name if omitted - if (findByClass && !matchingSet.classRegExp) { - matchingSet.classRegExp = new RegExp(matchingSet.className); - } - + return function(node, properties, levels, container) { + levels = levels || 50; while (levels-- && node && node.nodeName !== "BODY" && (!container || node !== container)) { - if (_isElement(node) && (!matchingSet.nodeName || _isSameNodeName(node.nodeName, matchingSet.nodeName)) && - (!findByStyle || _hasStyle(node, matchingSet.cssStyle, matchingSet.styleRegExp)) && - (!findByClass || _hasClassName(node, matchingSet.className, matchingSet.classRegExp)) - ) { + if (wysihtml5.dom.domNode(node).test(properties)) { return node; } node = node.parentNode; } return null; }; -})(); -;/** + +})();;/** * Get element's style for a specific css property * * @param {Element} element The element on which to retrieve the style * @param {String} property The CSS property to retrieve ("float", "display", "text-align", ...) * @@ -6651,11 +6967,11 @@ }); }; })(), href: (function() { - var REG_EXP = /^(#|\/|https?:\/\/|mailto:)/i; + var REG_EXP = /^(#|\/|https?:\/\/|mailto:|tel:)/i; return function(attributeValue) { if (!attributeValue || !attributeValue.match(REG_EXP)) { return null; } return attributeValue.replace(REG_EXP, function(match) { @@ -6672,18 +6988,28 @@ } return attributeValue.replace(REG_EXP, ""); }; })(), + // Integers. Does not work with floating point numbers and units numbers: (function() { var REG_EXP = /\D/g; return function(attributeValue) { attributeValue = (attributeValue || "").replace(REG_EXP, ""); return attributeValue || null; }; })(), + // Useful for with/height attributes where floating points and percentages are allowed + dimension: (function() { + var REG_EXP = /\D*(\d+)(\.\d+)?\s?(%)?\D*/; + return function(attributeValue) { + attributeValue = (attributeValue || "").replace(REG_EXP, "$1$2$3"); + return attributeValue || null; + }; + })(), + any: (function() { return function(attributeValue) { return attributeValue; }; })() @@ -7510,11 +7836,11 @@ }; var TableModifyerByCell = function (cell, table) { if (cell) { this.cell = cell; - this.table = api.getParentElement(cell, { nodeName: ["TABLE"] }); + this.table = api.getParentElement(cell, { query: "table" }); } else if (table) { this.table = table; this.cell = this.table.querySelectorAll('th, td')[0]; } }; @@ -7789,19 +8115,19 @@ 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"] }); + r = api.getParentElement(c.el, { query: "tr" }); if (r) { return r; } } } if (r === null && force) { - r = api.getParentElement(this.map[idx.row][idx.col].el, { nodeName: ["TR"] }) || null; + r = api.getParentElement(this.map[idx.row][idx.col].el, { query: "tr" }) || null; } return r; }, @@ -7817,11 +8143,11 @@ 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); + insertAfter(api.getParentElement(c.el, { query: "tr" }), rr); } }, canMerge: function(to) { this.to = to; @@ -8089,11 +8415,11 @@ return cells; }, // Removes the row of selected cell removeRow: function() { - var oldRow = api.getParentElement(this.cell, { nodeName: ["TR"] }); + var oldRow = api.getParentElement(this.cell, { query: "tr" }); if (oldRow) { this.setTableMap(); this.idx = this.getMapIndex(this.cell); if (this.idx !== false) { var modRow = this.map[this.idx.row]; @@ -8171,11 +8497,11 @@ switch (where) { case 'below': insertAfter(this.getRealRowEl(true), newRow); break; case 'above': - var cr = api.getParentElement(this.map[this.idx.row][this.idx.col].el, { nodeName: ["TR"] }); + var cr = api.getParentElement(this.map[this.idx.row][this.idx.col].el, { query: "tr" }); if (cr) { cr.parentNode.insertBefore(newRow, cr); } break; } @@ -8270,11 +8596,11 @@ } }, handleCellAddWithRowspan: function (cell, ridx, where) { var addRowsNr = parseInt(api.getAttribute(this.cell, 'rowspan'), 10) - 1, - crow = api.getParentElement(cell.el, { nodeName: ["TR"] }), + crow = api.getParentElement(cell.el, { query: "tr" }), cType = cell.el.tagName.toLowerCase(), cidx, temp_r_cells, doc = this.table.ownerDocument, nrow; @@ -8450,17 +8776,26 @@ } return 1; //Node.DOCUMENT_POSITION_DISCONNECTED; }; } })(); -;wysihtml5.dom.unwrap = function(node) { +;/* Unwraps element and returns list of childNodes that the node contained. + * + * Example: + * var childnodes = wysihtml5.dom.unwrap(document.querySelector('.unwrap-me')); +*/ + +wysihtml5.dom.unwrap = function(node) { + var children = []; if (node.parentNode) { while (node.lastChild) { + children.unshift(node.lastChild); wysihtml5.dom.insert(node.lastChild).after(node); } node.parentNode.removeChild(node); } + return children; };;/* * Methods for fetching pasted html before it gets inserted into content **/ /* Modern event.clipboardData driven approach. @@ -8497,10 +8832,15 @@ setTimeout(function () { composer.selection.setBookmark(selBookmark); f(cleanerDiv.innerHTML); cleanerDiv.parentNode.removeChild(cleanerDiv); }, 0); +};;wysihtml5.dom.removeInvisibleSpaces = function(node) { + var textNodes = wysihtml5.dom.getTextNodes(node); + for (var n = textNodes.length; n--;) { + textNodes[n].nodeValue = textNodes[n].nodeValue.replace(wysihtml5.INVISIBLE_SPACE_REG_EXP, ""); + } };;/** * Fix most common html formatting misbehaviors of browsers implementation when inserting * content via copy & paste contentEditable * * @author Christopher Blum @@ -8666,11 +9006,11 @@ upHandler = null; function init () { dom.observe(editable, "mousedown", function(event) { - var target = wysihtml5.dom.getParentElement(event.target, { nodeName: ["TD", "TH"] }); + var target = wysihtml5.dom.getParentElement(event.target, { query: "td, th" }); if (target) { handleSelectionMousedown(target); } }); @@ -8679,11 +9019,11 @@ function handleSelectionMousedown (target) { select.start = target; select.end = target; select.cells = [target]; - select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] }); + select.table = dom.getParentElement(select.start, { query: "table" }); if (select.table) { removeCellSelections(); dom.addClass(target, selection_class); moveHandler = dom.observe(editable, "mousemove", handleMouseMove); @@ -8710,15 +9050,15 @@ } } function handleMouseMove (event) { var curTable = null, - cell = dom.getParentElement(event.target, { nodeName: ["TD","TH"] }), + cell = dom.getParentElement(event.target, { nodeName: "td, th" }), oldEnd; if (cell && select.table && select.start) { - curTable = dom.getParentElement(cell, { nodeName: ["TABLE"] }); + curTable = dom.getParentElement(cell, { query: "table" }); if (curTable && curTable === select.table) { removeCellSelections(); oldEnd = select.end; select.end = cell; select.cells = dom.table.getCellsBetween(select.start, cell); @@ -8743,11 +9083,11 @@ } function bindSideclick () { var sideClickHandler = dom.observe(editable.ownerDocument, "click", function(event) { sideClickHandler.stop(); - if (dom.getParentElement(event.target, { nodeName: ["TABLE"] }) != select.table) { + if (dom.getParentElement(event.target, { query: "table" }) != select.table) { removeCellSelections(); select.table = null; select.start = null; select.end = null; editor.fire("tableunselect").fire("tableunselect:composer"); @@ -8756,11 +9096,11 @@ } function selectCells (start, end) { select.start = start; select.end = end; - select.table = dom.getParentElement(select.start, { nodeName: ["TABLE"] }); + select.table = dom.getParentElement(select.start, { query: "table" }); selectedCells = dom.table.getCellsBetween(select.start, select.end); addSelections(selectedCells); bindSideclick(); editor.fire("tableselect").fire("tableselect:composer"); } @@ -8958,11 +9298,11 @@ return this.setSelection(range); }, // Constructs a self removing whitespace (ain absolute positioned span) for placing selection caret when normal methods fail. // Webkit has an issue with placing caret into places where there are no textnodes near by. - creteTemporaryCaretSpaceAfter: function (node) { + createTemporaryCaretSpaceAfter: function (node) { var caretPlaceholder = this.doc.createElement('span'), caretPlaceholderText = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE), placeholderRemover = (function(event) { // Self-destructs the caret and keeps the text inserted into it by user var lastChild; @@ -9028,11 +9368,11 @@ * * @param {Object} node The element or text node where to position the caret in front of * @example * selection.setBefore(myElement); */ - setAfter: function(node) { + setAfter: function(node, notVisual) { var range = rangy.createRange(this.doc), originalScrollTop = this.doc.documentElement.scrollTop || this.doc.body.scrollTop || this.doc.defaultView.pageYOffset, originalScrollLeft = this.doc.documentElement.scrollLeft || this.doc.body.scrollLeft || this.doc.defaultView.pageXOffset, sel; @@ -9043,11 +9383,24 @@ sel = this.setSelection(range); // Webkit fails to add selection if there are no textnodes in that region // (like an uneditable container at the end of content). if (!sel) { - this.creteTemporaryCaretSpaceAfter(node); + if (notVisual) { + // If setAfter is used as internal between actions, self-removing caretPlaceholder has simpler implementation + // and remove itself in call stack end instead on user interaction + var caretPlaceholder = this.doc.createTextNode(wysihtml5.INVISIBLE_SPACE); + node.parentNode.insertBefore(caretPlaceholder, node.nextSibling); + this.selectNode(caretPlaceholder); + setTimeout(function() { + if (caretPlaceholder && caretPlaceholder.parentNode) { + caretPlaceholder.parentNode.removeChild(caretPlaceholder); + } + }, 0); + } else { + this.createTemporaryCaretSpaceAfter(node); + } } return sel; }, /** @@ -9068,11 +9421,10 @@ if (isEmpty && isElement && canHaveHTML && !avoidInvisibleSpace) { // Make sure that caret is visible in node by inserting a zero width no breaking space try { node.innerHTML = wysihtml5.INVISIBLE_SPACE; } catch(e) {} } - if (canHaveHTML) { range.selectNodeContents(node); } else { range.selectNode(node); } @@ -9142,10 +9494,23 @@ nodes = nodes.concat(curNodes); } return nodes; }, + filterElements: function(filter) { + var ranges = this.getOwnRanges(), + nodes = [], curNodes; + + for (var i = 0, maxi = ranges.length; i < maxi; i++) { + curNodes = ranges[i].getNodes([1], function(element){ + return filter(element, ranges[i]); + }); + 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++) { @@ -9162,14 +9527,14 @@ deleteContents: function() { var range = this.getRange(), startParent, endParent, uneditables, ev; if (this.unselectableClass) { - if ((startParent = wysihtml5.dom.getParentElement(range.startContainer, { className: this.unselectableClass }, false, this.contain))) { + if ((startParent = wysihtml5.dom.getParentElement(range.startContainer, { query: "." + this.unselectableClass }, false, this.contain))) { range.setStartBefore(startParent); } - if ((endParent = wysihtml5.dom.getParentElement(range.endContainer, { className: this.unselectableClass }, false, this.contain))) { + if ((endParent = wysihtml5.dom.getParentElement(range.endContainer, { query: "." + this.unselectableClass }, false, this.contain))) { range.setEndAfter(endParent); } // If customevents present notify uneditable elements of being deleted uneditables = range.getNodes([1], (function (node) { @@ -9235,11 +9600,11 @@ getSelectionParentsByTag: function(tagName) { var nodes = this.getSelectedOwnNodes(), curEl, parents = []; for (var i = 0, maxi = nodes.length; i < maxi; i++) { - curEl = (nodes[i].nodeName && nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml5.dom.getParentElement(nodes[i], { nodeName: ['LI']}, false, this.contain); + curEl = (nodes[i].nodeName && nodes[i].nodeName === 'LI') ? nodes[i] : wysihtml5.dom.getParentElement(nodes[i], { query: 'li'}, false, this.contain); if (curEl) { parents.push(curEl); } } return (parents.length) ? parents : null; @@ -9287,11 +9652,11 @@ caretIsInTheBeginnig: function(ofNode) { var selection = this.getSelection(), node = selection.anchorNode, offset = selection.anchorOffset; if (ofNode && node) { - return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml5.dom.getParentElement(node.parentNode, { nodeName: ofNode }, 1))); + return (offset === 0 && (node.nodeName && node.nodeName === ofNode.toUpperCase() || wysihtml5.dom.getParentElement(node.parentNode, { query: ofNode }, 1))); } else if (node) { return (offset === 0 && !this.getPreviousNode(node, true)); } }, @@ -9478,10 +9843,37 @@ if (range) { range.insertNode(node); } }, + splitElementAtCaret: function (element, insertNode) { + var sel = this.getSelection(), + range, contentAfterRangeStart, + firstChild, lastChild; + + if (sel.rangeCount > 0) { + range = sel.getRangeAt(0).cloneRange(); // Create a copy of the selection range to work with + + range.setEndAfter(element); // Place the end of the range after the element + contentAfterRangeStart = range.extractContents(); // Extract the contents of the element after the caret into a fragment + + element.parentNode.insertBefore(contentAfterRangeStart, element.nextSibling); + + firstChild = insertNode.firstChild; + lastChild = insertNode.lastChild; + + element.parentNode.insertBefore(insertNode, element.nextSibling); + + // Select inserted node contents + if (firstChild && lastChild) { + range.setStartBefore(firstChild); + range.setEndAfter(lastChild); + this.setSelection(range); + } + } + }, + /** * Wraps current selection with the given node * * @param {Object} node The node to surround the selected elements with */ @@ -9521,11 +9913,11 @@ tempElements, firstChild; tempElement.className = nodeOptions.className; - this.composer.commands.exec("formatBlock", nodeOptions.nodeName, nodeOptions.className); + this.composer.commands.exec("formatBlock", nodeOptions); tempDivElements = this.contain.querySelectorAll("." + nodeOptions.className); if (tempDivElements[0]) { tempDivElements[0].parentNode.insertBefore(tempElement, tempDivElements[0]); range.setStartBefore(tempDivElements[0]); @@ -9672,11 +10064,11 @@ }, getNodes: function(nodeType, filter) { var range = this.getRange(); if (range) { - return range.getNodes([nodeType], filter); + return range.getNodes(Array.isArray(nodeType) ? nodeType : [nodeType], filter); } else { return []; } }, @@ -10704,23 +11096,35 @@ * wysihtml5.commands.createLink.exec(composer, "createLink", { href: "http://www.google.de", target: "_blank" }); */ exec: function(composer, command, value) { var anchors = this.state(composer, command); if (anchors) { + // remove <a> tag if there's no attributes provided. + if ((!value || !value.href) && anchors.length !== null && anchors.length !== undefined && anchors.length > 0) + { + for(var i=0; i < anchors.length; i++) + { + wysihtml5.dom.unwrap(anchors[i]); + } + return; + } + // Selection contains links then change attributes of these links composer.selection.executeAndRestore(function() { _changeLinks(composer, anchors, value); }); } else { // Create links - value = typeof(value) === "object" ? value : { href: value }; - _format(composer, value); + if (value && value.href) { + value = typeof(value) === "object" ? value : { href: value }; + _format(composer, value); + } } }, state: function(composer, command) { - return wysihtml5.commands.formatInline.state(composer, command, "A"); + return wysihtml5.commands.formatInline.state(composer, command, "a"); } }; })(wysihtml5); ;(function(wysihtml5) { var dom = wysihtml5.dom; @@ -10731,11 +11135,11 @@ anchor, codeElement, textContent; for (; i<length; i++) { anchor = anchors[i]; - codeElement = dom.getParentElement(anchor, { nodeName: "code" }); + codeElement = dom.getParentElement(anchor, { query: "code" }); textContent = dom.getTextContent(anchor); // if <a> contains url-like text content, rename it to <code> to prevent re-autolinking // else replace <a> with its childNodes if (textContent.match(dom.autoLink.URL_REG_EXP) && !codeElement) { @@ -10929,228 +11333,372 @@ return false; } }; })(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 +;/* Formatblock + * Is used to insert block level elements + * It tries to solve the case that some block elements should not contain other block level elements (h1-6, p, ...) + * +*/ +(function(wysihtml5) { + + var dom = wysihtml5.dom, + // 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", "DIV"]; + UNNESTABLE_BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre"; + BLOCK_ELEMENTS = "h1, h2, h3, h4, h5, h6, p, pre, div, blockquote"; - /** - * Remove similiar classes (based on classRegExp) - * and add the desired class name - */ - function _addClass(element, className, classRegExp) { - if (element.className) { - _removeClass(element, classRegExp); - element.className = wysihtml5.lang.string(element.className + " " + className).trim(); - } else { - element.className = className; + // Removes empty block level elements + function cleanup(composer) { + var container = composer.element, + allElements = container.querySelectorAll(BLOCK_ELEMENTS), + uneditables = container.querySelectorAll(composer.config.uneditableContainerClassname), + elements = wysihtml5.lang.array(allElements).without(uneditables); + + for (var i = elements.length; i--;) { + if (elements[i].innerHTML === "") { + elements[i].parentNode.removeChild(elements[i]); + } } } - function _addStyle(element, cssStyle, styleRegExp) { - _removeStyle(element, styleRegExp); - if (element.getAttribute('style')) { - element.setAttribute('style', wysihtml5.lang.string(element.getAttribute('style') + " " + cssStyle).trim()); - } else { - element.setAttribute('style', cssStyle); + function defaultNodeName(composer) { + return composer.config.useLineBreaks ? "DIV" : "P"; + } + + // The outermost un-nestable block element parent of from node + function findOuterBlock(node, container, allBlocks) { + var n = node, + block = null; + + while (n && container && n !== container) { + if (n.nodeType === 1 && n.matches(allBlocks ? BLOCK_ELEMENTS : UNNESTABLE_BLOCK_ELEMENTS)) { + block = n; + } + n = n.parentNode; } + + return block; } - function _removeClass(element, classRegExp) { - var ret = classRegExp.test(element.className); - element.className = element.className.replace(classRegExp, ""); - if (wysihtml5.lang.string(element.className).trim() == '') { - element.removeAttribute('class'); + // Formats an element according to options nodeName, className, styleProperty, styleValue + // If element is not defined, creates new element + // if opotions is null, remove format instead + function applyOptionsToElement(element, options, composer) { + + if (!element) { + element = composer.doc.createElement(options.nodeName || defaultNodeName(composer)); + // Add invisible space as otherwise webkit cannot set selection or range to it correctly + element.appendChild(composer.doc.createTextNode(wysihtml5.INVISIBLE_SPACE)); } - return ret; + + if (options.nodeName && element.nodeName !== options.nodeName) { + element = dom.renameElement(element, options.nodeName); + } + + // Remove similar classes before applying className + if (options.classRegExp) { + element.className = element.className.replace(options.classRegExp, ""); + } + if (options.className) { + element.classList.add(options.className); + } + + if (options.styleProperty && typeof options.styleValue !== "undefined") { + element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = options.styleValue; + } + + return element; } - function _removeStyle(element, styleRegExp) { - var ret = styleRegExp.test(element.getAttribute('style')); - element.setAttribute('style', (element.getAttribute('style') || "").replace(styleRegExp, "")); - if (wysihtml5.lang.string(element.getAttribute('style') || "").trim() == '') { + // Unsets element properties by options + // If nodename given and matches current element, element is unwrapped or converted to default node (depending on presence of class and style attributes) + function removeOptionsFromElement(element, options, composer) { + var style, classes; + + if (options.styleProperty) { + element.style[wysihtml5.browser.fixStyleKey(options.styleProperty)] = ''; + } + if (options.className) { + element.classList.remove(options.className); + } + + if (options.classRegExp) { + element.className = element.className.replace(options.classRegExp, ""); + } + + // Clean up blank class attribute + if (element.getAttribute('class') !== null && element.getAttribute('class').trim() === "") { + element.removeAttribute('class'); + } + + if (options.nodeName && element.nodeName === options.nodeName) { + style = element.getAttribute('style'); + if (!style || style.trim() === '') { + dom.unwrap(element); + } else { + element = dom.renameElement(element, defaultNodeName(composer)); + } + } + + // Clean up blank style attribute + if (element.getAttribute('style') !== null && element.getAttribute('style').trim() === "") { element.removeAttribute('style'); } - return ret; } - function _removeLastChildIfLineBreak(node) { - var lastChild = node.lastChild; - if (lastChild && _isLineBreak(lastChild)) { - lastChild.parentNode.removeChild(lastChild); + // Unwraps block level elements from inside content + // Useful as not all block level elements can contain other block-levels + function unwrapBlocksFromContent(element) { + var contentBlocks = element.querySelectorAll(BLOCK_ELEMENTS) || []; // Find unnestable block elements in extracted contents + + for (var i = contentBlocks.length; i--;) { + if (!contentBlocks[i].nextSibling || contentBlocks[i].nextSibling.nodeType !== 1 || contentBlocks[i].nextSibling.nodeName !== 'BR') { + if ((contentBlocks[i].innerHTML || contentBlocks[i].nodeValue).trim() !== "") { + contentBlocks[i].parentNode.insertBefore(contentBlocks[i].ownerDocument.createElement('BR'), contentBlocks[i].nextSibling); + } + } + wysihtml5.dom.unwrap(contentBlocks[i]); } } - function _isLineBreak(node) { - return node.nodeName === "BR"; - } + // Fix ranges that visually cover whole block element to actually cover the block + function fixRangeCoverage(range, composer) { + var node; - /** - * Execute native query command - * and if necessary modify the inserted node's className - */ - function _execCommand(doc, composer, command, nodeName, className) { - var ranges = composer.selection.getOwnRanges(); - for (var i = ranges.length; i--;){ - composer.selection.getSelection().removeAllRanges(); - composer.selection.setSelection(ranges[i]); - if (className) { - var eventListener = dom.observe(doc, "DOMNodeInserted", function(event) { - var target = event.target, - displayStyle; - if (target.nodeType !== wysihtml5.ELEMENT_NODE) { - return; - } - displayStyle = dom.getStyle("display").from(target); - if (displayStyle.substr(0, 6) !== "inline") { - // Make sure that only block elements receive the given class - target.className += " " + className; - } - }); + if (range.startContainer && range.startContainer.nodeType === 1 && range.startContainer === range.endContainer) { + if (range.startContainer.firstChild === range.startContainer.lastChild && range.endOffset === 1) { + if (range.startContainer !== composer.element) { + range.setStartBefore(range.startContainer); + range.setEndAfter(range.endContainer); + } } - doc.execCommand(command, false, nodeName); + return; + } - if (eventListener) { - eventListener.stop(); + if (range.startContainer && range.startContainer.nodeType === 1 && range.endContainer.nodeType === 3) { + if (range.startContainer.firstChild === range.endContainer && range.endOffset === 1) { + if (range.startContainer !== composer.element) { + range.setEndAfter(range.startContainer); + } } + return; } - } - function _selectionWrap(composer, options) { - if (composer.selection.isCollapsed()) { - composer.selection.selectLine(); + if (range.endContainer && range.endContainer.nodeType === 1 && range.startContainer.nodeType === 3) { + if (range.endContainer.firstChild === range.startContainer && range.endOffset === 1) { + if (range.endContainer !== composer.element) { + range.setStartBefore(range.endContainer); + } + } + return; } - var surroundedNodes = composer.selection.surround(options); - for (var i = 0, imax = surroundedNodes.length; i < imax; i++) { - wysihtml5.dom.lineBreaks(surroundedNodes[i]).remove(); - _removeLastChildIfLineBreak(surroundedNodes[i]); - } - // rethink restoring selection - // composer.selection.selectNode(element, wysihtml5.browser.displaysCaretInEmptyContentEditableCorrectly()); + if (range.startContainer && range.startContainer.nodeType === 3 && range.startContainer === range.endContainer && range.startContainer.parentNode) { + if (range.startContainer.parentNode.firstChild === range.startContainer && range.endOffset == range.endContainer.length && range.startOffset === 0) { + node = range.startContainer.parentNode; + if (node !== composer.element) { + range.setStartBefore(node); + range.setEndAfter(node); + } + } + return; + } } - function _hasClasses(element) { - return !!wysihtml5.lang.string(element.className).trim(); - } + // Wrap the range with a block level element + // If element is one of unnestable block elements (ex: h2 inside h1), split nodes and insert between so nesting does not occur + function wrapRangeWithElement(range, options, defaultName, composer) { + var defaultOptions = (options) ? wysihtml5.lang.object(options).clone(true) : null; + if (defaultOptions) { + defaultOptions.nodeName = defaultOptions.nodeName || defaultName || defaultNodeName(composer); + } + fixRangeCoverage(range, composer); - function _hasStyles(element) { - return !!wysihtml5.lang.string(element.getAttribute('style') || '').trim(); - } + var r = range.cloneRange(), + rangeStartContainer = r.startContainer, + content = r.extractContents(), + fragment = composer.doc.createDocumentFragment(), + splitAllBlocks = !defaultOptions || (defaultName === "BLOCKQUOTE" && defaultOptions.nodeName && defaultOptions.nodeName === "BLOCKQUOTE"), + firstOuterBlock = findOuterBlock(rangeStartContainer, composer.element, splitAllBlocks), // The outermost un-nestable block element parent of selection start + wrapper, blocks, children; - wysihtml5.commands.formatBlock = { - exec: function(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp) { - var doc = composer.doc, - blockElements = this.state(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp), - useLineBreaks = composer.config.useLineBreaks, - defaultNodeName = useLineBreaks ? "DIV" : "P", - selectedNodes, classRemoveAction, blockRenameFound, styleRemoveAction, blockElement; - nodeName = typeof(nodeName) === "string" ? nodeName.toUpperCase() : nodeName; + if (options && options.nodeName && options.nodeName === "BLOCKQUOTE") { + var tmpEl = applyOptionsToElement(null, options, composer); + tmpEl.appendChild(content); + fragment.appendChild(tmpEl); + blocks = [tmpEl]; + } else { - if (blockElements.length) { - composer.selection.executeAndRestoreRangy(function() { - for (var b = blockElements.length; b--;) { - if (classRegExp) { - classRemoveAction = _removeClass(blockElements[b], classRegExp); - } - if (styleRegExp) { - styleRemoveAction = _removeStyle(blockElements[b], styleRegExp); - } + if (!content.firstChild) { + fragment.appendChild(applyOptionsToElement(null, options, composer)); + } else { - if ((styleRemoveAction || classRemoveAction) && nodeName === null && blockElements[b].nodeName != defaultNodeName) { - // dont rename or remove element when just setting block formating class or style - return; + while(content.firstChild) { + + if (content.firstChild.nodeType == 1 && content.firstChild.matches(BLOCK_ELEMENTS)) { + + if (options) { + // Escape(split) block formatting at caret + applyOptionsToElement(content.firstChild, options, composer); + if (content.firstChild.matches(UNNESTABLE_BLOCK_ELEMENTS)) { + unwrapBlocksFromContent(content.firstChild); + } + fragment.appendChild(content.firstChild); + + } else { + // Split block formating and add new block to wrap caret + unwrapBlocksFromContent(content.firstChild); + children = wysihtml5.dom.unwrap(content.firstChild); + for (var c = 0, cmax = children.length; c < cmax; c++) { + fragment.appendChild(children[c]); + } + + if (fragment.childNodes.length > 0) { + fragment.appendChild(composer.doc.createElement('BR')); + } } + } else { - var hasClasses = _hasClasses(blockElements[b]), - hasStyles = _hasStyles(blockElements[b]); - - if (!hasClasses && !hasStyles && (useLineBreaks || nodeName === "P")) { - // Insert a line break afterwards and beforewards when there are siblings - // that are not of type line break or block element - wysihtml5.dom.lineBreaks(blockElements[b]).add(); - dom.replaceWithChildNodes(blockElements[b]); + if (options) { + // Wrap subsequent non-block nodes inside new block element + wrapper = applyOptionsToElement(null, defaultOptions, composer); + while(content.firstChild && (content.firstChild.nodeType !== 1 || !content.firstChild.matches(BLOCK_ELEMENTS))) { + if (content.firstChild.nodeType == 1 && wrapper.matches(UNNESTABLE_BLOCK_ELEMENTS)) { + unwrapBlocksFromContent(content.firstChild); + } + wrapper.appendChild(content.firstChild); + } + fragment.appendChild(wrapper); + } else { - // 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); + // Escape(split) block formatting at selection + if (content.firstChild.nodeType == 1) { + unwrapBlocksFromContent(content.firstChild); + } + fragment.appendChild(content.firstChild); } + } - }); + } + } - return; + blocks = wysihtml5.lang.array(fragment.childNodes).get(); + } + + if (firstOuterBlock) { + // If selection starts inside un-nestable block, split-escape the unnestable point and insert node between + composer.selection.splitElementAtCaret(firstOuterBlock, fragment); + } else { + // Otherwise just insert + r.insertNode(fragment); + } + + return blocks; + } + + // Find closest block level element + function getParentBlockNodeName(element, composer) { + var parentNode = wysihtml5.dom.getParentElement(element, { + query: BLOCK_ELEMENTS + }, null, composer.element); + + return (parentNode) ? parentNode.nodeName : null; + } + + wysihtml5.commands.formatBlock = { + exec: function(composer, command, options) { + var newBlockElements = [], + placeholder, ranges, range, parent, bookmark, state; + + // If properties is passed as a string, look for tag with that tagName/query + if (typeof options === "string") { + options = { + nodeName: options.toUpperCase() + }; } - // 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.executeAndRestoreRangy(function() { - for (var n = selectedNodes.length; n--;) { - blockElement = dom.getParentElement(selectedNodes[n], { - nodeName: BLOCK_ELEMENTS_GROUP - }); - if (blockElement == composer.element) { - blockElement = null; - } - if (blockElement) { - // Rename current block element to new block element and add class - if (nodeName) { - blockElement = dom.renameElement(blockElement, nodeName); - } - if (className) { - _addClass(blockElement, className, classRegExp); - } - if (cssStyle) { - _addStyle(blockElement, cssStyle, styleRegExp); - } - blockRenameFound = true; - } + // Remove state if toggle set and state on and selection is collapsed + if (options && options.toggle) { + state = this.state(composer, command, options); + if (state) { + bookmark = rangy.saveSelection(composer.doc.defaultView || composer.doc.parentWindow); + for (var j in state) { + removeOptionsFromElement(state[j], options, composer); } + } + } - }); + // Otherwise expand selection so it will cover closest block if option caretSelectsBlock is true and selection is collapsed + if (!state) { - if (blockRenameFound) { - return; + if (composer.selection.isCollapsed()) { + parent = wysihtml5.dom.getParentElement(composer.selection.getOwnRanges()[0].startContainer, { + query: BLOCK_ELEMENTS + }, null, composer.element); + if (parent) { + bookmark = rangy.saveSelection(composer.doc.defaultView || composer.doc.parentWindow); + range = composer.selection.createRange(); + range.selectNode(parent); + composer.selection.setSelection(range); + } else if (!composer.isEmpty()) { + bookmark = rangy.saveSelection(composer.doc.defaultView || composer.doc.parentWindow); + composer.selection.selectLine(); + } } + + // And get all selection ranges of current composer and iterat + ranges = composer.selection.getOwnRanges(); + for (var i = ranges.length; i--;) { + newBlockElements = newBlockElements.concat(wrapRangeWithElement(ranges[i], options, getParentBlockNodeName(ranges[i].startContainer, composer), composer)); + } + } - _selectionWrap(composer, { - "nodeName": (nodeName || defaultNodeName), - "className": className || null, - "cssStyle": cssStyle || null - }); + // Remove empty block elements that may be left behind + cleanup(composer); + // Restore correct selection + if (bookmark) { + rangy.restoreSelection(bookmark); + } else { + range = composer.selection.createRange(); + range.setStartBefore(newBlockElements[0]); + range.setEndAfter(newBlockElements[newBlockElements.length - 1]); + composer.selection.setSelection(range); + } + + wysihtml5.dom.removeInvisibleSpaces(composer.element); + }, - state: function(composer, command, nodeName, className, classRegExp, cssStyle, styleRegExp) { - var nodes = composer.selection.getSelectedOwnNodes(), - parents = [], + // If properties as null is passed returns status describing all block level elements + state: function(composer, command, properties) { + + // If properties is passed as a string, look for tag with that tagName/query + if (typeof properties === "string") { + properties = { + query: properties + }; + } + + var nodes = composer.selection.filterElements((function (element) { // Finds matching elements inside selection + return wysihtml5.dom.domNode(element).test(properties || { query: BLOCK_ELEMENTS }); + }).bind(this)), + parentNodes = composer.selection.getSelectedOwnNodes(), 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, - classRegExp: classRegExp, - cssStyle: cssStyle, - styleRegExp: styleRegExp - }); - if (parent && wysihtml5.lang.array(parents).indexOf(parent) == -1) { - parents.push(parent); + // Finds matching elements that are parents of selection and adds to nodes list + for (var i = 0, maxi = parentNodes.length; i < maxi; i++) { + parent = dom.getParentElement(parentNodes[i], properties || { query: BLOCK_ELEMENTS }, null, composer.element); + if (parent && nodes.indexOf(parent) === -1) { + nodes.push(parent); } } - if (parents.length == 0) { - return false; - } - return parents; + + return (nodes.length === 0) ? false : nodes; } }; })(wysihtml5); @@ -11198,11 +11746,11 @@ var selectedNode = composer.selection.getSelectedNode(); if (selectedNode && selectedNode.nodeName && selectedNode.nodeName == "PRE"&& selectedNode.firstChild && selectedNode.firstChild.nodeName && selectedNode.firstChild.nodeName == "CODE") { return selectedNode; } else { - return wysihtml5.dom.getParentElement(selectedNode, { nodeName: "CODE" }) && wysihtml5.dom.getParentElement(selectedNode, { nodeName: "PRE" }); + return wysihtml5.dom.getParentElement(selectedNode, { query: "pre code" }); } } };;/** * formatInline scenarios for tag "B" (| = caret, |foo| = selected text) * @@ -11353,46 +11901,27 @@ } }; })(wysihtml5); ;(function(wysihtml5) { + var nodeOptions = { + nodeName: "BLOCKQUOTE", + toggle: true + }; + wysihtml5.commands.insertBlockQuote = { exec: function(composer, command) { - var state = this.state(composer, command), - endToEndParent = composer.selection.isEndToEndInNode(['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P']), - prevNode, nextNode; - - composer.selection.executeAndRestore(function() { - if (state) { - if (composer.config.useLineBreaks) { - wysihtml5.dom.lineBreaks(state).add(); - } - wysihtml5.dom.unwrap(state); - } else { - if (composer.selection.isCollapsed()) { - composer.selection.selectLine(); - } - - if (endToEndParent) { - var qouteEl = endToEndParent.ownerDocument.createElement('blockquote'); - wysihtml5.dom.insert(qouteEl).after(endToEndParent); - qouteEl.appendChild(endToEndParent); - } else { - composer.selection.surround({nodeName: "blockquote"}); - } - } - }); + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); }, - state: function(composer, command) { - var selectedNode = composer.selection.getSelectedNode(), - node = wysihtml5.dom.getParentElement(selectedNode, { nodeName: "BLOCKQUOTE" }, false, composer.element); - return (node) ? node : false; + state: function(composer, command) { + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); } }; -})(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); @@ -11423,12 +11952,12 @@ var doc = composer.doc, image = this.state(composer), textNode, parent; - if (image) { - // Image already selected, set the caret before it and delete it + // If image is selected and src ie empty, set the caret before it and delete the image + if (image && !value.src) { composer.selection.setBefore(image); parent = image.parentNode; parent.removeChild(image); // and it's parent <a> too if it hasn't got any other relevant child nodes @@ -11441,10 +11970,21 @@ // firefox and ie sometimes don't remove the image handles, even though the image got removed wysihtml5.quirks.redraw(composer.element); return; } + // If image selected change attributes accordingly + if (image) { + for (var key in value) { + if (value.hasOwnProperty(key)) { + image.setAttribute(key === "className" ? "class" : key, value[key]); + } + } + return; + } + + // Otherwise lets create the image image = doc.createElement(NODE_NAME); for (var i in value) { image.setAttribute(i === "className" ? "class" : i, value[i]); } @@ -11560,11 +12100,11 @@ el: null, other: false }; if (node) { - var parentLi = wysihtml5.dom.getParentElement(node, { nodeName: "LI" }), + var parentLi = wysihtml5.dom.getParentElement(node, { query: "li" }), otherNodeName = (nodeName === "UL") ? "OL" : "UL"; if (isNode(node, nodeName)) { ret.el = node; } else if (isNode(node, otherNodeName)) { @@ -11710,106 +12250,137 @@ // opera: only <i> return wysihtml5.commands.formatInline.state(composer, command, "i"); } }; ;(function(wysihtml5) { - var CLASS_NAME = "wysiwyg-text-align-center", - REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; + var nodeOptions = { + className: "wysiwyg-text-align-center", + classRegExp: /wysiwyg-text-align-[0-9a-z]+/g, + toggle: true + }; + wysihtml5.commands.justifyCenter = { exec: function(composer, command) { - return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); }, state: function(composer, command) { - return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); } }; + })(wysihtml5); ;(function(wysihtml5) { - var CLASS_NAME = "wysiwyg-text-align-left", - REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; + var nodeOptions = { + className: "wysiwyg-text-align-left", + classRegExp: /wysiwyg-text-align-[0-9a-z]+/g, + toggle: true + }; + wysihtml5.commands.justifyLeft = { exec: function(composer, command) { - return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); }, state: function(composer, command) { - return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); } }; })(wysihtml5); ;(function(wysihtml5) { - var CLASS_NAME = "wysiwyg-text-align-right", - REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; + var nodeOptions = { + className: "wysiwyg-text-align-right", + classRegExp: /wysiwyg-text-align-[0-9a-z]+/g, + toggle: true + }; + wysihtml5.commands.justifyRight = { exec: function(composer, command) { - return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); }, state: function(composer, command) { - return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); } }; })(wysihtml5); ;(function(wysihtml5) { - var CLASS_NAME = "wysiwyg-text-align-justify", - REG_EXP = /wysiwyg-text-align-[0-9a-z]+/g; + var nodeOptions = { + className: "wysiwyg-text-align-justify", + classRegExp: /wysiwyg-text-align-[0-9a-z]+/g, + toggle: true + }; + wysihtml5.commands.justifyFull = { exec: function(composer, command) { - return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); }, state: function(composer, command) { - return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, CLASS_NAME, REG_EXP); + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); } }; })(wysihtml5); ;(function(wysihtml5) { - var STYLE_STR = "text-align: right;", - REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi; + + var nodeOptions = { + styleProperty: "textAlign", + styleValue: "right", + toggle: true + }; wysihtml5.commands.alignRightStyle = { exec: function(composer, command) { - return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP); + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); }, state: function(composer, command) { - return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP); + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); } }; })(wysihtml5); ;(function(wysihtml5) { - var STYLE_STR = "text-align: left;", - REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi; + var nodeOptions = { + styleProperty: "textAlign", + styleValue: "left", + toggle: true + }; + wysihtml5.commands.alignLeftStyle = { exec: function(composer, command) { - return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP); + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); }, state: function(composer, command) { - return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP); + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); } }; + })(wysihtml5); ;(function(wysihtml5) { - var STYLE_STR = "text-align: center;", - REG_EXP = /(\s|^)text-align\s*:\s*[^;\s]+;?/gi; + var nodeOptions = { + styleProperty: "textAlign", + styleValue: "center", + toggle: true + }; + wysihtml5.commands.alignCenterStyle = { exec: function(composer, command) { - return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP); + return wysihtml5.commands.formatBlock.exec(composer, "formatBlock", nodeOptions); }, state: function(composer, command) { - return wysihtml5.commands.formatBlock.state(composer, "formatBlock", null, null, null, STYLE_STR, REG_EXP); + return wysihtml5.commands.formatBlock.state(composer, "formatBlock", nodeOptions); } }; + })(wysihtml5); ;wysihtml5.commands.redo = { exec: function(composer) { return composer.undoManager.redo(); }, @@ -12023,12 +12594,12 @@ listNode = liNode.parentNode; if (listNode.tagName === 'OL' || listNode.tagName === 'UL') { found = true; - outerListNode = wysihtml5.dom.getParentElement(listNode.parentNode, { nodeName: ['OL', 'UL']}, false, composer.element); - outerLiNode = wysihtml5.dom.getParentElement(listNode.parentNode, { nodeName: ['LI']}, false, composer.element); + outerListNode = wysihtml5.dom.getParentElement(listNode.parentNode, { query: 'ol, ul' }, false, composer.element); + outerLiNode = wysihtml5.dom.getParentElement(listNode.parentNode, { query: 'li' }, false, composer.element); if (outerListNode && outerLiNode) { if (liNode.nextSibling) { afterList = that.getAfterList(listNode, liNode); @@ -12646,11 +13217,11 @@ if (!links.length) { return; } var selectedNode = that.selection.getSelectedNode(event.target.ownerDocument), - link = dom.getParentElement(selectedNode, { nodeName: "A" }, 4), + link = dom.getParentElement(selectedNode, { query: "a" }, 4), textContent; if (!link) { return; } @@ -12711,15 +13282,15 @@ 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"]; + 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); + var parentElement = dom.getParentElement(selectedNode, { query: "p, div" }, 2); if (parentElement && dom.contains(that.element, parentElement)) { that.selection.executeAndRestore(function() { if (that.config.useLineBreaks) { dom.replaceWithChildNodes(parentElement); } else if (parentElement.nodeName !== "P") { @@ -12764,11 +13335,11 @@ } 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); + var blockElement = dom.getParentElement(that.selection.getSelectedNode(), { query: 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; @@ -12776,11 +13347,11 @@ if (blockElement.nodeName === "LI") { if (!selectedNode) { return; } - list = dom.getParentElement(selectedNode, { nodeName: LIST_TAGS }, 2); + list = dom.getParentElement(selectedNode, { query: LIST_TAGS }, 2); if (!list) { adjust(selectedNode); } } @@ -13036,50 +13607,16 @@ for(var i = 0, max = events.length; i < max; i++) { target.removeEventListener(events[i], callback, false); } }; - var deleteAroundEditable = function(selection, uneditable, element) { - // merge node with previous node from uneditable - var prevNode = selection.getPreviousNode(uneditable, true), - curNode = selection.getSelectedNode(); - - 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 { - while (curNode.firstChild) { - uneditable.parentNode.insertBefore(curNode.firstChild, uneditable); - } - } - if (curNode.parentNode) { - curNode.parentNode.removeChild(curNode); - } - selection.setBefore(first); - } else { - if (prevNode.nodeType == 1) { - prevNode.appendChild(curNode); - } else { - uneditable.parentNode.insertBefore(curNode, uneditable); - } - selection.setBefore(curNode); - } - } - }; - var handleDeleteKeyPress = function(event, composer) { var selection = composer.selection, element = composer.element; if (selection.isCollapsed()) { - if (selection.caretIsInTheBeginnig('LI')) { + if (selection.caretIsInTheBeginnig('li')) { event.preventDefault(); composer.commands.exec('outdentList'); } else if (selection.caretIsInTheBeginnig()) { event.preventDefault(); } else { @@ -13124,11 +13661,11 @@ }; var handleTabKeyDown = function(composer, element) { if (!composer.selection.isCollapsed()) { composer.selection.deleteContents(); - } else if (composer.selection.caretIsInTheBeginnig('LI')) { + } else if (composer.selection.caretIsInTheBeginnig('li')) { if (composer.commands.exec('indentList')) return; } // Is &emsp; close enough to tab. Could not find enough counter arguments for now. composer.commands.exec("insertHTML", "&emsp;"); @@ -13236,10 +13773,10 @@ var handleClick = function(event) { if (this.config.uneditableContainerClassname) { // If uneditables is configured, makes clicking on uneditable move caret after clicked element (so it can be deleted like text) // If uneditable needs text selection itself event.stopPropagation can be used to prevent this behaviour - var uneditable = wysihtml5.dom.getParentElement(event.target, { className: this.config.uneditableContainerClassname }, false, this.element); + var uneditable = wysihtml5.dom.getParentElement(event.target, { query: "." + this.config.uneditableContainerClassname }, false, this.element); if (uneditable) { this.selection.setAfter(uneditable); } } };