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 &aring;. @@ -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 = '&#xFEFF;', 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 = '&#xFEFF;', 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