app/assets/source/tinymce/tinymce.jquery.js in tinymce-rails-4.1.0 vs app/assets/source/tinymce/tinymce.jquery.js in tinymce-rails-4.1.2
- old
+ new
@@ -1,6 +1,6 @@
-// 4.1.0 (2014-06-18)
+// 4.1.2 (2014-07-15)
/**
* Compiled inline version. (Library mode)
*/
@@ -742,14 +742,17 @@
* @method toArray
* @param {Object} obj Object to convert into array.
* @return {Array} Array object based in input.
*/
function toArray(obj) {
- var array = [], i, l;
+ var array = obj, i, l;
- for (i = 0, l = obj.length; i < l; i++) {
- array[i] = obj[i];
+ if (!isArray(obj)) {
+ array = [];
+ for (i = 0, l = obj.length; i < l; i++) {
+ array[i] = obj[i];
+ }
}
return array;
}
@@ -1325,13 +1328,10 @@
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
- *
- * Some of this logic is based on jQuery code that is released under
- * MIT license that grants us to sublicense it under LGPL.
*/
/**
* This class mimics most of the jQuery API:
*
@@ -1340,11 +1340,10 @@
* - DOM traversial
* - DOM manipulation
* - Event binding
*
* This is not currently implemented:
- * - Offset
* - Dimension
* - Ajax
* - Animation
* - Advanced chaining
*
@@ -1389,12 +1388,14 @@
function domManipulate(targetNodes, sourceItem, callback, reverse) {
var i;
if (isString(sourceItem)) {
- sourceItem = createFragment(sourceItem);
+ sourceItem = createFragment(sourceItem, getElementDocument(targetNodes[0]));
} else if (sourceItem.length && !sourceItem.nodeType) {
+ sourceItem = DomQuery.makeArray(sourceItem);
+
if (reverse) {
for (i = sourceItem.length - 1; i >= 0; i--) {
domManipulate(targetNodes, sourceItem[i], callback, reverse);
}
} else {
@@ -1404,13 +1405,15 @@
}
return targetNodes;
}
- i = targetNodes.length;
- while (i--) {
- callback.call(targetNodes[i], sourceItem.parentNode ? sourceItem : sourceItem);
+ if (sourceItem.nodeType) {
+ i = targetNodes.length;
+ while (i--) {
+ callback.call(targetNodes[i], sourceItem);
+ }
}
return targetNodes;
}
@@ -1444,67 +1447,16 @@
var propFix = {
'for': 'htmlFor',
'class': 'className',
'readonly': 'readOnly'
};
+ var cssFix = {
+ float: 'cssFloat'
+ };
- var attrGetHooks = {}, attrSetHooks = {};
+ var attrHooks = {}, cssHooks = {};
- function appendHooks(target, hooks) {
- each(hooks, function(key, value) {
- each(key.split(' '), function() {
- target[this] = value;
- });
- });
- }
-
- if (Env.ie && Env.ie <= 7) {
- appendHooks(attrGetHooks, {
- maxlength: function(elm, value) {
- value = elm.maxLength;
-
- if (value === 0x7fffffff) {
- return undef;
- }
-
- return value;
- },
-
- size: function(elm, value) {
- value = elm.size;
-
- if (value === 20) {
- return undef;
- }
-
- return value;
- },
-
- 'class': function(elm) {
- return elm.className;
- },
-
- style: function(elm) {
- if (elm.style.cssText.length === 0) {
- return undef;
- }
-
- return elm.style.cssText;
- }
- });
-
- appendHooks(attrSetHooks, {
- 'class': function(elm, value) {
- elm.className = value;
- },
-
- style: function(elm, value) {
- elm.style.cssText = value;
- }
- });
- }
-
function DomQuery(selector, context) {
/*eslint new-cap:0 */
return new DomQuery.fn.init(selector, context);
}
@@ -1559,10 +1511,34 @@
}
return obj;
}
+ function grep(array, callback) {
+ var out = [];
+
+ each(array, function(i, item) {
+ if (callback(item, i)) {
+ out.push(item);
+ }
+ });
+
+ return out;
+ }
+
+ function getElementDocument(element) {
+ if (!element) {
+ return doc;
+ }
+
+ if (element.nodeType == 9) {
+ return element;
+ }
+
+ return element.ownerDocument;
+ }
+
DomQuery.fn = DomQuery.prototype = {
constructor: DomQuery,
/**
* Selector for the current set.
@@ -1629,19 +1605,23 @@
match = rquickExpr.exec(selector);
}
if (match) {
if (match[1]) {
- node = createFragment(selector, context).firstChild;
+ node = createFragment(selector, getElementDocument(context)).firstChild;
while (node) {
push.call(self, node);
node = node.nextSibling;
}
} else {
- node = doc.getElementById(match[2]);
+ node = getElementDocument(context).getElementById(match[2]);
+ if (!node) {
+ return self;
+ }
+
if (node.id !== match[2]) {
return self.find(selector);
}
self.length = 1;
@@ -1716,13 +1696,14 @@
} else if (isDefined(value)) {
this.each(function() {
var hook;
if (this.nodeType === 1) {
- hook = attrSetHooks[name];
- if (hook) {
- hook(this, value, name);
+ hook = attrHooks[name];
+ if (hook && hook.set) {
+ hook.set(this, value);
+ return;
}
if (value === null) {
this.removeAttribute(name, 2);
} else {
@@ -1730,21 +1711,21 @@
}
}
});
} else {
if (self[0] && self[0].nodeType === 1) {
+ hook = attrHooks[name];
+ if (hook && hook.get) {
+ return hook.get(self[0], name);
+ }
+
if (booleanMap[name]) {
return self.prop(name) ? name : undef;
}
value = self[0].getAttribute(name, 2);
- hook = attrGetHooks[name];
- if (hook) {
- return hook(self[0], value, name);
- }
-
if (value === null) {
value = undef;
}
}
@@ -1806,56 +1787,76 @@
* @param {String/Object} name Name of style to get or an object with styles to set.
* @param {String} value Optional value to set.
* @return {tinymce.dom.DomQuery/String} Current set or the specified style when only the name is specified.
*/
css: function(name, value) {
- var self = this;
+ var self = this, elm, hook;
+ function camel(name) {
+ return name.replace(/-(\D)/g, function(a, b) {
+ return b.toUpperCase();
+ });
+ }
+
+ function dashed(name) {
+ return name.replace(/[A-Z]/g, function(a) {
+ return '-' + a;
+ });
+ }
+
if (typeof name === "object") {
each(name, function(name, value) {
self.css(name, value);
});
} else {
- // Camelcase it, if needed
- name = name.replace(/-(\D)/g, function(a, b) {
- return b.toUpperCase();
- });
-
if (isDefined(value)) {
+ name = camel(name);
+
// Default px suffix on these
if (typeof(value) === 'number' && !numericCssMap[name]) {
value += 'px';
}
self.each(function() {
var style = this.style;
- // IE specific opacity
- if (name === "opacity" && this.runtimeStyle && typeof(this.runtimeStyle.opacity) === "undefined") {
- style.filter = value === '' ? '' : "alpha(opacity=" + (value * 100) + ")";
+ hook = cssHooks[name];
+ if (hook && hook.set) {
+ hook.set(this, value);
+ return;
}
try {
- style[name] = value;
+ this.style[cssFix[name] || name] = value;
} catch (ex) {
// Ignore
}
+
+ if (value === null || value === '') {
+ if (style.removeProperty) {
+ style.removeProperty(dashed(name));
+ } else {
+ style.removeAttribute(name);
+ }
+ }
});
} else {
- if (self.context.defaultView) {
- // Remove camelcase
- name = name.replace(/[A-Z]/g, function(a) {
- return '-' + a;
- });
+ elm = self[0];
+ hook = cssHooks[name];
+ if (hook && hook.get) {
+ return hook.get(elm);
+ }
+
+ if (elm.ownerDocument.defaultView) {
try {
- return self.context.defaultView.getComputedStyle(self[0], null).getPropertyValue(name);
+ return elm.ownerDocument.defaultView.getComputedStyle(elm, null).getPropertyValue(dashed(name));
} catch (ex) {
return undef;
}
- } else if (self[0].currentStyle) {
- return self[0].currentStyle[name];
+ } else if (elm.currentStyle) {
+ return elm.currentStyle[camel(name)];
}
}
}
return self;
@@ -2103,14 +2104,12 @@
*
* @method unwrap
* @return {tinymce.dom.DomQuery} Set with unwrapped nodes.
*/
unwrap: function() {
- return this.each(function() {
- var parentNode = DomQuery(this.parentNode);
- parentNode.before(parentNode.contents());
- parentNode.remove();
+ return this.parent().each(function() {
+ DomQuery(this).replaceWith(this.childNodes);
});
},
/**
* Clones all nodes in set.
@@ -2340,41 +2339,88 @@
/**
* Filters the current set with the specified selector.
*
* @method filter
- * @param {String} selector Selector to filter elements by.
+ * @param {String/function} selector Selector to filter elements by.
* @return {tinymce.dom.DomQuery} Set with filtered elements.
*/
filter: function(selector) {
+ if (typeof selector == 'function') {
+ return DomQuery(grep(this.toArray(), function(item, i) {
+ return selector(i, item);
+ }));
+ }
+
return DomQuery(DomQuery.filter(selector, this.toArray()));
},
/**
* Gets the current node or any partent matching the specified selector.
*
* @method closest
- * @param {String} selector Selector to get element by.
+ * @param {String/Element/tinymce.dom.DomQuery} selector Selector or element to find.
* @return {tinymce.dom.DomQuery} Set with closest elements.
*/
closest: function(selector) {
var result = [];
+ if (selector instanceof DomQuery) {
+ selector = selector[0];
+ }
+
this.each(function(i, node) {
while (node) {
- if (selector.nodeType && node == selector || DomQuery(node).is(selector)) {
+ if (typeof selector == 'string' && DomQuery(node).is(selector)) {
result.push(node);
break;
+ } else if (node == selector) {
+ result.push(node);
+ break;
}
node = node.parentNode;
}
});
return DomQuery(result);
},
+ /**
+ * Returns the offset of the first element in set or sets the top/left css properties of all elements in set.
+ *
+ * @method offset
+ * @param {Object} offset Optional offset object to set on each item.
+ * @return {Object/tinymce.dom.DomQuery} Returns the first element offset or the current set if you specified an offset.
+ */
+ offset: function(offset) {
+ var elm, doc, docElm;
+ var x = 0, y = 0, pos;
+
+ if (!offset) {
+ elm = this[0];
+
+ if (elm) {
+ doc = elm.ownerDocument;
+ docElm = doc.documentElement;
+
+ if (elm.getBoundingClientRect) {
+ pos = elm.getBoundingClientRect();
+ x = pos.left + (docElm.scrollLeft || doc.body.scrollLeft) - docElm.clientLeft;
+ y = pos.top + (docElm.scrollTop || doc.body.scrollTop) - docElm.clientTop;
+ }
+ }
+
+ return {
+ left: x,
+ top: y
+ };
+ }
+
+ return this.css(offset);
+ },
+
push: push,
sort: [].sort,
splice: [].splice
};
@@ -2441,10 +2487,25 @@
* @param {String} str String to remove whitespace from.
* @return {String} New string with removed whitespace.
*/
trim: trim,
+ /**
+ * Filters out items from the input array by calling the specified function for each item.
+ * If the function returns false the item will be excluded if it returns true it will be included.
+ *
+ * @static
+ * @method grep
+ * @param {Array} array Array of items to loop though.
+ * @param {function} callback Function to call for each item. Include/exclude depends on it's return value.
+ * @return {Array} New array with values imported and filtered based in input.
+ * @example
+ * // Filter out some items, this will return an array with 4 and 5
+ * var items = DomQuery.grep([1, 2, 3, 4, 5], function(v) {return v > 3;});
+ */
+ grep: grep,
+
// Sizzle
find: Sizzle,
expr: Sizzle.selectors,
unique: Sizzle.uniqueSort,
text: Sizzle.getText,
@@ -2465,11 +2526,25 @@
});
function dir(el, prop, until) {
var matched = [], cur = el[prop];
- while (cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !DomQuery(cur).is(until))) {
+ if (typeof until != 'string' && until instanceof DomQuery) {
+ until = until[0];
+ }
+
+ while (cur && cur.nodeType !== 9) {
+ if (until !== undefined) {
+ if (cur === until) {
+ break;
+ }
+
+ if (typeof until == 'string' && DomQuery(cur).is(until)) {
+ break;
+ }
+ }
+
if (cur.nodeType === 1) {
matched.push(cur);
}
cur = cur[prop];
@@ -2479,17 +2554,27 @@
}
function sibling(node, siblingName, nodeType, until) {
var result = [];
+ if (until instanceof DomQuery) {
+ until = until[0];
+ }
+
for (; node; node = node[siblingName]) {
if (nodeType && node.nodeType !== nodeType) {
continue;
}
- if (until && ((until.nodeType && node === until) || (DomQuery(node).is(until)))) {
- break;
+ if (until !== undefined) {
+ if (node === until) {
+ break;
+ }
+
+ if (typeof until == 'string' && DomQuery(node).is(until)) {
+ break;
+ }
}
result.push(node);
}
@@ -2530,22 +2615,10 @@
parents: function(node) {
return dir(node, "parentNode");
},
/**
- * Returns a new collection with the all the parents until the matching selector/element
- * of each item in current collection matching the optional selector.
- *
- * @method parentsUntil
- * @param {String/Element} until Until the matching selector or element.
- * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
- */
- parentsUntil: function(node, until) {
- return dir(node, "parentNode", until);
- },
-
- /**
* Returns a new collection with next sibling of each item in current collection matching the optional selector.
*
* @method next
* @param {String} selector Selector to match the next element against.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
@@ -2564,53 +2637,100 @@
prev: function(node) {
return firstSibling(node, 'previousSibling', 1);
},
/**
- * Returns a new collection with all next siblings of each item in current collection matching the optional selector.
+ * Returns all child elements matching the optional selector.
*
- * @method nextUntil
- * @param {String/Element} until Until the matching selector or element.
+ * @method children
+ * @param {String} selector Selector to match the elements against.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
*/
- nextUntil: function(node, selector) {
- return sibling(node, 'nextSibling', 1, selector).slice(1);
+ children: function(node) {
+ return sibling(node.firstChild, 'nextSibling', 1);
},
/**
- * Returns a new collection with all previous siblings of each item in current collection matching the optional selector.
+ * Returns all child nodes matching the optional selector.
*
- * @method prevUntil
- * @param {String/Element} until Until the matching selector or element.
+ * @method contents
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
*/
- prevUntil: function(node, selector) {
- return sibling(node, 'previousSibling', 1, selector).slice(1);
+ contents: function(node) {
+ return Tools.toArray((node.nodeName === "iframe" ? node.contentDocument || node.contentWindow.document : node).childNodes);
+ }
+ }, function(name, fn) {
+ DomQuery.fn[name] = function(selector) {
+ var self = this, result = [];
+
+ self.each(function() {
+ var nodes = fn.call(result, this, selector, result);
+
+ if (nodes) {
+ if (DomQuery.isArray(nodes)) {
+ result.push.apply(result, nodes);
+ } else {
+ result.push(nodes);
+ }
+ }
+ });
+
+ // If traversing on multiple elements we might get the same elements twice
+ if (this.length > 1) {
+ result = DomQuery.unique(result);
+
+ if (name.indexOf('parents') === 0) {
+ result = result.reverse();
+ }
+ }
+
+ result = DomQuery(result);
+
+ if (selector) {
+ return result.filter(selector);
+ }
+
+ return result;
+ };
+ });
+
+ each({
+ /**
+ * Returns a new collection with the all the parents until the matching selector/element
+ * of each item in current collection matching the optional selector.
+ *
+ * @method parentsUntil
+ * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
+ * @return {tinymce.dom.DomQuery} New DomQuery instance with all matching parents.
+ */
+ parentsUntil: function(node, until) {
+ return dir(node, "parentNode", until);
},
/**
- * Returns all child elements matching the optional selector.
+ * Returns a new collection with all next siblings of each item in current collection matching the optional selector.
*
- * @method children
- * @param {String} selector Selector to match the elements against.
+ * @method nextUntil
+ * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
*/
- children: function(node) {
- return sibling(node.firstChild, 'nextSibling', 1);
+ nextUntil: function(node, until) {
+ return sibling(node, 'nextSibling', 1, until).slice(1);
},
/**
- * Returns all child nodes matching the optional selector.
+ * Returns a new collection with all previous siblings of each item in current collection matching the optional selector.
*
- * @method contents
+ * @method prevUntil
+ * @param {String/Element/tinymce.dom.DomQuery} until Until the matching selector or element.
* @return {tinymce.dom.DomQuery} New DomQuery instance with all matching elements.
*/
- contents: function(node) {
- return Tools.toArray((node.nodeName === "iframe" ? node.contentDocument || node.contentWindow.document : node).childNodes);
+ prevUntil: function(node, until) {
+ return sibling(node, 'previousSibling', 1, until).slice(1);
}
}, function(name, fn) {
- DomQuery.fn[name] = function(selector) {
+ DomQuery.fn[name] = function(selector, filter) {
var self = this, result = [];
self.each(function() {
var nodes = fn.call(result, this, selector, result);
@@ -2621,20 +2741,23 @@
result.push(nodes);
}
}
});
- result = DomQuery.unique(result);
+ // If traversing on multiple elements we might get the same elements twice
+ if (this.length > 1) {
+ result = DomQuery.unique(result);
- if (name.indexOf('parents') === 0 || name === 'prevUntil') {
- result = result.reverse();
+ if (name.indexOf('parents') === 0 || name === 'prevUntil') {
+ result = result.reverse();
+ }
}
result = DomQuery(result);
- if (selector && name.indexOf("Until") == -1) {
- return result.filter(selector);
+ if (filter) {
+ return result.filter(filter);
}
return result;
};
});
@@ -2655,18 +2778,102 @@
DomQuery.overrideDefaults = function(callback) {
var defaults;
function jQuerySub(selector, context) {
defaults = defaults || callback();
- return new jQuerySub.fn.init(selector || defaults.element, context || defaults.context);
+
+ if (arguments.length === 0) {
+ selector = defaults.element;
+ }
+
+ if (!context) {
+ context = defaults.context;
+ }
+
+ return new jQuerySub.fn.init(selector, context);
}
DomQuery.extend(jQuerySub, this);
return jQuerySub;
};
+ function appendHooks(targetHooks, prop, hooks) {
+ each(hooks, function(name, func) {
+ targetHooks[name] = targetHooks[name] || {};
+ targetHooks[name][prop] = func;
+ });
+ }
+
+ if (Env.ie && Env.ie < 8) {
+ appendHooks(attrHooks, 'get', {
+ maxlength: function(elm) {
+ var value = elm.maxLength;
+
+ if (value === 0x7fffffff) {
+ return undef;
+ }
+
+ return value;
+ },
+
+ size: function(elm) {
+ var value = elm.size;
+
+ if (value === 20) {
+ return undef;
+ }
+
+ return value;
+ },
+
+ 'class': function(elm) {
+ return elm.className;
+ },
+
+ style: function(elm) {
+ var value = elm.style.cssText;
+
+ if (value.length === 0) {
+ return undef;
+ }
+
+ return value;
+ }
+ });
+
+ appendHooks(attrHooks, 'set', {
+ 'class': function(elm, value) {
+ elm.className = value;
+ },
+
+ style: function(elm, value) {
+ elm.style.cssText = value;
+ }
+ });
+ }
+
+ if (Env.ie && Env.ie < 9) {
+ cssFix.float = 'styleFloat';
+
+ appendHooks(cssHooks, 'set', {
+ opacity: function(elm, value) {
+ var style = elm.style;
+
+ if (value === null || value === '') {
+ style.removeAttribute('filter');
+ } else {
+ style.zoom = 1;
+ style.filter = 'alpha(opacity=' + (value * 100) + ')';
+ }
+ }
+ });
+ }
+
+ DomQuery.attrHooks = attrHooks;
+ DomQuery.cssHooks = cssHooks;
+
return DomQuery;
});
// Included from: js/tinymce/classes/html/Styles.js
@@ -3048,34 +3255,48 @@
/**
* TreeWalker class enables you to walk the DOM in a linear manner.
*
* @class tinymce.dom.TreeWalker
+ * @example
+ * var walker = new tinymce.dom.TreeWalker(startNode);
+ *
+ * do {
+ * console.log(walker.current());
+ * } while (walker.next());
*/
define("tinymce/dom/TreeWalker", [], function() {
- return function(start_node, root_node) {
- var node = start_node;
+ /**
+ * Constructs a new TreeWalker instance.
+ *
+ * @constructor
+ * @method TreeWalker
+ * @param {Node} startNode Node to start walking from.
+ * @param {node} rootNode Optional root node to never walk out of.
+ */
+ return function(startNode, rootNode) {
+ var node = startNode;
- function findSibling(node, start_name, sibling_name, shallow) {
+ function findSibling(node, startName, siblingName, shallow) {
var sibling, parent;
if (node) {
// Walk into nodes if it has a start
- if (!shallow && node[start_name]) {
- return node[start_name];
+ if (!shallow && node[startName]) {
+ return node[startName];
}
// Return the sibling if it has one
- if (node != root_node) {
- sibling = node[sibling_name];
+ if (node != rootNode) {
+ sibling = node[siblingName];
if (sibling) {
return sibling;
}
// Walk up the parents to look for siblings
- for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) {
- sibling = parent[sibling_name];
+ for (parent = node.parentNode; parent && parent != rootNode; parent = parent.parentNode) {
+ sibling = parent[siblingName];
if (sibling) {
return sibling;
}
}
}
@@ -4383,18 +4604,66 @@
"tinymce/dom/Range",
"tinymce/html/Entities",
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/dom/StyleSheetLoader"
-], function(Sizzle, DomQuery, Styles, EventUtils, TreeWalker, Range, Entities, Env, Tools, StyleSheetLoader) {
+], function(Sizzle, $, Styles, EventUtils, TreeWalker, Range, Entities, Env, Tools, StyleSheetLoader) {
// Shorten names
- var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim, extend = Tools.extend;
- var isWebKit = Env.webkit, isIE = Env.ie;
+ var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim;
+ var isIE = Env.ie;
var simpleSelectorRe = /^([a-z0-9],?)+$/i;
var whiteSpaceRegExp = /^[ \t\r\n]*$/;
- var numericCssMap = Tools.makeMap('fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom', ' ');
+ function setupAttrHooks(domUtils, settings) {
+ var attrHooks = {}, keepValues = settings.keep_values, keepUrlHook;
+
+ keepUrlHook = {
+ set: function($elm, value, name) {
+ if (settings.url_converter) {
+ value = settings.url_converter.call(settings.url_converter_scope || domUtils, value, name, $elm[0]);
+ }
+
+ $elm.attr('data-mce-' + name, value).attr(name, value);
+ },
+
+ get: function($elm, name) {
+ return $elm.attr('data-mce-' + name) || $elm.attr(name);
+ }
+ };
+
+ attrHooks = {
+ style: {
+ set: function($elm, value) {
+ if (value !== null && typeof value === 'object') {
+ $elm.css(value);
+ return;
+ }
+
+ if (keepValues) {
+ $elm.attr('data-mce-style', value);
+ }
+
+ $elm.attr('style', value);
+ },
+
+ get: function($elm) {
+ var value = $elm.attr('data-mce-style') || $elm.attr('style');
+
+ value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName);
+
+ return value;
+ }
+ }
+ };
+
+ if (keepValues) {
+ attrHooks.href = attrHooks.src = keepUrlHook;
+ }
+
+ return attrHooks;
+ }
+
/**
* Constructs a new DOMUtils instance. Consult the Wiki for more details on settings etc for this class.
*
* @constructor
* @method DOMUtils
@@ -4408,29 +4677,24 @@
self.win = window;
self.files = {};
self.counter = 0;
self.stdMode = !isIE || doc.documentMode >= 8;
self.boxModel = !isIE || doc.compatMode == "CSS1Compat" || self.stdMode;
- self.hasOuterHTML = "outerHTML" in doc.createElement("a");
self.styleSheetLoader = new StyleSheetLoader(doc);
- this.boundEvents = [];
-
- self.settings = settings = extend({
- keep_values: false,
- hex_colors: 1
- }, settings);
-
+ self.boundEvents = [];
+ self.settings = settings = settings || {};
self.schema = settings.schema;
self.styles = new Styles({
url_converter: settings.url_converter,
url_converter_scope: settings.url_converter_scope
}, settings.schema);
self.fixDoc(doc);
self.events = settings.ownEvents ? new EventUtils(settings.proxy) : EventUtils.Event;
+ self.attrHooks = setupAttrHooks(self, settings);
blockElementsMap = settings.schema ? settings.schema.getBlockElements() : {};
- self.$ = DomQuery.overrideDefaults(function() {
+ self.$ = $.overrideDefaults(function() {
return {
context: doc,
element: self.getRoot()
};
});
@@ -4459,26 +4723,20 @@
return !!blockElementsMap[node];
};
}
DOMUtils.prototype = {
- root: null,
- props: {
- "for": "htmlFor",
- "class": "className",
- className: "className",
- checked: "checked",
- disabled: "disabled",
- maxlength: "maxLength",
- readonly: "readOnly",
- selected: "selected",
- value: "value",
- id: "id",
- name: "name",
- type: "type"
+ $$: function(elm) {
+ if (typeof elm == 'string') {
+ elm = this.get(elm);
+ }
+
+ return this.$(elm);
},
+ root: null,
+
fixDoc: function(doc) {
var settings = this.settings, name;
if (isIE && settings.schema) {
// Add missing HTML 4/5 elements to IE
@@ -4516,23 +4774,11 @@
self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName));
});
return clone;
}
-/*
- // Setup HTML5 patched document fragment
- if (!self.frag) {
- self.frag = doc.createDocumentFragment();
- self.fixDoc(self.frag);
- }
- // Make a deep copy by adding it to the document fragment then removing it this removed the :section
- clone = doc.createElement('div');
- self.frag.appendChild(clone);
- clone.innerHTML = node.outerHTML;
- self.frag.removeChild(clone);
-*/
return clone.firstChild;
},
/**
* Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not
@@ -4542,11 +4788,11 @@
* @return {Element} Root element for the utility class.
*/
getRoot: function() {
var self = this;
- return self.get(self.settings.root_element) || self.doc.body;
+ return self.settings.root_element || self.doc.body;
},
/**
* Returns the viewport of the window.
*
@@ -4755,12 +5001,12 @@
* tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass')
*/
select: function(selector, scope) {
var self = this;
- //Sizzle.selectors.cacheLength = 0;
- return Sizzle(selector, self.get(scope) || self.get(self.settings.root_element) || self.doc, []);
+ /*eslint new-cap:0 */
+ return Sizzle(selector, self.get(scope) || self.settings.root_element || self.doc, []);
},
/**
* Returns true/false if the specified element matches the specified css pattern.
*
@@ -4797,10 +5043,12 @@
if (elm.nodeType && elm.nodeType != 1) {
return false;
}
var elms = elm.nodeType ? [elm] : elm;
+
+ /*eslint new-cap:0 */
return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0;
},
// #endif
@@ -4914,42 +5162,41 @@
/**
* Removes/deletes the specified element(s) from the DOM.
*
* @method remove
* @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids.
- * @param {Boolean} keep_children Optional state to keep children or not. If set to true all children will be
+ * @param {Boolean} keepChildren Optional state to keep children or not. If set to true all children will be
* placed at the location of the removed element.
* @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements
* were passed in.
* @example
* // Removes all paragraphs in the active editor
* tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p'));
*
* // Removes an element by id in the document
* tinymce.DOM.remove('mydiv');
*/
- remove: function(node, keep_children) {
- return this.run(node, function(node) {
- var child, parent = node.parentNode;
+ remove: function(node, keepChildren) {
+ node = this.$$(node);
- if (!parent) {
- return null;
- }
+ if (keepChildren) {
+ node.each(function() {
+ var child;
- if (keep_children) {
- while ((child = node.firstChild)) {
- // IE 8 will crash if you don't remove completely empty text nodes
- if (!isIE || child.nodeType !== 3 || child.nodeValue) {
- parent.insertBefore(child, node);
+ while ((child = this.firstChild)) {
+ if (child.nodeType == 3 && child.data.length === 0) {
+ this.removeChild(child);
} else {
- node.removeChild(child);
+ this.parentNode.insertBefore(child, this);
}
}
- }
+ }).remove();
+ } else {
+ node.remove();
+ }
- return parent.removeChild(node);
- });
+ return node.length > 1 ? node.toArray() : node[0];
},
/**
* Sets the CSS style value on a HTML element. The name can be a camelcase string
* or the CSS style name like background-color.
@@ -4964,54 +5211,15 @@
*
* // Sets a style value to an element by id in the current document
* tinymce.DOM.setStyle('mydiv', 'background-color', 'red');
*/
setStyle: function(elm, name, value) {
- return this.run(elm, function(elm) {
- var self = this, style, key;
+ elm = this.$$(elm).css(name, value);
- if (name) {
- if (typeof(name) === 'string') {
- style = elm.style;
-
- // Camelcase it, if needed
- name = name.replace(/-(\D)/g, function(a, b) {
- return b.toUpperCase();
- });
-
- // Default px suffix on these
- if (((typeof(value) === 'number') || /^[\-0-9\.]+$/.test(value)) && !numericCssMap[name]) {
- value += 'px';
- }
-
- // IE specific opacity
- if (name === "opacity" && elm.runtimeStyle && typeof(elm.runtimeStyle.opacity) === "undefined") {
- style.filter = value === '' ? '' : "alpha(opacity=" + (value * 100) + ")";
- }
-
- if (name == "float") {
- // Old IE vs modern browsers
- name = "cssFloat" in elm.style ? "cssFloat" : "styleFloat";
- }
-
- try {
- style[name] = value;
- } catch (ex) {
- // Ignore IE errors
- }
-
- // Force update of the style data
- if (self.settings.update_styles) {
- elm.removeAttribute('data-mce-style');
- }
- } else {
- for (key in name) {
- self.setStyle(elm, key, name[key]);
- }
- }
- }
- });
+ if (this.settings.update_styles) {
+ elm.attr('data-mce-style', null);
+ }
},
/**
* Returns the current style or runtime/computed value of an element.
*
@@ -5020,46 +5228,26 @@
* @param {String} name Style name to return.
* @param {Boolean} computed Computed style.
* @return {String} Current style or computed style value of an element.
*/
getStyle: function(elm, name, computed) {
- elm = this.get(elm);
+ elm = this.$$(elm);
- if (!elm) {
- return;
+ if (computed) {
+ return elm.css(name);
}
- // W3C
- if (this.doc.defaultView && computed) {
- // Remove camelcase
- name = name.replace(/[A-Z]/g, function(a) {
- return '-' + a;
- });
-
- try {
- return this.doc.defaultView.getComputedStyle(elm, null).getPropertyValue(name);
- } catch (ex) {
- // Old safari might fail
- return null;
- }
- }
-
// Camelcase it, if needed
name = name.replace(/-(\D)/g, function(a, b) {
return b.toUpperCase();
});
if (name == 'float') {
name = isIE ? 'styleFloat' : 'cssFloat';
}
- // IE & Opera
- if (elm.currentStyle && computed) {
- return elm.currentStyle[name];
- }
-
- return elm.style ? elm.style[name] : undefined;
+ return elm[0] && elm[0].style ? elm[0].style[name] : undefined;
},
/**
* Sets multiple styles on the specified element(s).
*
@@ -5072,17 +5260,13 @@
*
* // Sets styles to an element by id in the current document
* tinymce.DOM.setStyles('mydiv', {'background-color': 'red', 'color': 'green'});
*/
setStyles: function(elm, styles) {
- this.setStyle(elm, styles);
+ this.$$(elm).css(styles);
},
- css: function(elm, name, value) {
- this.setStyle(elm, name, value);
- },
-
/**
* Removes all attributes from an element or elements.
*
* @method removeAllAttribs
* @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from.
@@ -5109,77 +5293,33 @@
*
* // Sets class attribute on a specific element in the current page
* tinymce.dom.setAttrib('mydiv', 'class', 'myclass');
*/
setAttrib: function(elm, name, value) {
- var self = this;
+ var self = this, originalValue, hook, settings = self.settings;
- // What's the point
- if (!elm || !name) {
- return;
+ if (value === '') {
+ value = null;
}
- return this.run(elm, function(elm) {
- var settings = self.settings;
- var originalValue = elm.getAttribute(name);
+ elm = self.$$(elm);
+ originalValue = elm.attr(name);
- if (value !== null) {
- switch (name) {
- case "style":
- if (!is(value, 'string')) {
- each(value, function(value, name) {
- self.setStyle(elm, name, value);
- });
+ hook = self.attrHooks[name];
+ if (hook && hook.set) {
+ hook.set(elm, value, name);
+ } else {
+ elm.attr(name, value);
+ }
- return;
- }
-
- // No mce_style for elements with these since they might get resized by the user
- if (settings.keep_values) {
- if (value) {
- elm.setAttribute('data-mce-style', value, 2);
- } else {
- elm.removeAttribute('data-mce-style', 2);
- }
- }
-
- elm.style.cssText = value;
- break;
-
- case "class":
- elm.className = value || ''; // Fix IE null bug
- break;
-
- case "src":
- case "href":
- if (settings.keep_values) {
- if (settings.url_converter) {
- value = settings.url_converter.call(settings.url_converter_scope || self, value, name, elm);
- }
-
- self.setAttrib(elm, 'data-mce-' + name, value, 2);
- }
-
- break;
-
- case "shape":
- elm.setAttribute('data-mce-style', value);
- break;
- }
- }
-
- if (is(value) && value !== null && value.length !== 0) {
- elm.setAttribute(name, '' + value, 2);
- } else {
- elm.removeAttribute(name, 2);
- }
-
- // fire onChangeAttrib event for attributes that have changed
- if (originalValue != value && settings.onSetAttrib) {
- settings.onSetAttrib({attrElm: elm, attrName: name, attrValue: value});
- }
- });
+ if (originalValue != value && settings.onSetAttrib) {
+ settings.onSetAttrib({
+ attrElm: elm,
+ attrName: name,
+ attrValue: value
+ });
+ }
},
/**
* Sets two or more specified attributes of an element or elements.
*
@@ -5194,13 +5334,13 @@
* tinymce.DOM.setAttribs('mydiv', {'class': 'myclass', title: 'some title'});
*/
setAttribs: function(elm, attrs) {
var self = this;
- return this.run(elm, function(elm) {
+ self.$$(elm).each(function(i, node) {
each(attrs, function(value, name) {
- self.setAttrib(elm, name, value);
+ self.setAttrib(node, name, value);
});
});
},
/**
@@ -5211,143 +5351,26 @@
* @param {String} name Name of attribute to get.
* @param {String} defaultVal Optional default value to return if the attribute didn't exist.
* @return {String} Attribute value string, default value or null if the attribute wasn't found.
*/
getAttrib: function(elm, name, defaultVal) {
- var value, self = this, undef;
+ var self = this, hook, value;
- elm = self.get(elm);
+ elm = self.$$(elm);
- if (!elm || elm.nodeType !== 1) {
- return defaultVal === undef ? false : defaultVal;
+ hook = self.attrHooks[name];
+ if (hook && hook.get) {
+ value = hook.get(elm, name);
+ } else {
+ value = elm.attr(name);
}
- if (!is(defaultVal)) {
- defaultVal = '';
+ if (typeof value == 'undefined') {
+ value = defaultVal || '';
}
- // Try the mce variant for these
- if (/^(src|href|style|coords|shape)$/.test(name)) {
- value = elm.getAttribute("data-mce-" + name);
-
- if (value) {
- return value;
- }
- }
-
- if (isIE && self.props[name]) {
- value = elm[self.props[name]];
- value = value && value.nodeValue ? value.nodeValue : value;
- }
-
- if (!value) {
- value = elm.getAttribute(name, 2);
- }
-
- // Check boolean attribs
- if (/^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noshade|nowrap|readonly|selected)$/.test(name)) {
- if (elm[self.props[name]] === true && value === '') {
- return name;
- }
-
- return value ? name : '';
- }
-
- // Inner input elements will override attributes on form elements
- if (elm.nodeName === "FORM" && elm.getAttributeNode(name)) {
- return elm.getAttributeNode(name).nodeValue;
- }
-
- if (name === 'style') {
- value = value || elm.style.cssText;
-
- if (value) {
- value = self.serializeStyle(self.parseStyle(value), elm.nodeName);
-
- if (self.settings.keep_values) {
- elm.setAttribute('data-mce-style', value);
- }
- }
- }
-
- // Remove Apple and WebKit stuff
- if (isWebKit && name === "class" && value) {
- value = value.replace(/(apple|webkit)\-[a-z\-]+/gi, '');
- }
-
- // Handle IE issues
- if (isIE) {
- switch (name) {
- case 'rowspan':
- case 'colspan':
- // IE returns 1 as default value
- if (value === 1) {
- value = '';
- }
-
- break;
-
- case 'size':
- // IE returns +0 as default value for size
- if (value === '+0' || value === 20 || value === 0) {
- value = '';
- }
-
- break;
-
- case 'width':
- case 'height':
- case 'vspace':
- case 'checked':
- case 'disabled':
- case 'readonly':
- if (value === 0) {
- value = '';
- }
-
- break;
-
- case 'hspace':
- // IE returns -1 as default value
- if (value === -1) {
- value = '';
- }
-
- break;
-
- case 'maxlength':
- case 'tabindex':
- // IE returns default value
- if (value === 32768 || value === 2147483647 || value === '32768') {
- value = '';
- }
-
- break;
-
- case 'multiple':
- case 'compact':
- case 'noshade':
- case 'nowrap':
- if (value === 65535) {
- return name;
- }
-
- return defaultVal;
-
- case 'shape':
- value = value.toLowerCase();
- break;
-
- default:
- // IE has odd anonymous function for event attributes
- if (name.indexOf('on') === 0 && value) {
- value = ('' + value).replace(/^function\s+\w+\(\)\s+\{\s+(.*)\s+\}$/, '$1');
- }
- }
- }
-
- return (value !== undef && value !== null && value !== '') ? '' + value : defaultVal;
+ return value;
},
/**
* Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields.
*
@@ -5537,26 +5560,11 @@
*
* // Adds a class to a specific element in the current page
* tinymce.DOM.addClass('mydiv', 'myclass');
*/
addClass: function(elm, cls) {
- return this.run(elm, function(elm) {
- var clsVal;
-
- if (!cls) {
- return 0;
- }
-
- if (this.hasClass(elm, cls)) {
- return elm.className;
- }
-
- clsVal = this.removeClass(elm, cls);
- elm.className = clsVal = (clsVal !== '' ? (clsVal + ' ') : '') + cls;
-
- return clsVal;
- });
+ this.$$(elm).addClass(cls);
},
/**
* Removes a class from the specified element or elements.
*
@@ -5571,36 +5579,11 @@
*
* // Removes a class from a specific element in the current page
* tinymce.DOM.removeClass('mydiv', 'myclass');
*/
removeClass: function(elm, cls) {
- var self = this, re;
-
- return self.run(elm, function(elm) {
- var val;
-
- if (self.hasClass(elm, cls)) {
- if (!re) {
- re = new RegExp("(^|\\s+)" + cls + "(\\s+|$)", "g");
- }
-
- val = elm.className.replace(re, ' ');
- val = trim(val != ' ' ? val : '');
-
- elm.className = val;
-
- // Empty class attr
- if (!val) {
- elm.removeAttribute('class');
- elm.removeAttribute('className');
- }
-
- return val;
- }
-
- return elm.className;
- });
+ this.toggleClass(elm, cls, false);
},
/**
* Returns true if the specified element has the specified class.
*
@@ -5608,17 +5591,11 @@
* @param {String/Element} n HTML element or element id string to check CSS class on.
* @param {String} c CSS class to check for.
* @return {Boolean} true/false if the specified element has the specified class.
*/
hasClass: function(elm, cls) {
- elm = this.get(elm);
-
- if (!elm || !cls) {
- return false;
- }
-
- return (' ' + elm.className + ' ').indexOf(' ' + cls + ' ') !== -1;
+ return this.$$(elm).hasClass(cls);
},
/**
* Toggles the specified class on/off.
*
@@ -5626,29 +5603,25 @@
* @param {Element} elm Element to toggle class on.
* @param {[type]} cls Class to toggle on/off.
* @param {[type]} state Optional state to set.
*/
toggleClass: function(elm, cls, state) {
- state = state === undefined ? !this.hasClass(elm, cls) : state;
-
- if (this.hasClass(elm, cls) !== state) {
- if (state) {
- this.addClass(elm, cls);
- } else {
- this.removeClass(elm, cls);
+ this.$$(elm).toggleClass(cls, state).each(function() {
+ if (this.className === '') {
+ $(this).attr('class', null);
}
- }
+ });
},
/**
* Shows the specified element(s) by ID by setting the "display" style.
*
* @method show
* @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show.
*/
show: function(elm) {
- return this.setStyle(elm, 'display', 'block');
+ this.$$(elm).show();
},
/**
* Hides the specified element(s) by ID by setting the "display" style.
*
@@ -5657,24 +5630,22 @@
* @example
* // Hides an element by id in the document
* tinymce.DOM.hide('myid');
*/
hide: function(elm) {
- return this.setStyle(elm, 'display', 'none');
+ this.$$(elm).hide();
},
/**
* Returns true/false if the element is hidden or not by checking the "display" style.
*
* @method isHidden
* @param {String/Element} e Id or element to check display state on.
* @return {Boolean} true/false if the element is hidden or not.
*/
isHidden: function(elm) {
- elm = this.get(elm);
-
- return !elm || elm.style.display == 'none' || this.getStyle(elm, 'display') == 'none';
+ return this.$$(elm).css('display') == 'none';
},
/**
* Returns a unique id. This can be useful when generating elements on the fly.
* This method will not check if the element already exists.
@@ -5690,57 +5661,48 @@
/**
* Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means
* URLs will get converted, hex color values fixed etc. Check processHTML for details.
*
* @method setHTML
- * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to set HTML inside of.
+ * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set HTML inside of.
* @param {String} h HTML content to set as inner HTML of the element.
* @example
* // Sets the inner HTML of all paragraphs in the active editor
* tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html');
*
* // Sets the inner HTML of an element by id in the document
* tinymce.DOM.setHTML('mydiv', 'some inner html');
*/
- setHTML: function(element, html) {
- var self = this;
+ setHTML: function(elm, html) {
+ elm = this.$$(elm);
- return self.run(element, function(element) {
- if (isIE) {
+ if (isIE) {
+ elm.each(function(i, target) {
+ if (target.canHaveHTML === false) {
+ return;
+ }
+
// Remove all child nodes, IE keeps empty text nodes in DOM
- while (element.firstChild) {
- element.removeChild(element.firstChild);
+ while (target.firstChild) {
+ target.removeChild(target.firstChild);
}
try {
// IE will remove comments from the beginning
// unless you padd the contents with something
- element.innerHTML = '<br />' + html;
- element.removeChild(element.firstChild);
+ target.innerHTML = '<br>' + html;
+ target.removeChild(target.firstChild);
} catch (ex) {
- // IE sometimes produces an unknown runtime error on innerHTML if it's a block element
- // within a block element for example a div inside a p
- // This seems to fix this problem
-
- // Create new div with HTML contents and a BR in front to keep comments
- var newElement = self.create('div');
- newElement.innerHTML = '<br />' + html;
-
- // Add all children from div to target
- each(grep(newElement.childNodes), function(node, i) {
- // Skip br element
- if (i && element.canHaveHTML) {
- element.appendChild(node);
- }
- });
+ // IE sometimes produces an unknown runtime error on innerHTML if it's a div inside a p
+ $('<div>').html('<br>' + html).contents().slice(1).appendTo(target);
}
- } else {
- element.innerHTML = html;
- }
- return html;
- });
+ return html;
+ });
+ } else {
+ elm.html(html);
+ }
},
/**
* Returns the outer HTML of an element.
*
@@ -5750,26 +5712,12 @@
* @example
* tinymce.DOM.getOuterHTML(editorElement);
* tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody());
*/
getOuterHTML: function(elm) {
- var doc, self = this;
-
- elm = self.get(elm);
-
- if (!elm) {
- return null;
- }
-
- if (elm.nodeType === 1 && self.hasOuterHTML) {
- return elm.outerHTML;
- }
-
- doc = (elm.ownerDocument || self.doc).createElement("body");
- doc.appendChild(elm.cloneNode(true));
-
- return doc.innerHTML;
+ elm = this.get(elm);
+ return elm.nodeType == 1 ? elm.outerHTML : $('<div>').append($(elm).clone()).html();
},
/**
* Sets the specified outer HTML on an element or elements.
*
@@ -5782,49 +5730,20 @@
* tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '<div>some html</div>');
*
* // Sets the outer HTML of an element by id in the document
* tinymce.DOM.setOuterHTML('mydiv', '<div>some html</div>');
*/
- setOuterHTML: function(elm, html, doc) {
+ setOuterHTML: function(elm, html) {
var self = this;
- return self.run(elm, function(elm) {
- function set() {
- var node, tempElm;
-
- tempElm = doc.createElement("body");
- tempElm.innerHTML = html;
-
- node = tempElm.lastChild;
- while (node) {
- self.insertAfter(node.cloneNode(true), elm);
- node = node.previousSibling;
- }
-
- self.remove(elm);
+ self.$$(elm).each(function() {
+ try {
+ this.outerHTML = html;
+ } catch (ex) {
+ // OuterHTML for IE it sometimes produces an "unknown runtime error"
+ self.remove($(this).html(html), true);
}
-
- // Only set HTML on elements
- if (elm.nodeType == 1) {
- doc = doc || elm.ownerDocument || self.doc;
-
- if (isIE) {
- try {
- // Try outerHTML for IE it sometimes produces an unknown runtime error
- if (elm.nodeType == 1 && self.hasOuterHTML) {
- elm.outerHTML = html;
- } else {
- set();
- }
- } catch (ex) {
- // Fix for unknown runtime error
- set();
- }
- } else {
- set();
- }
- }
});
},
/**
* Entity decodes a string. This method decodes any HTML entities, such as å.
@@ -5850,18 +5769,18 @@
* @method insertAfter
* @param {Element} node Element to insert after the reference.
* @param {Element/String/Array} reference_node Reference element, element id or array of elements to insert after.
* @return {Element/Array} Element that got added or an array with elements.
*/
- insertAfter: function(node, reference_node) {
- reference_node = this.get(reference_node);
+ insertAfter: function(node, referenceNode) {
+ referenceNode = this.get(referenceNode);
return this.run(node, function(node) {
var parent, nextSibling;
- parent = reference_node.parentNode;
- nextSibling = reference_node.nextSibling;
+ parent = referenceNode.parentNode;
+ nextSibling = referenceNode.nextSibling;
if (nextSibling) {
parent.insertBefore(node, nextSibling);
} else {
parent.appendChild(node);
@@ -5912,12 +5831,12 @@
if (elm.nodeName != name.toUpperCase()) {
// Rename block element
newElm = self.create(name);
// Copy attribs to new block
- each(self.getAttribs(elm), function(attr_node) {
- self.setAttrib(newElm, attr_node.nodeName, self.getAttrib(elm, attr_node.nodeName));
+ each(self.getAttribs(elm), function(attrNode) {
+ self.setAttrib(newElm, attrNode.nodeName, self.getAttrib(elm, attrNode.nodeName));
});
// Replace block
self.replace(newElm, elm, 1);
}
@@ -6993,10 +6912,641 @@
* }
* });
* });
*/
+// Included from: js/tinymce/classes/dom/RangeUtils.js
+
+/**
+ * RangeUtils.js
+ *
+ * Copyright, Moxiecode Systems AB
+ * Released under LGPL License.
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class contains a few utility methods for ranges.
+ *
+ * @class tinymce.dom.RangeUtils
+ * @private
+ */
+define("tinymce/dom/RangeUtils", [
+ "tinymce/util/Tools",
+ "tinymce/dom/TreeWalker"
+], function(Tools, TreeWalker) {
+ var each = Tools.each;
+
+ function getEndChild(container, index) {
+ var childNodes = container.childNodes;
+
+ index--;
+
+ if (index > childNodes.length - 1) {
+ index = childNodes.length - 1;
+ } else if (index < 0) {
+ index = 0;
+ }
+
+ return childNodes[index] || container;
+ }
+
+ function RangeUtils(dom) {
+ /**
+ * Walks the specified range like object and executes the callback for each sibling collection it finds.
+ *
+ * @method walk
+ * @param {Object} rng Range like object.
+ * @param {function} callback Callback function to execute for each sibling collection.
+ */
+ this.walk = function(rng, callback) {
+ var startContainer = rng.startContainer,
+ startOffset = rng.startOffset,
+ endContainer = rng.endContainer,
+ endOffset = rng.endOffset,
+ ancestor, startPoint,
+ endPoint, node, parent, siblings, nodes;
+
+ // Handle table cell selection the table plugin enables
+ // you to fake select table cells and perform formatting actions on them
+ nodes = dom.select('td.mce-item-selected,th.mce-item-selected');
+ if (nodes.length > 0) {
+ each(nodes, function(node) {
+ callback([node]);
+ });
+
+ return;
+ }
+
+ /**
+ * Excludes start/end text node if they are out side the range
+ *
+ * @private
+ * @param {Array} nodes Nodes to exclude items from.
+ * @return {Array} Array with nodes excluding the start/end container if needed.
+ */
+ function exclude(nodes) {
+ var node;
+
+ // First node is excluded
+ node = nodes[0];
+ if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
+ nodes.splice(0, 1);
+ }
+
+ // Last node is excluded
+ node = nodes[nodes.length - 1];
+ if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
+ nodes.splice(nodes.length - 1, 1);
+ }
+
+ return nodes;
+ }
+
+ /**
+ * Collects siblings
+ *
+ * @private
+ * @param {Node} node Node to collect siblings from.
+ * @param {String} name Name of the sibling to check for.
+ * @return {Array} Array of collected siblings.
+ */
+ function collectSiblings(node, name, end_node) {
+ var siblings = [];
+
+ for (; node && node != end_node; node = node[name]) {
+ siblings.push(node);
+ }
+
+ return siblings;
+ }
+
+ /**
+ * Find an end point this is the node just before the common ancestor root.
+ *
+ * @private
+ * @param {Node} node Node to start at.
+ * @param {Node} root Root/ancestor element to stop just before.
+ * @return {Node} Node just before the root element.
+ */
+ function findEndPoint(node, root) {
+ do {
+ if (node.parentNode == root) {
+ return node;
+ }
+
+ node = node.parentNode;
+ } while (node);
+ }
+
+ function walkBoundary(start_node, end_node, next) {
+ var siblingName = next ? 'nextSibling' : 'previousSibling';
+
+ for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
+ parent = node.parentNode;
+ siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
+
+ if (siblings.length) {
+ if (!next) {
+ siblings.reverse();
+ }
+
+ callback(exclude(siblings));
+ }
+ }
+ }
+
+ // If index based start position then resolve it
+ if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
+ startContainer = startContainer.childNodes[startOffset];
+ }
+
+ // If index based end position then resolve it
+ if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
+ endContainer = getEndChild(endContainer, endOffset);
+ }
+
+ // Same container
+ if (startContainer == endContainer) {
+ return callback(exclude([startContainer]));
+ }
+
+ // Find common ancestor and end points
+ ancestor = dom.findCommonAncestor(startContainer, endContainer);
+
+ // Process left side
+ for (node = startContainer; node; node = node.parentNode) {
+ if (node === endContainer) {
+ return walkBoundary(startContainer, ancestor, true);
+ }
+
+ if (node === ancestor) {
+ break;
+ }
+ }
+
+ // Process right side
+ for (node = endContainer; node; node = node.parentNode) {
+ if (node === startContainer) {
+ return walkBoundary(endContainer, ancestor);
+ }
+
+ if (node === ancestor) {
+ break;
+ }
+ }
+
+ // Find start/end point
+ startPoint = findEndPoint(startContainer, ancestor) || startContainer;
+ endPoint = findEndPoint(endContainer, ancestor) || endContainer;
+
+ // Walk left leaf
+ walkBoundary(startContainer, startPoint, true);
+
+ // Walk the middle from start to end point
+ siblings = collectSiblings(
+ startPoint == startContainer ? startPoint : startPoint.nextSibling,
+ 'nextSibling',
+ endPoint == endContainer ? endPoint.nextSibling : endPoint
+ );
+
+ if (siblings.length) {
+ callback(exclude(siblings));
+ }
+
+ // Walk right leaf
+ walkBoundary(endContainer, endPoint);
+ };
+
+ /**
+ * Splits the specified range at it's start/end points.
+ *
+ * @private
+ * @param {Range/RangeObject} rng Range to split.
+ * @return {Object} Range position object.
+ */
+ this.split = function(rng) {
+ var startContainer = rng.startContainer,
+ startOffset = rng.startOffset,
+ endContainer = rng.endContainer,
+ endOffset = rng.endOffset;
+
+ function splitText(node, offset) {
+ return node.splitText(offset);
+ }
+
+ // Handle single text node
+ if (startContainer == endContainer && startContainer.nodeType == 3) {
+ if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
+ endContainer = splitText(startContainer, startOffset);
+ startContainer = endContainer.previousSibling;
+
+ if (endOffset > startOffset) {
+ endOffset = endOffset - startOffset;
+ startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
+ endOffset = endContainer.nodeValue.length;
+ startOffset = 0;
+ } else {
+ endOffset = 0;
+ }
+ }
+ } else {
+ // Split startContainer text node if needed
+ if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
+ startContainer = splitText(startContainer, startOffset);
+ startOffset = 0;
+ }
+
+ // Split endContainer text node if needed
+ if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
+ endContainer = splitText(endContainer, endOffset).previousSibling;
+ endOffset = endContainer.nodeValue.length;
+ }
+ }
+
+ return {
+ startContainer: startContainer,
+ startOffset: startOffset,
+ endContainer: endContainer,
+ endOffset: endOffset
+ };
+ };
+
+ /**
+ * Normalizes the specified range by finding the closest best suitable caret location.
+ *
+ * @private
+ * @param {Range} rng Range to normalize.
+ * @return {Boolean} True/false if the specified range was normalized or not.
+ */
+ this.normalize = function(rng) {
+ var normalized, collapsed;
+
+ function normalizeEndPoint(start) {
+ var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap;
+ var directionLeft, isAfterNode;
+
+ function hasBrBeforeAfter(node, left) {
+ var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body);
+
+ while ((node = walker[left ? 'prev' : 'next']())) {
+ if (node.nodeName === "BR") {
+ return true;
+ }
+ }
+ }
+
+ function isPrevNode(node, name) {
+ return node.previousSibling && node.previousSibling.nodeName == name;
+ }
+
+ // Walks the dom left/right to find a suitable text node to move the endpoint into
+ // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
+ function findTextNodeRelative(left, startNode) {
+ var walker, lastInlineElement, parentBlockContainer;
+
+ startNode = startNode || container;
+ parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body;
+
+ // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
+ // This: <p><br>|</p> becomes <p>|<br></p>
+ if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) {
+ container = startNode.parentNode;
+ offset = dom.nodeIndex(startNode);
+ normalized = true;
+ return;
+ }
+
+ // Walk left until we hit a text node we can move to or a block/br/img
+ walker = new TreeWalker(startNode, parentBlockContainer);
+ while ((node = walker[left ? 'prev' : 'next']())) {
+ // Break if we hit a non content editable node
+ if (dom.getContentEditableParent(node) === "false") {
+ return;
+ }
+
+ // Found text node that has a length
+ if (node.nodeType === 3 && node.nodeValue.length > 0) {
+ container = node;
+ offset = left ? node.nodeValue.length : 0;
+ normalized = true;
+ return;
+ }
+
+ // Break if we find a block or a BR/IMG/INPUT etc
+ if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
+ return;
+ }
+
+ lastInlineElement = node;
+ }
+
+ // Only fetch the last inline element when in caret mode for now
+ if (collapsed && lastInlineElement) {
+ container = lastInlineElement;
+ normalized = true;
+ offset = 0;
+ }
+ }
+
+ container = rng[(start ? 'start' : 'end') + 'Container'];
+ offset = rng[(start ? 'start' : 'end') + 'Offset'];
+ isAfterNode = container.nodeType == 1 && offset === container.childNodes.length;
+ nonEmptyElementsMap = dom.schema.getNonEmptyElements();
+ directionLeft = start;
+
+ if (container.nodeType == 1 && offset > container.childNodes.length - 1) {
+ directionLeft = false;
+ }
+
+ // If the container is a document move it to the body element
+ if (container.nodeType === 9) {
+ container = dom.getRoot();
+ offset = 0;
+ }
+
+ // If the container is body try move it into the closest text node or position
+ if (container === body) {
+ // If start is before/after a image, table etc
+ if (directionLeft) {
+ node = container.childNodes[offset > 0 ? offset - 1 : 0];
+ if (node) {
+ if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") {
+ return;
+ }
+ }
+ }
+
+ // Resolve the index
+ if (container.hasChildNodes()) {
+ offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
+ container = container.childNodes[offset];
+ offset = 0;
+
+ // Don't walk into elements that doesn't have any child nodes like a IMG
+ if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) {
+ // Walk the DOM to find a text node to place the caret at or a BR
+ node = container;
+ walker = new TreeWalker(container, body);
+
+ do {
+ // Found a text node use that position
+ if (node.nodeType === 3 && node.nodeValue.length > 0) {
+ offset = directionLeft ? 0 : node.nodeValue.length;
+ container = node;
+ normalized = true;
+ break;
+ }
+
+ // Found a BR/IMG element that we can place the caret before
+ if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
+ offset = dom.nodeIndex(node);
+ container = node.parentNode;
+
+ // Put caret after image when moving the end point
+ if (node.nodeName == "IMG" && !directionLeft) {
+ offset++;
+ }
+
+ normalized = true;
+ break;
+ }
+ } while ((node = (directionLeft ? walker.next() : walker.prev())));
+ }
+ }
+ }
+
+ // Lean the caret to the left if possible
+ if (collapsed) {
+ // So this: <b>x</b><i>|x</i>
+ // Becomes: <b>x|</b><i>x</i>
+ // Seems that only gecko has issues with this
+ if (container.nodeType === 3 && offset === 0) {
+ findTextNodeRelative(true);
+ }
+
+ // Lean left into empty inline elements when the caret is before a BR
+ // So this: <i><b></b><i>|<br></i>
+ // Becomes: <i><b>|</b><i><br></i>
+ // Seems that only gecko has issues with this.
+ // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p>
+ if (container.nodeType === 1) {
+ node = container.childNodes[offset];
+
+ // Offset is after the containers last child
+ // then use the previous child for normalization
+ if (!node) {
+ node = container.childNodes[offset - 1];
+ }
+
+ if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') &&
+ !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) {
+ findTextNodeRelative(true, node);
+ }
+ }
+ }
+
+ // Lean the start of the selection right if possible
+ // So this: x[<b>x]</b>
+ // Becomes: x<b>[x]</b>
+ if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) {
+ findTextNodeRelative(false);
+ }
+
+ // Set endpoint if it was normalized
+ if (normalized) {
+ rng['set' + (start ? 'Start' : 'End')](container, offset);
+ }
+ }
+
+ collapsed = rng.collapsed;
+
+ normalizeEndPoint(true);
+
+ if (!collapsed) {
+ normalizeEndPoint();
+ }
+
+ // If it was collapsed then make sure it still is
+ if (normalized && collapsed) {
+ rng.collapse(true);
+ }
+
+ return normalized;
+ };
+ }
+
+ /**
+ * Compares two ranges and checks if they are equal.
+ *
+ * @static
+ * @method compareRanges
+ * @param {DOMRange} rng1 First range to compare.
+ * @param {DOMRange} rng2 First range to compare.
+ * @return {Boolean} true/false if the ranges are equal.
+ */
+ RangeUtils.compareRanges = function(rng1, rng2) {
+ if (rng1 && rng2) {
+ // Compare native IE ranges
+ if (rng1.item || rng1.duplicate) {
+ // Both are control ranges and the selected element matches
+ if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) {
+ return true;
+ }
+
+ // Both are text ranges and the range matches
+ if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) {
+ return true;
+ }
+ } else {
+ // Compare w3c ranges
+ return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
+ }
+ }
+
+ return false;
+ };
+
+ return RangeUtils;
+});
+
+// Included from: js/tinymce/classes/NodeChange.js
+
+/**
+ * NodeChange.js
+ *
+ * Copyright, Moxiecode Systems AB
+ * Released under LGPL License.
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * This class handles the nodechange event dispatching both manual and though selection change events.
+ *
+ * @class tinymce.NodeChange
+ * @private
+ */
+define("tinymce/NodeChange", [
+ "tinymce/dom/RangeUtils"
+], function(RangeUtils) {
+ return function(editor) {
+ var lastRng, lastPath = [];
+
+ /**
+ * Returns true/false if the current element path has been changed or not.
+ *
+ * @private
+ * @return {Boolean} True if the element path is the same false if it's not.
+ */
+ function isSameElementPath(startElm) {
+ var i, currentPath;
+
+ currentPath = editor.$(startElm).parentsUntil(editor.getBody()).add(startElm);
+ if (currentPath.length === lastPath.length) {
+ for (i = currentPath.length; i >= 0; i--) {
+ if (currentPath[i] !== lastPath[i]) {
+ break;
+ }
+ }
+
+ if (i === -1) {
+ lastPath = currentPath;
+ return true;
+ }
+ }
+
+ lastPath = currentPath;
+
+ return false;
+ }
+
+ // Gecko doesn't support the "selectionchange" event
+ if (!('onselectionchange' in editor.getDoc())) {
+ editor.on('NodeChange Click MouseUp KeyUp', function(e) {
+ var nativeRng, fakeRng;
+
+ // Since DOM Ranges mutate on modification
+ // of the DOM we need to clone it's contents
+ nativeRng = editor.selection.getRng();
+ fakeRng = {
+ startContainer: nativeRng.startContainer,
+ startOffset: nativeRng.startOffset,
+ endContainer: nativeRng.endContainer,
+ endOffset: nativeRng.endOffset
+ };
+
+ // Always treat nodechange as a selectionchange since applying
+ // formatting to the current range wouldn't update the range but it's parent
+ if (e.type == 'nodechange' || !RangeUtils.compareRanges(fakeRng, lastRng)) {
+ editor.fire('SelectionChange');
+ }
+
+ lastRng = fakeRng;
+ });
+ }
+
+ // IE has a bug where it fires a selectionchange on right click that has a range at the start of the body
+ // When the contextmenu event fires the selection is located at the right location
+ editor.on('contextmenu', function() {
+ editor.fire('SelectionChange');
+ });
+
+ editor.on('SelectionChange', function() {
+ var startElm = editor.selection.getStart();
+
+ // Selection change might fire when focus is lost so check if the start is still within the body
+ if (!isSameElementPath(startElm) && editor.dom.isChildOf(startElm, editor.getBody())) {
+ editor.nodeChanged({selectionChange: true});
+ }
+ });
+
+ /**
+ * Distpaches out a onNodeChange event to all observers. This method should be called when you
+ * need to update the UI states or element path etc.
+ *
+ * @method nodeChanged
+ * @param {Object} args Optional args to pass to NodeChange event handlers.
+ */
+ this.nodeChanged = function(args) {
+ var selection = editor.selection, node, parents, root;
+
+ // Fix for bug #1896577 it seems that this can not be fired while the editor is loading
+ if (editor.initialized && !editor.settings.disable_nodechange && !editor.settings.readonly) {
+ // Get start node
+ root = editor.getBody();
+ node = selection.getStart() || root;
+ node = node.ownerDocument != editor.getDoc() ? editor.getBody() : node;
+
+ // Edge case for <p>|<img></p>
+ if (node.nodeName == 'IMG' && selection.isCollapsed()) {
+ node = node.parentNode;
+ }
+
+ // Get parents and add them to object
+ parents = [];
+ editor.dom.getParent(node, function(node) {
+ if (node === root) {
+ return true;
+ }
+
+ parents.push(node);
+ });
+
+ args = args || {};
+ args.element = node;
+ args.parents = parents;
+
+ editor.fire('NodeChange', args);
+ }
+ };
+ };
+});
+
// Included from: js/tinymce/classes/html/Node.js
/**
* Node.js
*
@@ -7905,30 +8455,30 @@
return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
}
// Parses the specified valid_elements string and adds to the current rules
// This function is a bit hard to read since it's heavily optimized for speed
- function addValidElements(valid_elements) {
+ function addValidElements(validElements) {
var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
prefix, outputName, globalAttributes, globalAttributesOrder, key, value,
elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/,
attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
hasPatternsRegExp = /[*?+]/;
- if (valid_elements) {
+ if (validElements) {
// Split valid elements into an array with rules
- valid_elements = split(valid_elements, ',');
+ validElements = split(validElements, ',');
if (elements['@']) {
globalAttributes = elements['@'].attributes;
globalAttributesOrder = elements['@'].attributesOrder;
}
// Loop all rules
- for (ei = 0, el = valid_elements.length; ei < el; ei++) {
+ for (ei = 0, el = validElements.length; ei < el; ei++) {
// Parse element rule
- matches = elementRuleRegExp.exec(valid_elements[ei]);
+ matches = elementRuleRegExp.exec(validElements[ei]);
if (matches) {
// Setup local names for matches
prefix = matches[1];
elementName = matches[2];
outputName = matches[3];
@@ -8054,30 +8604,30 @@
}
}
}
}
- function setValidElements(valid_elements) {
+ function setValidElements(validElements) {
elements = {};
patternElements = [];
- addValidElements(valid_elements);
+ addValidElements(validElements);
each(schemaItems, function(element, name) {
children[name] = element.children;
});
}
// Adds custom non HTML elements to the schema
- function addCustomElements(custom_elements) {
+ function addCustomElements(customElements) {
var customElementRegExp = /^(~)?(.+)$/;
- if (custom_elements) {
+ if (customElements) {
// Flush cached items since we are altering the default maps
mapCache.text_block_elements = mapCache.block_elements = null;
- each(split(custom_elements, ','), function(rule) {
+ each(split(customElements, ','), function(rule) {
var matches = customElementRegExp.exec(rule),
inline = matches[1] === '~',
cloneName = inline ? 'span' : 'div',
name = matches[2];
@@ -8111,15 +8661,15 @@
});
}
}
// Adds valid children to the schema object
- function addValidChildren(valid_children) {
+ function addValidChildren(validChildren) {
var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;
- if (valid_children) {
- each(split(valid_children, ','), function(rule) {
+ if (validChildren) {
+ each(split(validChildren, ','), function(rule) {
var matches = childRuleRegExp.exec(rule), parent, prefix;
if (matches) {
prefix = matches[1];
@@ -8560,11 +9110,11 @@
*/
function findEndTag(schema, html, startIndex) {
var count = 1, index, matches, tokenRegExp, shortEndedElements;
shortEndedElements = schema.getShortEndedElements();
- tokenRegExp = /<([!?\/])?([A-Za-z0-9\-\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g;
+ tokenRegExp = /<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g;
tokenRegExp.lastIndex = index = startIndex;
while ((matches = tokenRegExp.exec(html))) {
index = tokenRegExp.lastIndex;
@@ -8728,11 +9278,11 @@
'(?:!--([\\w\\W]*?)-->)|' + // Comment
'(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
'(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
'(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
'(?:\\/([^>]+)>)|' + // End element
- '(?:([A-Za-z0-9\\-\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element
+ '(?:([A-Za-z0-9\\-_\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element
')', 'g');
attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g;
// Setup lookup tables for empty elements and boolean attributes
@@ -11336,11 +11886,15 @@
startW = selectedElm.clientWidth;
startH = selectedElm.clientHeight;
ratio = startH / startW;
selectedHandle = handle;
- handle.startPos = dom.getPos(handle.elm, rootElement);
+ handle.startPos = {
+ x: targetWidth * handle[0] + selectedElmX,
+ y: targetHeight * handle[1] + selectedElmY
+ };
+
startScrollWidth = rootElement.scrollWidth;
startScrollHeight = rootElement.scrollHeight;
selectedElmGhost = selectedElm.cloneNode(true);
dom.addClass(selectedElmGhost, 'mce-clonedresizable');
@@ -11459,11 +12013,11 @@
each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function(img) {
img.removeAttribute('data-mce-selected');
});
controlElm = e.type == 'mousedown' ? e.target : selection.getNode();
- controlElm = dom.getParent(controlElm, isIE ? 'table' : 'table,img,hr');
+ controlElm = dom.$(controlElm).closest(isIE ? 'table' : 'table,img,hr')[0];
if (isChildOrEqual(controlElm, rootElement)) {
disableGeckoResize();
if (isChildOrEqual(selection.getStart(), controlElm) && isChildOrEqual(selection.getEnd(), controlElm)) {
@@ -11509,18 +12063,24 @@
}
}
// Remove native selection and let the magic begin
resizeStarted = true;
+ editor.fire('ObjectResizeStart', {
+ target: selectedElm,
+ width: selectedElm.clientWidth,
+ height: selectedElm.clientHeight
+ });
editor.getDoc().selection.empty();
showResizeRect(target, name, lastMouseDownEvent);
}
function nativeControlSelect(e) {
var target = e.srcElement;
if (target != selectedElm) {
+ editor.fire('ObjectSelected', {target: target});
detachResizeStartListener();
if (target.id.indexOf('mceResizeHandle') === 0) {
e.returnValue = false;
return;
@@ -11597,11 +12157,11 @@
if (Env.ie >= 11) {
// TODO: Drag/drop doesn't work
editor.on('mouseup', function(e) {
var nodeName = e.target.nodeName;
- if (/^(TABLE|IMG|HR)$/.test(nodeName)) {
+ if (!resizeStarted && /^(TABLE|IMG|HR)$/.test(nodeName)) {
editor.selection.select(e.target, nodeName == 'TABLE');
editor.nodeChanged();
}
});
@@ -11656,899 +12216,402 @@
destroy: destroy
};
};
});
-// Included from: js/tinymce/classes/dom/RangeUtils.js
+// Included from: js/tinymce/classes/dom/BookmarkManager.js
/**
- * RangeUtils.js
+ * BookmarkManager.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
- * This class contains a few utility methods for ranges.
+ * This class handles selection bookmarks.
*
- * @class tinymce.dom.RangeUtils
- * @private
+ * @class tinymce.dom.BookmarkManager
*/
-define("tinymce/dom/RangeUtils", [
- "tinymce/util/Tools",
- "tinymce/dom/TreeWalker"
-], function(Tools, TreeWalker) {
- var each = Tools.each;
+define("tinymce/dom/BookmarkManager", [
+ "tinymce/Env",
+ "tinymce/util/Tools"
+], function(Env, Tools) {
+ /**
+ * Constructs a new BookmarkManager instance for a specific selection instance.
+ *
+ * @constructor
+ * @method BookmarkManager
+ * @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for.
+ */
+ function BookmarkManager(selection) {
+ var dom = selection.dom;
- function getEndChild(container, index) {
- var childNodes = container.childNodes;
-
- index--;
-
- if (index > childNodes.length - 1) {
- index = childNodes.length - 1;
- } else if (index < 0) {
- index = 0;
- }
-
- return childNodes[index] || container;
- }
-
- function RangeUtils(dom) {
/**
- * Walks the specified range like object and executes the callback for each sibling collection it finds.
+ * Returns a bookmark location for the current selection. This bookmark object
+ * can then be used to restore the selection after some content modification to the document.
*
- * @method walk
- * @param {Object} rng Range like object.
- * @param {function} callback Callback function to execute for each sibling collection.
+ * @method getBookmark
+ * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
+ * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
+ * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
+ * @example
+ * // Stores a bookmark of the current selection
+ * var bm = tinymce.activeEditor.selection.getBookmark();
+ *
+ * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
+ *
+ * // Restore the selection bookmark
+ * tinymce.activeEditor.selection.moveToBookmark(bm);
*/
- this.walk = function(rng, callback) {
- var startContainer = rng.startContainer,
- startOffset = rng.startOffset,
- endContainer = rng.endContainer,
- endOffset = rng.endOffset,
- ancestor, startPoint,
- endPoint, node, parent, siblings, nodes;
+ this.getBookmark = function(type, normalized) {
+ var rng, rng2, id, collapsed, name, element, chr = '', styles;
- // Handle table cell selection the table plugin enables
- // you to fake select table cells and perform formatting actions on them
- nodes = dom.select('td.mce-item-selected,th.mce-item-selected');
- if (nodes.length > 0) {
- each(nodes, function(node) {
- callback([node]);
+ function findIndex(name, element) {
+ var index = 0;
+
+ Tools.each(dom.select(name), function(node, i) {
+ if (node == element) {
+ index = i;
+ }
});
- return;
+ return index;
}
- /**
- * Excludes start/end text node if they are out side the range
- *
- * @private
- * @param {Array} nodes Nodes to exclude items from.
- * @return {Array} Array with nodes excluding the start/end container if needed.
- */
- function exclude(nodes) {
- var node;
+ function normalizeTableCellSelection(rng) {
+ function moveEndPoint(start) {
+ var container, offset, childNodes, prefix = start ? 'start' : 'end';
- // First node is excluded
- node = nodes[0];
- if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
- nodes.splice(0, 1);
- }
+ container = rng[prefix + 'Container'];
+ offset = rng[prefix + 'Offset'];
- // Last node is excluded
- node = nodes[nodes.length - 1];
- if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
- nodes.splice(nodes.length - 1, 1);
+ if (container.nodeType == 1 && container.nodeName == "TR") {
+ childNodes = container.childNodes;
+ container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)];
+ if (container) {
+ offset = start ? 0 : container.childNodes.length;
+ rng['set' + (start ? 'Start' : 'End')](container, offset);
+ }
+ }
}
- return nodes;
- }
+ moveEndPoint(true);
+ moveEndPoint();
- /**
- * Collects siblings
- *
- * @private
- * @param {Node} node Node to collect siblings from.
- * @param {String} name Name of the sibling to check for.
- * @return {Array} Array of collected siblings.
- */
- function collectSiblings(node, name, end_node) {
- var siblings = [];
-
- for (; node && node != end_node; node = node[name]) {
- siblings.push(node);
- }
-
- return siblings;
+ return rng;
}
- /**
- * Find an end point this is the node just before the common ancestor root.
- *
- * @private
- * @param {Node} node Node to start at.
- * @param {Node} root Root/ancestor element to stop just before.
- * @return {Node} Node just before the root element.
- */
- function findEndPoint(node, root) {
- do {
- if (node.parentNode == root) {
- return node;
- }
+ function getLocation() {
+ var rng = selection.getRng(true), root = dom.getRoot(), bookmark = {};
- node = node.parentNode;
- } while (node);
- }
+ function getPoint(rng, start) {
+ var container = rng[start ? 'startContainer' : 'endContainer'],
+ offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;
- function walkBoundary(start_node, end_node, next) {
- var siblingName = next ? 'nextSibling' : 'previousSibling';
+ if (container.nodeType == 3) {
+ if (normalized) {
+ for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) {
+ offset += node.nodeValue.length;
+ }
+ }
- for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
- parent = node.parentNode;
- siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
+ point.push(offset);
+ } else {
+ childNodes = container.childNodes;
- if (siblings.length) {
- if (!next) {
- siblings.reverse();
+ if (offset >= childNodes.length && childNodes.length) {
+ after = 1;
+ offset = Math.max(0, childNodes.length - 1);
}
- callback(exclude(siblings));
+ point.push(dom.nodeIndex(childNodes[offset], normalized) + after);
}
- }
- }
- // If index based start position then resolve it
- if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
- startContainer = startContainer.childNodes[startOffset];
- }
+ for (; container && container != root; container = container.parentNode) {
+ point.push(dom.nodeIndex(container, normalized));
+ }
- // If index based end position then resolve it
- if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
- endContainer = getEndChild(endContainer, endOffset);
- }
+ return point;
+ }
- // Same container
- if (startContainer == endContainer) {
- return callback(exclude([startContainer]));
- }
+ bookmark.start = getPoint(rng, true);
- // Find common ancestor and end points
- ancestor = dom.findCommonAncestor(startContainer, endContainer);
-
- // Process left side
- for (node = startContainer; node; node = node.parentNode) {
- if (node === endContainer) {
- return walkBoundary(startContainer, ancestor, true);
+ if (!selection.isCollapsed()) {
+ bookmark.end = getPoint(rng);
}
- if (node === ancestor) {
- break;
- }
+ return bookmark;
}
- // Process right side
- for (node = endContainer; node; node = node.parentNode) {
- if (node === startContainer) {
- return walkBoundary(endContainer, ancestor);
+ if (type == 2) {
+ element = selection.getNode();
+ name = element ? element.nodeName : null;
+
+ if (name == 'IMG') {
+ return {name: name, index: findIndex(name, element)};
}
- if (node === ancestor) {
- break;
+ if (selection.tridentSel) {
+ return selection.tridentSel.getBookmark(type);
}
+
+ return getLocation();
}
- // Find start/end point
- startPoint = findEndPoint(startContainer, ancestor) || startContainer;
- endPoint = findEndPoint(endContainer, ancestor) || endContainer;
-
- // Walk left leaf
- walkBoundary(startContainer, startPoint, true);
-
- // Walk the middle from start to end point
- siblings = collectSiblings(
- startPoint == startContainer ? startPoint : startPoint.nextSibling,
- 'nextSibling',
- endPoint == endContainer ? endPoint.nextSibling : endPoint
- );
-
- if (siblings.length) {
- callback(exclude(siblings));
+ // Handle simple range
+ if (type) {
+ return {rng: selection.getRng()};
}
- // Walk right leaf
- walkBoundary(endContainer, endPoint);
- };
+ rng = selection.getRng();
+ id = dom.uniqueId();
+ collapsed = selection.isCollapsed();
+ styles = 'overflow:hidden;line-height:0px';
- /**
- * Splits the specified range at it's start/end points.
- *
- * @private
- * @param {Range/RangeObject} rng Range to split.
- * @return {Object} Range position object.
- */
- this.split = function(rng) {
- var startContainer = rng.startContainer,
- startOffset = rng.startOffset,
- endContainer = rng.endContainer,
- endOffset = rng.endOffset;
+ // Explorer method
+ if (rng.duplicate || rng.item) {
+ // Text selection
+ if (!rng.item) {
+ rng2 = rng.duplicate();
- function splitText(node, offset) {
- return node.splitText(offset);
- }
+ try {
+ // Insert start marker
+ rng.collapse();
+ rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
- // Handle single text node
- if (startContainer == endContainer && startContainer.nodeType == 3) {
- if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
- endContainer = splitText(startContainer, startOffset);
- startContainer = endContainer.previousSibling;
+ // Insert end marker
+ if (!collapsed) {
+ rng2.collapse(false);
- if (endOffset > startOffset) {
- endOffset = endOffset - startOffset;
- startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
- endOffset = endContainer.nodeValue.length;
- startOffset = 0;
- } else {
- endOffset = 0;
+ // Detect the empty space after block elements in IE and move the
+ // end back one character <p></p>] becomes <p>]</p>
+ rng.moveToElementText(rng2.parentElement());
+ if (rng.compareEndPoints('StartToEnd', rng2) === 0) {
+ rng2.move('character', -1);
+ }
+
+ rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
+ }
+ } catch (ex) {
+ // IE might throw unspecified error so lets ignore it
+ return null;
}
+ } else {
+ // Control selection
+ element = rng.item(0);
+ name = element.nodeName;
+
+ return {name: name, index: findIndex(name, element)};
}
} else {
- // Split startContainer text node if needed
- if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
- startContainer = splitText(startContainer, startOffset);
- startOffset = 0;
+ element = selection.getNode();
+ name = element.nodeName;
+ if (name == 'IMG') {
+ return {name: name, index: findIndex(name, element)};
}
- // Split endContainer text node if needed
- if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
- endContainer = splitText(endContainer, endOffset).previousSibling;
- endOffset = endContainer.nodeValue.length;
+ // W3C method
+ rng2 = normalizeTableCellSelection(rng.cloneRange());
+
+ // Insert end marker
+ if (!collapsed) {
+ rng2.collapse(false);
+ rng2.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_end', style: styles}, chr));
}
+
+ rng = normalizeTableCellSelection(rng);
+ rng.collapse(true);
+ rng.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_start', style: styles}, chr));
}
- return {
- startContainer: startContainer,
- startOffset: startOffset,
- endContainer: endContainer,
- endOffset: endOffset
- };
+ selection.moveToBookmark({id: id, keep: 1});
+
+ return {id: id};
};
/**
- * Normalizes the specified range by finding the closest best suitable caret location.
+ * Restores the selection to the specified bookmark.
*
- * @private
- * @param {Range} rng Range to normalize.
- * @return {Boolean} True/false if the specified range was normalized or not.
+ * @method moveToBookmark
+ * @param {Object} bookmark Bookmark to restore selection from.
+ * @return {Boolean} true/false if it was successful or not.
+ * @example
+ * // Stores a bookmark of the current selection
+ * var bm = tinymce.activeEditor.selection.getBookmark();
+ *
+ * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
+ *
+ * // Restore the selection bookmark
+ * tinymce.activeEditor.selection.moveToBookmark(bm);
*/
- this.normalize = function(rng) {
- var normalized, collapsed;
+ this.moveToBookmark = function(bookmark) {
+ var rng, root, startContainer, endContainer, startOffset, endOffset;
- function normalizeEndPoint(start) {
- var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap;
- var directionLeft, isAfterNode;
+ function setEndPoint(start) {
+ var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;
- function hasBrBeforeAfter(node, left) {
- var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body);
+ if (point) {
+ offset = point[0];
- while ((node = walker[left ? 'prev' : 'next']())) {
- if (node.nodeName === "BR") {
- return true;
- }
- }
- }
+ // Find container node
+ for (node = root, i = point.length - 1; i >= 1; i--) {
+ children = node.childNodes;
- function isPrevNode(node, name) {
- return node.previousSibling && node.previousSibling.nodeName == name;
- }
-
- // Walks the dom left/right to find a suitable text node to move the endpoint into
- // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
- function findTextNodeRelative(left, startNode) {
- var walker, lastInlineElement, parentBlockContainer;
-
- startNode = startNode || container;
- parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body;
-
- // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
- // This: <p><br>|</p> becomes <p>|<br></p>
- if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) {
- container = startNode.parentNode;
- offset = dom.nodeIndex(startNode);
- normalized = true;
- return;
- }
-
- // Walk left until we hit a text node we can move to or a block/br/img
- walker = new TreeWalker(startNode, parentBlockContainer);
- while ((node = walker[left ? 'prev' : 'next']())) {
- // Break if we hit a non content editable node
- if (dom.getContentEditableParent(node) === "false") {
+ if (point[i] > children.length - 1) {
return;
}
- // Found text node that has a length
- if (node.nodeType === 3 && node.nodeValue.length > 0) {
- container = node;
- offset = left ? node.nodeValue.length : 0;
- normalized = true;
- return;
- }
+ node = children[point[i]];
+ }
- // Break if we find a block or a BR/IMG/INPUT etc
- if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
- return;
- }
+ // Move text offset to best suitable location
+ if (node.nodeType === 3) {
+ offset = Math.min(point[0], node.nodeValue.length);
+ }
- lastInlineElement = node;
+ // Move element offset to best suitable location
+ if (node.nodeType === 1) {
+ offset = Math.min(point[0], node.childNodes.length);
}
- // Only fetch the last inline element when in caret mode for now
- if (collapsed && lastInlineElement) {
- container = lastInlineElement;
- normalized = true;
- offset = 0;
+ // Set offset within container node
+ if (start) {
+ rng.setStart(node, offset);
+ } else {
+ rng.setEnd(node, offset);
}
}
- container = rng[(start ? 'start' : 'end') + 'Container'];
- offset = rng[(start ? 'start' : 'end') + 'Offset'];
- isAfterNode = container.nodeType == 1 && offset === container.childNodes.length;
- nonEmptyElementsMap = dom.schema.getNonEmptyElements();
- directionLeft = start;
+ return true;
+ }
- if (container.nodeType == 1 && offset > container.childNodes.length - 1) {
- directionLeft = false;
- }
+ function restoreEndPoint(suffix) {
+ var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;
- // If the container is a document move it to the body element
- if (container.nodeType === 9) {
- container = dom.getRoot();
- offset = 0;
- }
+ if (marker) {
+ node = marker.parentNode;
- // If the container is body try move it into the closest text node or position
- if (container === body) {
- // If start is before/after a image, table etc
- if (directionLeft) {
- node = container.childNodes[offset > 0 ? offset - 1 : 0];
- if (node) {
- if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") {
- return;
- }
+ if (suffix == 'start') {
+ if (!keep) {
+ idx = dom.nodeIndex(marker);
+ } else {
+ node = marker.firstChild;
+ idx = 1;
}
- }
- // Resolve the index
- if (container.hasChildNodes()) {
- offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
- container = container.childNodes[offset];
- offset = 0;
+ startContainer = endContainer = node;
+ startOffset = endOffset = idx;
+ } else {
+ if (!keep) {
+ idx = dom.nodeIndex(marker);
+ } else {
+ node = marker.firstChild;
+ idx = 1;
+ }
- // Don't walk into elements that doesn't have any child nodes like a IMG
- if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) {
- // Walk the DOM to find a text node to place the caret at or a BR
- node = container;
- walker = new TreeWalker(container, body);
+ endContainer = node;
+ endOffset = idx;
+ }
- do {
- // Found a text node use that position
- if (node.nodeType === 3 && node.nodeValue.length > 0) {
- offset = directionLeft ? 0 : node.nodeValue.length;
- container = node;
- normalized = true;
- break;
- }
+ if (!keep) {
+ prev = marker.previousSibling;
+ next = marker.nextSibling;
- // Found a BR/IMG element that we can place the caret before
- if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
- offset = dom.nodeIndex(node);
- container = node.parentNode;
+ // Remove all marker text nodes
+ Tools.each(Tools.grep(marker.childNodes), function(node) {
+ if (node.nodeType == 3) {
+ node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
+ }
+ });
- // Put caret after image when moving the end point
- if (node.nodeName == "IMG" && !directionLeft) {
- offset++;
- }
-
- normalized = true;
- break;
- }
- } while ((node = (directionLeft ? walker.next() : walker.prev())));
+ // Remove marker but keep children if for example contents where inserted into the marker
+ // Also remove duplicated instances of the marker for example by a
+ // split operation or by WebKit auto split on paste feature
+ while ((marker = dom.get(bookmark.id + '_' + suffix))) {
+ dom.remove(marker, 1);
}
- }
- }
- // Lean the caret to the left if possible
- if (collapsed) {
- // So this: <b>x</b><i>|x</i>
- // Becomes: <b>x|</b><i>x</i>
- // Seems that only gecko has issues with this
- if (container.nodeType === 3 && offset === 0) {
- findTextNodeRelative(true);
- }
+ // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
+ // and we are sniffing since adding a lot of detection code for a browser with 3% of the market
+ // isn't worth the effort. Sorry, Opera but it's just a fact
+ if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) {
+ idx = prev.nodeValue.length;
+ prev.appendData(next.nodeValue);
+ dom.remove(next);
- // Lean left into empty inline elements when the caret is before a BR
- // So this: <i><b></b><i>|<br></i>
- // Becomes: <i><b>|</b><i><br></i>
- // Seems that only gecko has issues with this.
- // Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p>
- if (container.nodeType === 1) {
- node = container.childNodes[offset];
-
- // Offset is after the containers last child
- // then use the previous child for normalization
- if (!node) {
- node = container.childNodes[offset - 1];
+ if (suffix == 'start') {
+ startContainer = endContainer = prev;
+ startOffset = endOffset = idx;
+ } else {
+ endContainer = prev;
+ endOffset = idx;
+ }
}
-
- if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') &&
- !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) {
- findTextNodeRelative(true, node);
- }
}
}
+ }
- // Lean the start of the selection right if possible
- // So this: x[<b>x]</b>
- // Becomes: x<b>[x]</b>
- if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) {
- findTextNodeRelative(false);
+ function addBogus(node) {
+ // Adds a bogus BR element for empty block elements
+ if (dom.isBlock(node) && !node.innerHTML && !Env.ie) {
+ node.innerHTML = '<br data-mce-bogus="1" />';
}
- // Set endpoint if it was normalized
- if (normalized) {
- rng['set' + (start ? 'Start' : 'End')](container, offset);
- }
+ return node;
}
- collapsed = rng.collapsed;
+ if (bookmark) {
+ if (bookmark.start) {
+ rng = dom.createRng();
+ root = dom.getRoot();
- normalizeEndPoint(true);
+ if (selection.tridentSel) {
+ return selection.tridentSel.moveToBookmark(bookmark);
+ }
- if (!collapsed) {
- normalizeEndPoint();
- }
+ if (setEndPoint(true) && setEndPoint()) {
+ selection.setRng(rng);
+ }
+ } else if (bookmark.id) {
+ // Restore start/end points
+ restoreEndPoint('start');
+ restoreEndPoint('end');
- // If it was collapsed then make sure it still is
- if (normalized && collapsed) {
- rng.collapse(true);
+ if (startContainer) {
+ rng = dom.createRng();
+ rng.setStart(addBogus(startContainer), startOffset);
+ rng.setEnd(addBogus(endContainer), endOffset);
+ selection.setRng(rng);
+ }
+ } else if (bookmark.name) {
+ selection.select(dom.select(bookmark.name)[bookmark.index]);
+ } else if (bookmark.rng) {
+ selection.setRng(bookmark.rng);
+ }
}
-
- return normalized;
};
}
/**
- * Compares two ranges and checks if they are equal.
+ * Returns true/false if the specified node is a bookmark node or not.
*
* @static
- * @method compareRanges
- * @param {DOMRange} rng1 First range to compare.
- * @param {DOMRange} rng2 First range to compare.
- * @return {Boolean} true/false if the ranges are equal.
+ * @method isBookmarkNode
+ * @param {DOMNode} node DOM Node to check if it's a bookmark node or not.
+ * @return {Boolean} true/false if the node is a bookmark node or not.
*/
- RangeUtils.compareRanges = function(rng1, rng2) {
- if (rng1 && rng2) {
- // Compare native IE ranges
- if (rng1.item || rng1.duplicate) {
- // Both are control ranges and the selected element matches
- if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) {
- return true;
- }
-
- // Both are text ranges and the range matches
- if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) {
- return true;
- }
- } else {
- // Compare w3c ranges
- return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
- }
- }
-
- return false;
+ BookmarkManager.isBookmarkNode = function(node) {
+ return node && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark';
};
- return RangeUtils;
+ return BookmarkManager;
});
-// Included from: js/tinymce/classes/dom/BookmarkManager.js
-
-/**
- * BookmarkManager.js
- *
- * Copyright, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
-
-/**
- * This class handles selection bookmarks.
- *
- * @class tinymce.dom.BookmarkManager
- */
-define("tinymce/dom/BookmarkManager", [
- "tinymce/Env",
- "tinymce/util/Tools"
-], function(Env, Tools) {
- /**
- * Constructs a new BookmarkManager instance for a specific selection instance.
- *
- * @constructor
- * @method BookmarkManager
- * @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for.
- */
- function BookmarkManager(selection) {
- var dom = selection.dom;
-
- /**
- * Returns a bookmark location for the current selection. This bookmark object
- * can then be used to restore the selection after some content modification to the document.
- *
- * @method getBookmark
- * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
- * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
- * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
- * @example
- * // Stores a bookmark of the current selection
- * var bm = tinymce.activeEditor.selection.getBookmark();
- *
- * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
- *
- * // Restore the selection bookmark
- * tinymce.activeEditor.selection.moveToBookmark(bm);
- */
- this.getBookmark = function(type, normalized) {
- var rng, rng2, id, collapsed, name, element, chr = '', styles;
-
- function findIndex(name, element) {
- var index = 0;
-
- Tools.each(dom.select(name), function(node, i) {
- if (node == element) {
- index = i;
- }
- });
-
- return index;
- }
-
- function normalizeTableCellSelection(rng) {
- function moveEndPoint(start) {
- var container, offset, childNodes, prefix = start ? 'start' : 'end';
-
- container = rng[prefix + 'Container'];
- offset = rng[prefix + 'Offset'];
-
- if (container.nodeType == 1 && container.nodeName == "TR") {
- childNodes = container.childNodes;
- container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)];
- if (container) {
- offset = start ? 0 : container.childNodes.length;
- rng['set' + (start ? 'Start' : 'End')](container, offset);
- }
- }
- }
-
- moveEndPoint(true);
- moveEndPoint();
-
- return rng;
- }
-
- function getLocation() {
- var rng = selection.getRng(true), root = dom.getRoot(), bookmark = {};
-
- function getPoint(rng, start) {
- var container = rng[start ? 'startContainer' : 'endContainer'],
- offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;
-
- if (container.nodeType == 3) {
- if (normalized) {
- for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling) {
- offset += node.nodeValue.length;
- }
- }
-
- point.push(offset);
- } else {
- childNodes = container.childNodes;
-
- if (offset >= childNodes.length && childNodes.length) {
- after = 1;
- offset = Math.max(0, childNodes.length - 1);
- }
-
- point.push(dom.nodeIndex(childNodes[offset], normalized) + after);
- }
-
- for (; container && container != root; container = container.parentNode) {
- point.push(dom.nodeIndex(container, normalized));
- }
-
- return point;
- }
-
- bookmark.start = getPoint(rng, true);
-
- if (!selection.isCollapsed()) {
- bookmark.end = getPoint(rng);
- }
-
- return bookmark;
- }
-
- if (type == 2) {
- element = selection.getNode();
- name = element ? element.nodeName : null;
-
- if (name == 'IMG') {
- return {name: name, index: findIndex(name, element)};
- }
-
- if (selection.tridentSel) {
- return selection.tridentSel.getBookmark(type);
- }
-
- return getLocation();
- }
-
- // Handle simple range
- if (type) {
- return {rng: selection.getRng()};
- }
-
- rng = selection.getRng();
- id = dom.uniqueId();
- collapsed = selection.isCollapsed();
- styles = 'overflow:hidden;line-height:0px';
-
- // Explorer method
- if (rng.duplicate || rng.item) {
- // Text selection
- if (!rng.item) {
- rng2 = rng.duplicate();
-
- try {
- // Insert start marker
- rng.collapse();
- rng.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');
-
- // Insert end marker
- if (!collapsed) {
- rng2.collapse(false);
-
- // Detect the empty space after block elements in IE and move the
- // end back one character <p></p>] becomes <p>]</p>
- rng.moveToElementText(rng2.parentElement());
- if (rng.compareEndPoints('StartToEnd', rng2) === 0) {
- rng2.move('character', -1);
- }
-
- rng2.pasteHTML('<span data-mce-type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
- }
- } catch (ex) {
- // IE might throw unspecified error so lets ignore it
- return null;
- }
- } else {
- // Control selection
- element = rng.item(0);
- name = element.nodeName;
-
- return {name: name, index: findIndex(name, element)};
- }
- } else {
- element = selection.getNode();
- name = element.nodeName;
- if (name == 'IMG') {
- return {name: name, index: findIndex(name, element)};
- }
-
- // W3C method
- rng2 = normalizeTableCellSelection(rng.cloneRange());
-
- // Insert end marker
- if (!collapsed) {
- rng2.collapse(false);
- rng2.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_end', style: styles}, chr));
- }
-
- rng = normalizeTableCellSelection(rng);
- rng.collapse(true);
- rng.insertNode(dom.create('span', {'data-mce-type': "bookmark", id: id + '_start', style: styles}, chr));
- }
-
- selection.moveToBookmark({id: id, keep: 1});
-
- return {id: id};
- };
-
- /**
- * Restores the selection to the specified bookmark.
- *
- * @method moveToBookmark
- * @param {Object} bookmark Bookmark to restore selection from.
- * @return {Boolean} true/false if it was successful or not.
- * @example
- * // Stores a bookmark of the current selection
- * var bm = tinymce.activeEditor.selection.getBookmark();
- *
- * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content');
- *
- * // Restore the selection bookmark
- * tinymce.activeEditor.selection.moveToBookmark(bm);
- */
- this.moveToBookmark = function(bookmark) {
- var rng, root, startContainer, endContainer, startOffset, endOffset;
-
- function setEndPoint(start) {
- var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;
-
- if (point) {
- offset = point[0];
-
- // Find container node
- for (node = root, i = point.length - 1; i >= 1; i--) {
- children = node.childNodes;
-
- if (point[i] > children.length - 1) {
- return;
- }
-
- node = children[point[i]];
- }
-
- // Move text offset to best suitable location
- if (node.nodeType === 3) {
- offset = Math.min(point[0], node.nodeValue.length);
- }
-
- // Move element offset to best suitable location
- if (node.nodeType === 1) {
- offset = Math.min(point[0], node.childNodes.length);
- }
-
- // Set offset within container node
- if (start) {
- rng.setStart(node, offset);
- } else {
- rng.setEnd(node, offset);
- }
- }
-
- return true;
- }
-
- function restoreEndPoint(suffix) {
- var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;
-
- if (marker) {
- node = marker.parentNode;
-
- if (suffix == 'start') {
- if (!keep) {
- idx = dom.nodeIndex(marker);
- } else {
- node = marker.firstChild;
- idx = 1;
- }
-
- startContainer = endContainer = node;
- startOffset = endOffset = idx;
- } else {
- if (!keep) {
- idx = dom.nodeIndex(marker);
- } else {
- node = marker.firstChild;
- idx = 1;
- }
-
- endContainer = node;
- endOffset = idx;
- }
-
- if (!keep) {
- prev = marker.previousSibling;
- next = marker.nextSibling;
-
- // Remove all marker text nodes
- Tools.each(Tools.grep(marker.childNodes), function(node) {
- if (node.nodeType == 3) {
- node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
- }
- });
-
- // Remove marker but keep children if for example contents where inserted into the marker
- // Also remove duplicated instances of the marker for example by a
- // split operation or by WebKit auto split on paste feature
- while ((marker = dom.get(bookmark.id + '_' + suffix))) {
- dom.remove(marker, 1);
- }
-
- // If siblings are text nodes then merge them unless it's Opera since it some how removes the node
- // and we are sniffing since adding a lot of detection code for a browser with 3% of the market
- // isn't worth the effort. Sorry, Opera but it's just a fact
- if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) {
- idx = prev.nodeValue.length;
- prev.appendData(next.nodeValue);
- dom.remove(next);
-
- if (suffix == 'start') {
- startContainer = endContainer = prev;
- startOffset = endOffset = idx;
- } else {
- endContainer = prev;
- endOffset = idx;
- }
- }
- }
- }
- }
-
- function addBogus(node) {
- // Adds a bogus BR element for empty block elements
- if (dom.isBlock(node) && !node.innerHTML && !Env.ie) {
- node.innerHTML = '<br data-mce-bogus="1" />';
- }
-
- return node;
- }
-
- if (bookmark) {
- if (bookmark.start) {
- rng = dom.createRng();
- root = dom.getRoot();
-
- if (selection.tridentSel) {
- return selection.tridentSel.moveToBookmark(bookmark);
- }
-
- if (setEndPoint(true) && setEndPoint()) {
- selection.setRng(rng);
- }
- } else if (bookmark.id) {
- // Restore start/end points
- restoreEndPoint('start');
- restoreEndPoint('end');
-
- if (startContainer) {
- rng = dom.createRng();
- rng.setStart(addBogus(startContainer), startOffset);
- rng.setEnd(addBogus(endContainer), endOffset);
- selection.setRng(rng);
- }
- } else if (bookmark.name) {
- selection.select(dom.select(bookmark.name)[bookmark.index]);
- } else if (bookmark.rng) {
- selection.setRng(bookmark.rng);
- }
- }
- };
- }
-
- /**
- * Returns true/false if the specified node is a bookmark node or not.
- *
- * @static
- * @method isBookmarkNode
- * @param {DOMNode} node DOM Node to check if it's a bookmark node or not.
- * @return {Boolean} true/false if the node is a bookmark node or not.
- */
- BookmarkManager.isBookmarkNode = function(node) {
- return node && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark';
- };
-
- return BookmarkManager;
-});
-
// Included from: js/tinymce/classes/dom/Selection.js
/**
* Selection.js
*
@@ -12979,23 +13042,23 @@
/**
* Collapse the selection to start or end of range.
*
* @method collapse
- * @param {Boolean} to_start Optional boolean state if to collapse to end or not. Defaults to start.
+ * @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to start.
*/
- collapse: function(to_start) {
+ collapse: function(toStart) {
var self = this, rng = self.getRng(), node;
// Control range on IE
if (rng.item) {
node = rng.item(0);
rng = self.win.document.body.createTextRange();
rng.moveToElementText(node);
}
- rng.collapse(!!to_start);
+ rng.collapse(!!toStart);
self.setRng(rng);
},
/**
* Returns the browsers internal selection object.
@@ -14136,66 +14199,10 @@
}
return rng;
}
- function applyStyleToList(node, bookmark, wrapElm, newWrappers, process) {
- var nodes = [], listIndex = -1, list, startIndex = -1, endIndex = -1, currentWrapElm;
-
- // find the index of the first child list.
- each(node.childNodes, function(n, index) {
- if (n.nodeName === "UL" || n.nodeName === "OL") {
- listIndex = index;
- list = n;
- return false;
- }
- });
-
- // get the index of the bookmarks
- each(node.childNodes, function(n, index) {
- if (isBookmarkNode(n)) {
- if (n.id == bookmark.id + "_start") {
- startIndex = index;
- } else if (n.id == bookmark.id + "_end") {
- endIndex = index;
- }
- }
- });
-
- // if the selection spans across an embedded list, or there isn't an embedded list - handle processing normally
- if (listIndex <= 0 || (startIndex < listIndex && endIndex > listIndex)) {
- each(grep(node.childNodes), process);
- return 0;
- } else {
- currentWrapElm = dom.clone(wrapElm, FALSE);
-
- // create a list of the nodes on the same side of the list as the selection
- each(grep(node.childNodes), function(n, index) {
- if ((startIndex < listIndex && index < listIndex) || (startIndex > listIndex && index > listIndex)) {
- nodes.push(n);
- n.parentNode.removeChild(n);
- }
- });
-
- // insert the wrapping element either before or after the list.
- if (startIndex < listIndex) {
- node.insertBefore(currentWrapElm, list);
- } else if (startIndex > listIndex) {
- node.insertBefore(currentWrapElm, list.nextSibling);
- }
-
- // add the new nodes to the list.
- newWrappers.push(currentWrapElm);
-
- each(nodes, function(node) {
- currentWrapElm.appendChild(node);
- });
-
- return currentWrapElm;
- }
- }
-
function applyRngStyle(rng, bookmark, node_specific) {
var newWrappers = [], wrapName, wrapElm, contentEditable = true;
// Setup wrapper element
wrapName = format.inline || format.block;
@@ -14288,14 +14295,10 @@
node.parentNode.insertBefore(currentWrapElm, node);
newWrappers.push(currentWrapElm);
}
currentWrapElm.appendChild(node);
- } else if (nodeName == 'li' && bookmark) {
- // Start wrapping - if we are in a list node and have a bookmark, then
- // we will always begin by wrapping in a new element.
- currentWrapElm = applyStyleToList(node, bookmark, wrapElm, newWrappers, process);
} else {
// Start a new wrapper for possible children
currentWrapElm = 0;
each(grep(node.childNodes), process);
@@ -14556,16 +14559,16 @@
});
return formatRoot;
}
- function wrapAndSplit(format_root, container, target, split) {
+ function wrapAndSplit(formatRoot, container, target, split) {
var parent, clone, lastClone, firstClone, i, formatRootParent;
// Format root found then clone formats and split it
- if (format_root) {
- formatRootParent = format_root.parentNode;
+ if (formatRoot) {
+ formatRootParent = formatRoot.parentNode;
for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) {
clone = dom.clone(parent, FALSE);
for (i = 0; i < formatList.length; i++) {
@@ -14588,12 +14591,12 @@
lastClone = clone;
}
}
// Never split block elements if the format is mixed
- if (split && (!format.mixed || !isBlock(format_root))) {
- container = dom.split(format_root, container);
+ if (split && (!format.mixed || !isBlock(formatRoot))) {
+ container = dom.split(formatRoot, container);
}
// Wrap container in cloned formats
if (lastClone) {
target.parentNode.insertBefore(lastClone, target);
@@ -14953,10 +14956,15 @@
currentFormats = {};
ed.on('NodeChange', function(e) {
var parents = getParents(e.element), matchedFormats = {};
+ // Ignore bogus nodes like the <a> tag created by moveStart()
+ parents = Tools.grep(parents, function(node) {
+ return !node.getAttribute('data-mce-bogus');
+ });
+
// Check for new formats
each(formatChangeData, function(callbacks, format) {
each(parents, function(node) {
if (matchNode(node, format, {}, callbacks.similar)) {
if (!currentFormats[format]) {
@@ -16137,11 +16145,11 @@
for (node = walker.current(); node; node = walker.next()) {
if (node.nodeType == 3 && !isWhiteSpaceNode(node)) {
// IE has a "neat" feature where it moves the start node into the closest element
// we can avoid this by inserting an element before it and then remove it after we set the selection
- tmpNode = dom.create('a', null, INVISIBLE_CHAR);
+ tmpNode = dom.create('a', {'data-mce-bogus': 'all'}, INVISIBLE_CHAR);
node.parentNode.insertBefore(tmpNode, node);
// Set selection and remove tmpNode
rng.setStart(node, 0);
selection.setRng(rng);
@@ -16323,42 +16331,10 @@
if (!e.isDefaultPrevented()) {
editor.nodeChanged();
}
});
- // Selection range isn't updated until after the click events default handler is executed
- // so we need to wait for the selection to update on Gecko/WebKit it happens right away.
- // On IE it might take a while so we listen for the SelectionChange event.
- //
- // We can't use the SelectionChange on all browsers event since Gecko doesn't support that.
- if (Env.ie) {
- editor.on('MouseUp', function(e) {
- if (!e.isDefaultPrevented()) {
- editor.once('SelectionChange', function() {
- // Selection change might fire when focus is lost
- if (editor.dom.isChildOf(editor.selection.getStart(), editor.getBody())) {
- editor.nodeChanged();
- }
- });
-
- editor.nodeChanged();
- }
- });
- } else {
- editor.on('MouseUp', function() {
- editor.nodeChanged();
- });
-
- editor.on('Click', function(e) {
- if (!e.isDefaultPrevented()) {
- setTimeout(function() {
- editor.nodeChanged();
- }, 0);
- }
- });
- }
-
self = {
// Explose for debugging reasons
data: data,
/**
@@ -21575,11 +21551,17 @@
* @constructor
*/
return function(settings) {
var root = settings.root, focusedElement, focusedControl;
- focusedElement = document.activeElement;
+ try {
+ focusedElement = document.activeElement;
+ } catch (ex) {
+ // IE sometimes fails to return a proper element
+ focusedElement = document.body;
+ }
+
focusedControl = root.getParentCtrl(focusedElement);
/**
* Returns the currently focused elements wai aria role of the currently
* focused element or specified element.
@@ -23951,48 +23933,45 @@
* @param {Object} settings Name/value object with settings.
*/
msgBox: function(settings) {
var buttons, callback = settings.callback || function() {};
+ function createButton(text, status, primary) {
+ return {
+ type: "button",
+ text: text,
+ subtype: primary ? 'primary' : '',
+ onClick: function(e) {
+ e.control.parents()[1].close();
+ callback(status);
+ }
+ };
+ }
+
switch (settings.buttons) {
case MessageBox.OK_CANCEL:
buttons = [
- {type: "button", text: "Ok", subtype: "primary", onClick: function(e) {
- e.control.parents()[1].close();
- callback(true);
- }},
-
- {type: "button", text: "Cancel", onClick: function(e) {
- e.control.parents()[1].close();
- callback(false);
- }}
+ createButton('Ok', true, true),
+ createButton('Cancel', false)
];
break;
case MessageBox.YES_NO:
- buttons = [
- {type: "button", text: "Ok", subtype: "primary", onClick: function(e) {
- e.control.parents()[1].close();
- callback(true);
- }}
- ];
- break;
-
case MessageBox.YES_NO_CANCEL:
buttons = [
- {type: "button", text: "Ok", subtype: "primary", onClick: function(e) {
- e.control.parents()[1].close();
- }}
+ createButton('Yes', 1, true),
+ createButton('No', 0)
];
+
+ if (settings.buttons == MessageBox.YES_NO_CANCEL) {
+ buttons.push(createButton('Cancel', -1));
+ }
break;
default:
buttons = [
- {type: "button", text: "Ok", subtype: "primary", onClick: function(e) {
- e.control.parents()[1].close();
- callback(true);
- }}
+ createButton('Ok', true, true)
];
break;
}
return new Window({
@@ -24800,12 +24779,10 @@
}
if (e.nodeName == 'A' && dom.hasClass(e, 'mce-item-anchor')) {
selection.select(e);
}
-
- editor.nodeChanged();
});
}
/**
* Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements.
@@ -25431,10 +25408,30 @@
e.preventDefault();
});
});
}
+ /**
+ * Sometimes WebKit/Blink generates BR elements with the Apple-interchange-newline class.
+ *
+ * Scenario:
+ * 1) Create a table 2x2.
+ * 2) Select and copy cells A2-B2.
+ * 3) Paste and it will add BR element to table cell.
+ */
+ function removeAppleInterchangeBrs() {
+ parser.addNodeFilter('br', function(nodes) {
+ var i = nodes.length;
+
+ while (i--) {
+ if (nodes[i].attr('class') == 'Apple-interchange-newline') {
+ nodes[i].remove();
+ }
+ }
+ });
+ }
+
// All browsers
removeBlockQuoteOnBackSpace();
emptyEditorWhenDeleting();
normalizeSelection();
@@ -25444,10 +25441,11 @@
inputMethodFocus();
selectControlElements();
setDefaultBlockType();
blockFormSubmitInsideEditor();
disableBackspaceIntoATable();
+ removeAppleInterchangeBrs();
// iOS
if (Env.iOS) {
selectionChangeNodeChanged();
restoreFocusOnKeyDown();
@@ -25923,10 +25921,11 @@
*/
define("tinymce/Editor", [
"tinymce/dom/DOMUtils",
"tinymce/dom/DomQuery",
"tinymce/AddOnManager",
+ "tinymce/NodeChange",
"tinymce/html/Node",
"tinymce/dom/Serializer",
"tinymce/html/Serializer",
"tinymce/dom/Selection",
"tinymce/Formatter",
@@ -25944,11 +25943,11 @@
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/EditorObservable",
"tinymce/Shortcuts"
], function(
- DOMUtils, DomQuery, AddOnManager, Node, DomSerializer, Serializer,
+ DOMUtils, DomQuery, AddOnManager, NodeChange, Node, DomSerializer, Serializer,
Selection, Formatter, UndoManager, EnterKey, ForceBlocks, EditorCommands,
URI, ScriptLoader, EventUtils, WindowManager,
Schema, DomParser, Quirks, Env, Tools, EditorObservable, Shortcuts
) {
// Shorten these names
@@ -26016,13 +26015,13 @@
render_ui: true,
indentation: '30px',
inline_styles: true,
convert_fonts_to_spans: true,
indent: 'simple',
- indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,' +
+ indent_before: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,ol,li,dl,dt,dd,area,table,thead,' +
'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist',
- indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,' +
+ indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,ol,li,dl,dt,dd,area,table,thead,' +
'tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist',
validate: true,
entity_encoding: 'named',
url_converter: self.convertURL,
url_converter_scope: self,
@@ -26123,10 +26122,14 @@
self.execCommands = {};
self.queryStateCommands = {};
self.queryValueCommands = {};
self.loadedCSS = {};
+ if (settings.target) {
+ self.targetElm = settings.target;
+ }
+
self.suffix = editorManager.suffix;
self.editorManager = editorManager;
self.inline = settings.inline;
// Call setup
@@ -26432,16 +26435,10 @@
deltaHeight: settings.delta_height
});
// Resize editor
if (!settings.content_editable) {
- DOM.setStyles(o.sizeContainer || o.editorContainer, {
- wi2dth: w,
- // TODO: Fix this
- h2eight: h
- });
-
h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : '');
if (h < minHeight) {
h = minHeight;
}
}
@@ -26515,12 +26512,13 @@
if (bodyClass.indexOf('=') != -1) {
bodyClass = self.getParam('body_class', '', 'hash');
bodyClass = bodyClass[self.id] || '';
}
- self.iframeHTML += '</head><body id="' + bodyId + '" class="mce-content-body ' + bodyClass + '" ' +
- 'onload="window.parent.tinymce.get(\'' + self.id + '\').fire(\'load\');"><br></body></html>';
+ self.iframeHTML += '</head><body id="' + bodyId +
+ '" class="mce-content-body ' + bodyClass +
+ '" data-id="' + self.id + '"><br></body></html>';
/*eslint no-script-url:0 */
var domainRelaxUrl = 'javascript:(function(){' +
'document.open();document.domain="' + document.domain + '";' +
'var ed = window.parent.tinymce.get("' + self.id + '");document.write(ed.iframeHTML);' +
@@ -26531,26 +26529,35 @@
url = domainRelaxUrl;
}
// Create iframe
// TODO: ACC add the appropriate description on this.
- n = DOM.add(o.iframeContainer, 'iframe', {
+ var ifr = DOM.create('iframe', {
id: self.id + "_ifr",
- src: url || 'javascript:""', // Workaround for HTTPS warning in IE6/7
+ //src: url || 'javascript:""', // Workaround for HTTPS warning in IE6/7
frameBorder: '0',
allowTransparency: "true",
title: self.editorManager.translate(
- "Rich Text Area. Press ALT-F9 for menu. " +
- "Press ALT-F10 for toolbar. Press ALT-0 for help"
+ "Rich Text Area. Press ALT-F9 for menu. " +
+ "Press ALT-F10 for toolbar. Press ALT-0 for help"
),
style: {
width: '100%',
height: h,
display: 'block' // Important for Gecko to render the iframe correctly
}
});
+ ifr.onload = function() {
+ ifr.onload = null;
+ self.fire("load");
+ };
+
+ DOM.setAttrib("src", url || 'javascript:""');
+
+ n = DOM.add(o.iframeContainer, ifr);
+
// Try accessing the document this will fail on IE when document.domain is set to the same as location.hostname
// Then we have to force domain relaxing using the domainRelaxUrl approach very ugly!!
if (ie) {
try {
self.getDoc();
@@ -26558,16 +26565,18 @@
n.src = url = domainRelaxUrl;
}
}
self.contentAreaContainer = o.iframeContainer;
+ self.iframeElement = ifr;
if (o.editorContainer) {
DOM.get(o.editorContainer).style.display = self.orgDisplay;
+ self.hidden = DOM.isHidden(o.editorContainer);
}
- DOM.get(self.id).style.display = 'none';
+ self.getElement().style.display = 'none';
DOM.setAttrib(self.id, 'aria-hidden', true);
if (!url) {
self.initContentBody();
}
@@ -26581,11 +26590,11 @@
*
* @method initContentBody
* @private
*/
initContentBody: function(skipWrite) {
- var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), body, contentCssText;
+ var self = this, settings = self.settings, targetElm = self.getElement(), doc = self.getDoc(), body, contentCssText;
// Restore visibility on target element
if (!settings.inline) {
self.getElement().style.visibility = self.orgVisibility;
}
@@ -26654,11 +26663,11 @@
url_converter: self.convertURL,
url_converter_scope: self,
hex_colors: settings.force_hex_style_colors,
class_filter: settings.class_filter,
update_styles: true,
- root_element: settings.content_editable ? self.id : null,
+ root_element: self.inline ? self.getBody() : null,
collect: settings.content_editable,
schema: self.schema,
onSetAttrib: function(e) {
self.fire('SetAttrib', e);
}
@@ -26728,11 +26737,11 @@
while (i--) {
node = nodes[i];
if (node.isEmpty(nonEmptyElements)) {
- node.empty().append(new Node('br', 1)).shortEnded = true;
+ node.append(new Node('br', 1)).shortEnded = true;
}
}
});
/**
@@ -26783,21 +26792,22 @@
self.undoManager = new UndoManager(self);
self.forceBlocks = new ForceBlocks(self);
self.enterKey = new EnterKey(self);
self.editorCommands = new EditorCommands(self);
+ self._nodeChangeDispatcher = new NodeChange(self);
self.fire('PreInit');
if (!settings.browser_spellcheck && !settings.gecko_spellcheck) {
doc.body.spellcheck = false; // Gecko
DOM.setAttrib(body, "spellcheck", "false");
}
self.fire('PostRender');
- self.quirks = Quirks(self);
+ self.quirks = new Quirks(self);
if (settings.directionality) {
body.dir = settings.directionality;
}
@@ -26885,17 +26895,17 @@
/**
* Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection
* it will also place DOM focus inside the editor.
*
* @method focus
- * @param {Boolean} skip_focus Skip DOM focus. Just set is as the active editor.
+ * @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor.
*/
- focus: function(skip_focus) {
+ focus: function(skipFocus) {
var oed, self = this, selection = self.selection, contentEditable = self.settings.content_editable, rng;
var controlElm, doc = self.getDoc(), body;
- if (!skip_focus) {
+ if (!skipFocus) {
// Get selected control element
rng = selection.getRng();
if (rng.item) {
controlElm = rng.item(0);
}
@@ -27068,40 +27078,11 @@
*
* @method nodeChanged
* @param {Object} args Optional args to pass to NodeChange event handlers.
*/
nodeChanged: function(args) {
- var self = this, selection = self.selection, node, parents, root;
-
- // Fix for bug #1896577 it seems that this can not be fired while the editor is loading
- if (self.initialized && !self.settings.disable_nodechange && !self.settings.readonly) {
- // Get start node
- root = self.getBody();
- node = selection.getStart() || root;
- node = ie && node.ownerDocument != self.getDoc() ? self.getBody() : node; // Fix for IE initial state
-
- // Edge case for <p>|<img></p>
- if (node.nodeName == 'IMG' && selection.isCollapsed()) {
- node = node.parentNode;
- }
-
- // Get parents and add them to object
- parents = [];
- self.dom.getParent(node, function(node) {
- if (node === root) {
- return true;
- }
-
- parents.push(node);
- });
-
- args = args || {};
- args.element = node;
- args.parents = parents;
-
- self.fire('NodeChange', args);
- }
+ this._nodeChangeDispatcher.nodeChanged(args);
},
/**
* Adds a button that later gets created by the theme in the editors toolbars.
*
@@ -27643,11 +27624,11 @@
} else if (!ie) {
// We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret
content = '<br data-mce-bogus="1">';
}
- body.innerHTML = content;
+ self.dom.setHTML(body, content);
self.fire('SetContent', args);
} else {
// Parse and serialize the html
if (args.format !== 'raw') {
@@ -27791,11 +27772,15 @@
*
* @method getElement
* @return {Element} HTML DOM element for the replaced element.
*/
getElement: function() {
- return DOM.get(this.settings.content_element || this.id);
+ if (!this.targetElm) {
+ this.targetElm = DOM.get(this.id);
+ }
+
+ return this.targetElm;
},
/**
* Returns the iframes window object.
*
@@ -27804,11 +27789,11 @@
*/
getWin: function() {
var self = this, elm;
if (!self.contentWindow) {
- elm = DOM.get(self.id + "_ifr");
+ elm = self.iframeElement;
if (elm) {
self.contentWindow = elm.contentWindow;
}
}
@@ -27902,31 +27887,27 @@
switch (elm.nodeName) {
case 'TABLE':
cls = settings.visual_table_class || 'mce-item-table';
value = dom.getAttrib(elm, 'border');
- if (!value || value == '0') {
- if (self.hasVisual) {
- dom.addClass(elm, cls);
- } else {
- dom.removeClass(elm, cls);
- }
+ if ((!value || value == '0') && self.hasVisual) {
+ dom.addClass(elm, cls);
+ } else {
+ dom.removeClass(elm, cls);
}
return;
case 'A':
if (!dom.getAttrib(elm, 'href', false)) {
value = dom.getAttrib(elm, 'name') || elm.id;
cls = settings.visual_anchor_class || 'mce-item-anchor';
- if (value) {
- if (self.hasVisual) {
- dom.addClass(elm, cls);
- } else {
- dom.removeClass(elm, cls);
- }
+ if (value && self.hasVisual) {
+ dom.addClass(elm, cls);
+ } else {
+ dom.removeClass(elm, cls);
}
}
return;
}
@@ -28033,11 +28014,12 @@
DOM.unbind(form, 'submit reset', self.formEventDelegate);
}
self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null;
- self.settings.content_element = self.bodyElement = self.contentDocument = self.contentWindow = null;
+ self.bodyElement = self.contentDocument = self.contentWindow = null;
+ self.iframeElement = self.targetElm = null;
if (self.selection) {
self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null;
}
@@ -28248,24 +28230,40 @@
var editor = e.editor;
editor.on('init', function() {
// Gecko/WebKit has ghost selections in iframes and IE only has one selection per browser tab
if (editor.inline || Env.ie) {
- // On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes
- editor.on('nodechange keyup', function() {
- var node = document.activeElement;
+ // Use the onbeforedeactivate event when available since it works better see #7023
+ if ("onbeforedeactivate" in document && Env.ie < 9) {
+ editor.dom.bind(editor.getBody(), 'beforedeactivate', function() {
+ try {
+ editor.lastRng = editor.selection.getRng();
+ } catch (ex) {
+ // IE throws "Unexcpected call to method or property access" some times so lets ignore it
+ }
+ });
+ } else {
+ // On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes
+ editor.on('nodechange mouseup keyup', function(e) {
+ var node = getActiveElement();
- // IE 11 reports active element as iframe not body of iframe
- if (node && node.id == editor.id + '_ifr') {
- node = editor.getBody();
- }
+ // Only act on manual nodechanges
+ if (e.type == 'nodechange' && e.selectionChange) {
+ return;
+ }
- if (editor.dom.isChildOf(node, editor.getBody())) {
- editor.lastRng = editor.selection.getRng();
- }
- });
+ // IE 11 reports active element as iframe not body of iframe
+ if (node && node.id == editor.id + '_ifr') {
+ node = editor.getBody();
+ }
+ if (editor.dom.isChildOf(node, editor.getBody())) {
+ editor.lastRng = editor.selection.getRng();
+ }
+ });
+ }
+
// Handles the issue with WebKit not retaining selection within inline document
// If the user releases the mouse out side the body since a mouse up event wont occur on the body
if (Env.webkit && !selectionChangeHandler) {
selectionChangeHandler = function() {
var activeEditor = editorManager.activeEditor;
@@ -28338,12 +28336,13 @@
if (!documentFocusInHandler) {
documentFocusInHandler = function(e) {
var activeEditor = editorManager.activeEditor;
if (activeEditor && e.target.ownerDocument == document) {
- // Check to make sure we have a valid selection
- if (activeEditor.selection) {
+ // Check to make sure we have a valid selection don't update the bookmark if it's
+ // a focusin to the body of the editor see #7025
+ if (activeEditor.selection && e.target != activeEditor.getBody()) {
activeEditor.selection.lastFocusBookmark = createBookmark(activeEditor.dom, activeEditor.lastRng);
}
// Fire a blur event if the element isn't a UI element
if (!isUIElement(e.target) && editorManager.focusedEditor == activeEditor) {
@@ -28503,19 +28502,19 @@
* Minor version of TinyMCE build.
*
* @property minorVersion
* @type String
*/
- minorVersion: '1.0',
+ minorVersion: '1.2',
/**
* Release date of TinyMCE build.
*
* @property releaseDate
* @type String
*/
- releaseDate: '2014-06-18',
+ releaseDate: '2014-07-15',
/**
* Collection of editor instances.
*
* @property editors
@@ -28671,55 +28670,58 @@
}
return id;
}
- function createEditor(id, settings) {
+ function createEditor(id, settings, targetElm) {
if (!purgeDestroyedEditor(self.get(id))) {
var editor = new Editor(id, settings, self);
+ editor.targetElm = editor.targetElm || targetElm;
editors.push(editor);
editor.render();
}
}
- function execCallback(se, n, s) {
- var f = se[n];
+ function execCallback(name) {
+ var callback = settings[name];
- if (!f) {
+ if (!callback) {
return;
}
- return f.apply(s || this, Array.prototype.slice.call(arguments, 2));
+ return callback.apply(self, Array.prototype.slice.call(arguments, 2));
}
- function hasClass(n, c) {
- return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c);
+ function hasClass(elm, className) {
+ return className.constructor === RegExp ? className.test(elm.className) : DOM.hasClass(elm, className);
}
function readyHandler() {
var l, co;
DOM.unbind(window, 'ready', readyHandler);
- execCallback(settings, 'onpageload');
+ execCallback('onpageload');
if (settings.types) {
// Process type specific selector
each(settings.types, function(type) {
each(DOM.select(type.selector), function(elm) {
- createEditor(createId(elm), extend({}, settings, type));
+ createEditor(createId(elm), extend({}, settings, type), elm);
});
});
return;
} else if (settings.selector) {
// Process global selector
each(DOM.select(settings.selector), function(elm) {
- createEditor(createId(elm), settings);
+ createEditor(createId(elm), settings, elm);
});
return;
+ } else if (settings.target) {
+ createEditor(createId(settings.target), settings);
}
// Fallback to old setting
switch (settings.mode) {
case "exact":
@@ -28735,11 +28737,11 @@
each(document.forms, function(f) {
each(f.elements, function(e) {
if (e.name === v) {
v = 'mce_editor_' + instanceCounter++;
DOM.setAttrib(e, 'id', v);
- createEditor(v, settings);
+ createEditor(v, settings, e);
}
});
});
}
});
@@ -28752,11 +28754,11 @@
if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) {
return;
}
if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) {
- createEditor(createId(elm), settings);
+ createEditor(createId(elm), settings, elm);
}
});
break;
}
@@ -28772,20 +28774,20 @@
ed.on('init', function() {
l++;
// All done
if (l == co) {
- execCallback(settings, 'oninit');
+ execCallback('oninit');
}
});
} else {
l++;
}
// All done
if (l == co) {
- execCallback(settings, 'oninit');
+ execCallback('oninit');
}
});
}
}
@@ -28901,11 +28903,15 @@
// Remove editors by selector
if (typeof(selector) == "string") {
selector = selector.selector || selector;
each(DOM.select(selector), function(elm) {
- self.remove(editors[elm.id]);
+ editor = editors[elm.id];
+
+ if (editor) {
+ self.remove(editor);
+ }
});
return;
}
@@ -31812,41 +31818,35 @@
return false;
}
self.on('select', function(e) {
- var parents = [], node, body = editor.getBody();
-
editor.focus();
-
- node = editor.selection.getStart();
- while (node && node != body) {
- if (!isHidden(node)) {
- parents.push(node);
- }
-
- node = node.parentNode;
- }
-
- editor.selection.select(parents[parents.length - 1 - e.index]);
+ editor.selection.select(this.data()[e.index].element);
editor.nodeChanged();
});
editor.on('nodeChange', function(e) {
- var parents = [], selectionParents = e.parents, i = selectionParents.length;
+ var outParents = [], parents = e.parents, i = parents.length;
while (i--) {
- if (selectionParents[i].nodeType == 1 && !isHidden(selectionParents[i])) {
+ if (parents[i].nodeType == 1 && !isHidden(parents[i])) {
var args = editor.fire('ResolveName', {
- name: selectionParents[i].nodeName.toLowerCase(),
- target: selectionParents[i]
+ name: parents[i].nodeName.toLowerCase(),
+ target: parents[i]
});
- parents.push({name: args.name});
+ if (!args.isDefaultPrevented()) {
+ outParents.push({name: args.name, element: parents[i]});
+ }
+
+ if (args.isPropagationStopped()) {
+ break;
+ }
}
}
- self.data(parents);
+ self.data(outParents);
});
return self._super();
}
});
\ No newline at end of file