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   close enough to tab. Could not find enough counter arguments for now.
composer.commands.exec("insertHTML", " ");
@@ -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);
}
}
};