app/assets/source/tinymce/tinymce.jquery.js in tinymce-rails-4.0.26 vs app/assets/source/tinymce/tinymce.jquery.js in tinymce-rails-4.0.28

- old
+ new

@@ -1,6 +1,6 @@ -// 4.0.26 (2014-05-06) +// 4.0.28 (2014-05-27) /** * Compiled inline version. (Library mode) */ @@ -144,14 +144,19 @@ /*eslint max-len:0 */ var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, trimRightRegExp = /\s+$/, - undef, i, encodingLookup = {}, encodingItems, invisibleChar = '\uFEFF'; + undef, i, encodingLookup = {}, encodingItems, validStyles, invalidStyles, invisibleChar = '\uFEFF'; settings = settings || {}; + if (schema) { + validStyles = schema.getValidStyles(); + invalidStyles = schema.getInvalidStyles(); + } + encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' '); for (i = 0; i < encodingItems.length; i++) { encodingLookup[encodingItems[i]] = invisibleChar + i; encodingLookup[invisibleChar + i] = encodingItems[i]; } @@ -405,20 +410,20 @@ /** * Serializes the specified style object into a string. * * @method serialize * @param {Object} styles Object to serialize as string for example: {border: '1px solid red'} - * @param {String} element_name Optional element name, if specified only the styles that matches the schema will be serialized. + * @param {String} elementName Optional element name, if specified only the styles that matches the schema will be serialized. * @return {String} String representation of the style object for example: border: 1px solid red. */ - serialize: function(styles, element_name) { + serialize: function(styles, elementName) { var css = '', name, value; function serializeStyles(name) { var styleList, i, l, value; - styleList = schema.styles[name]; + styleList = validStyles[name]; if (styleList) { for (i = 0, l = styleList.length; i < l; i++) { name = styleList[i]; value = styles[name]; @@ -427,22 +432,40 @@ } } } } + function isValid(name, elementName) { + var styleMap; + + styleMap = invalidStyles['*']; + if (styleMap && styleMap[name]) { + return false; + } + + styleMap = invalidStyles[elementName]; + if (styleMap && styleMap[name]) { + return false; + } + + return true; + } + // Serialize styles according to schema - if (element_name && schema && schema.styles) { + if (elementName && validStyles) { // Serialize global styles and element specific styles serializeStyles('*'); - serializeStyles(element_name); + serializeStyles(elementName); } else { // Output the styles in the order they are inside the object for (name in styles) { value = styles[name]; if (value !== undef && value.length > 0) { - css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; + if (!invalidStyles || isValid(name, elementName)) { + css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; + } } } } return css; @@ -464,10 +487,15 @@ */ /*jshint loopfunc:true*/ /*eslint no-loop-func:0 */ +/** + * This class wraps the browsers native event logic with more convenient methods. + * + * @class tinymce.dom.EventUtils + */ define("tinymce/dom/EventUtils", [], function() { "use strict"; var eventExpandoPrefix = "mce-data-"; var mouseEventRe = /^(?:mouse|contextmenu)|click/; @@ -3272,11 +3300,13 @@ // Wrap node name as func if (is(selector, 'string')) { selectorVal = selector; if (selector === '*') { - selector = function(node) {return node.nodeType == 1;}; + selector = function(node) { + return node.nodeType == 1; + }; } else { selector = function(node) { return self.is(node, selectorVal); }; } @@ -3589,11 +3619,11 @@ name = name.replace(/-(\D)/g, function(a, b) { return b.toUpperCase(); }); // Default px suffix on these - if (typeof(value) === 'number' && !numericCssMap[name]) { + 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") { @@ -6350,11 +6380,12 @@ // Extend with HTML4 attributes unless it's html5-strict if (type != "html5-strict") { addAttrs("script", "language xml:space"); addAttrs("style", "xml:space"); - addAttrs("object", "declare classid codebase codetype archive standby align border hspace vspace"); + addAttrs("object", "declare classid code codebase codetype archive standby align border hspace vspace"); + addAttrs("embed", "align name hspace vspace"); addAttrs("param", "valuetype type"); addAttrs("a", "charset name rev shape coords"); addAttrs("br", "clear"); addAttrs("applet", "codebase archive code object alt name width height align hspace vspace"); addAttrs("img", "name longdesc align border hspace vspace"); @@ -6419,21 +6450,43 @@ mapCache[type] = schema; return schema; } + function compileElementMap(value, mode) { + var styles; + + if (value) { + styles = {}; + + if (typeof value == 'string') { + value = { + '*': value + }; + } + + // Convert styles into a rule list + each(value, function(value, key) { + styles[key] = mode == 'map' ? makeMap(value, /[, ]/) : explode(value, /[, ]/); + }); + } + + return styles; + } + /** * Constructs a new Schema instance. * * @constructor * @method Schema * @param {Object} settings Name/value settings object. */ return function(settings) { - var self = this, elements = {}, children = {}, patternElements = [], validStyles, schemaItems; - var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap; - var blockElementsMap, nonEmptyElementsMap, textBlockElementsMap, customElementsMap = {}, specialElements = {}; + var self = this, elements = {}, children = {}, patternElements = [], validStyles, invalidStyles, schemaItems; + var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, validClasses; + var blockElementsMap, nonEmptyElementsMap, textBlockElementsMap, textInlineElementsMap; + var customElementsMap = {}, specialElements = {}; // Creates an lookup table map object for the specified option or the default value function createLookupTable(option, default_value, extendWith) { var value = settings[option]; @@ -6461,20 +6514,14 @@ // Allow all elements and attributes if verify_html is set to false if (settings.verify_html === false) { settings.valid_elements = '*[*]'; } - // Build styles list - if (settings.valid_styles) { - validStyles = {}; + validStyles = compileElementMap(settings.valid_styles); + invalidStyles = compileElementMap(settings.invalid_styles, 'map'); + validClasses = compileElementMap(settings.valid_classes, 'map'); - // Convert styles into a rule list - each(settings.valid_styles, function(value, key) { - validStyles[key] = explode(value); - }); - } - // Setup map objects whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea video audio iframe object'); selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' + 'meta param embed source wbr track'); @@ -6484,10 +6531,12 @@ textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' + 'blockquote center dir fieldset header footer article section hgroup aside nav figure'); blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' + 'th tr td li ol ul caption dl dt dd noscript menu isindex option ' + 'datalist select optgroup', textBlockElementsMap); + textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font strike u var cite ' + + 'dfn code mark q sup sub samp'); each((settings.special || 'script noscript style textarea').split(' '), function(name) { specialElements[name] = new RegExp('<\/' + name + '[^>]*>','gi'); }); @@ -6840,16 +6889,38 @@ self.children = children; /** * Name/value map object with valid styles for each element. * - * @field styles + * @method getValidStyles * @type Object */ - self.styles = validStyles; + self.getValidStyles = function() { + return validStyles; + }; /** + * Name/value map object with valid styles for each element. + * + * @method getInvalidStyles + * @type Object + */ + self.getInvalidStyles = function() { + return invalidStyles; + }; + + /** + * Name/value map object with valid classes for each element. + * + * @method getValidClasses + * @type Object + */ + self.getValidClasses = function() { + return validClasses; + }; + + /** * Returns a map with boolean attributes. * * @method getBoolAttrs * @return {Object} Name/value lookup map for boolean attributes. */ @@ -6876,10 +6947,20 @@ self.getTextBlockElements = function() { return textBlockElementsMap; }; /** + * Returns a map of inline text format nodes for example strong/span or ins. + * + * @method getTextInlineElements + * @return {Object} Name/value lookup map for text format elements. + */ + self.getTextInlineElements = function() { + return textInlineElementsMap; + }; + + /** * Returns a map with short ended elements such as BR or IMG. * * @method getShortEndedElements * @return {Object} Name/value lookup map for short ended elements. */ @@ -7105,10 +7186,40 @@ "tinymce/util/Tools" ], function(Schema, Entities, Tools) { var each = Tools.each; /** + * Returns the index of the end tag for a specific start tag. This can be + * used to skip all children of a parent element from being processed. + */ + function skipUntilEndTag(schema, html, startIndex) { + var count = 1, matches, tokenRegExp, shortEndedElements; + + shortEndedElements = schema.getShortEndedElements(); + tokenRegExp = /<([!?\/])?([A-Za-z0-9\-\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g; + tokenRegExp.lastIndex = startIndex; + + while ((matches = tokenRegExp.exec(html))) { + if (matches[1] === '/') { // End element + count--; + } else if (!matches[1]) { // Start element + if (matches[2] in shortEndedElements) { + continue; + } + + count++; + } + + if (count === 0) { + break; + } + } + + return tokenRegExp.lastIndex; + } + + /** * Constructs a new SaxParser instance. * * @constructor * @method SaxParser * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks. @@ -7385,11 +7496,17 @@ isValidElement = false; } } // Invalidate element if it's marked as bogus - if (attrList.map['data-mce-bogus']) { + if ((attr = attrList.map['data-mce-bogus'])) { + if (attr === 'all') { + index = skipUntilEndTag(schema, html, tokenRegExp.lastIndex); + tokenRegExp.lastIndex = index; + continue; + } + isValidElement = false; } } if (isValidElement) { @@ -8048,11 +8165,11 @@ node.empty().append(new Node('#text', '3')).value = '\u00a0'; } else { // Leave nodes that have a name like <a name="name"> if (!node.attributes.map.name && !node.attributes.map.id) { tempNode = node.parent; - node.empty().remove(); + node.unwrap(); node = tempNode; return; } } } @@ -8227,10 +8344,52 @@ } while (sibling); } } }); } + + if (settings.validate && schema.getValidClasses()) { + self.addAttributeFilter('class', function(nodes) { + var i = nodes.length, node, classList, ci, className, classValue; + var validClasses = schema.getValidClasses(), validClassesMap, valid; + + while (i--) { + node = nodes[i]; + classList = node.attr('class').split(' '); + classValue = ''; + + for (ci = 0; ci < classList.length; ci++) { + className = classList[ci]; + valid = false; + + validClassesMap = validClasses['*']; + if (validClassesMap && validClassesMap[className]) { + valid = true; + } + + validClassesMap = validClasses[node.name]; + if (!valid && validClassesMap && !validClassesMap[className]) { + valid = true; + } + + if (valid) { + if (classValue) { + classValue += ' '; + } + + classValue += className; + } + } + + if (!classValue.length) { + classValue = null; + } + + node.attr('class', classValue); + } + }); + } }; }); // Included from: js/tinymce/classes/html/Writer.js @@ -8709,19 +8868,10 @@ node.remove(); } } }); - // Remove expando attributes - htmlParser.addAttributeFilter('data-mce-expando', function(nodes, name) { - var i = nodes.length; - - while (i--) { - nodes[i].attr(name, null); - } - }); - htmlParser.addNodeFilter('noscript', function(nodes) { var i = nodes.length, node; while (i--) { node = nodes[i].firstChild; @@ -8732,11 +8882,11 @@ } }); // Force script into CDATA sections and remove the mce- prefix also add comments around styles htmlParser.addNodeFilter('script,style', function(nodes, name) { - var i = nodes.length, node, value; + var i = nodes.length, node, value, type; function trim(value) { /*jshint maxlen:255 */ /*eslint max-len:0 */ return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n') @@ -8748,13 +8898,16 @@ while (i--) { node = nodes[i]; value = node.firstChild ? node.firstChild.value : ''; if (name === "script") { - // Remove mce- prefix from script elements and remove default text/javascript mime type (HTML5) - var type = (node.attr('type') || 'text/javascript').replace(/^mce\-/, ''); - node.attr('type', type === 'text/javascript' ? null : type); + // Remove mce- prefix from script elements and remove default type since the user specified + // a script element without type attribute + type = node.attr('type'); + if (type) { + node.attr('type', type == 'mce-no/type' ? null : type.replace(/^mce\-/, '')); + } if (value.length > 0) { node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>'; } } else { @@ -8817,17 +8970,23 @@ } }); } // Remove internal data attributes - htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style,data-mce-selected', function(nodes, name) { - var i = nodes.length; + htmlParser.addAttributeFilter( + 'data-mce-src,data-mce-href,data-mce-style,' + + 'data-mce-selected,data-mce-expando,' + + 'data-mce-type,data-mce-resize', - while (i--) { - nodes[i].attr(name, null); + function(nodes, name) { + var i = nodes.length; + + while (i--) { + nodes[i].attr(name, null); + } } - }); + ); // Return public methods return { /** * Schema instance that was used to when the Serializer was constructed. @@ -9159,19 +9318,21 @@ return; } // Find the text node and offset while (sibling) { - nodeValue = sibling.nodeValue; - textNodeOffset += nodeValue.length; + if (sibling.nodeType == 3) { + nodeValue = sibling.nodeValue; + textNodeOffset += nodeValue.length; - // We are at or passed the position we where looking for - if (textNodeOffset >= offset) { - container = sibling; - textNodeOffset -= offset; - textNodeOffset = nodeValue.length - textNodeOffset; - break; + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + textNodeOffset = nodeValue.length - textNodeOffset; + break; + } } sibling = sibling.nextSibling; } } else { @@ -9192,17 +9353,19 @@ return; } while (sibling) { - textNodeOffset += sibling.nodeValue.length; + if (sibling.nodeType == 3) { + textNodeOffset += sibling.nodeValue.length; - // We are at or passed the position we where looking for - if (textNodeOffset >= offset) { - container = sibling; - textNodeOffset -= offset; - break; + // We are at or passed the position we where looking for + if (textNodeOffset >= offset) { + container = sibling; + textNodeOffset -= offset; + break; + } } sibling = sibling.previousSibling; } } @@ -9531,12 +9694,12 @@ modifierPressed: function(e) { return e.shiftKey || e.ctrlKey || e.altKey; }, metaKeyPressed: function(e) { - // Check if ctrl or meta key is pressed also check if alt is false for Polish users - return (Env.mac ? e.metaKey : e.ctrlKey) && !e.altKey; + // Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states + return (Env.mac ? e.metaKey : e.ctrlKey && !e.altKey); } }; }); // Included from: js/tinymce/classes/dom/ControlSelection.js @@ -10064,31 +10227,45 @@ }); // Included from: js/tinymce/classes/dom/RangeUtils.js /** - * Range.js + * RangeUtils.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /** - * RangeUtils + * 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 @@ -10197,11 +10374,11 @@ startContainer = startContainer.childNodes[startOffset]; } // If index based end position then resolve it if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { - endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)]; + endContainer = getEndChild(endContainer, endOffset); } // Same container if (startContainer == endContainer) { return callback(exclude([startContainer])); @@ -10544,10 +10721,402 @@ }; return RangeUtils; }); +// 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 * @@ -10570,15 +11139,16 @@ define("tinymce/dom/Selection", [ "tinymce/dom/TreeWalker", "tinymce/dom/TridentSelection", "tinymce/dom/ControlSelection", "tinymce/dom/RangeUtils", + "tinymce/dom/BookmarkManager", "tinymce/Env", "tinymce/util/Tools" -], function(TreeWalker, TridentSelection, ControlSelection, RangeUtils, Env, Tools) { - var each = Tools.each, grep = Tools.grep, trim = Tools.trim; - var isIE = Env.ie, isOpera = Env.opera; +], function(TreeWalker, TridentSelection, ControlSelection, RangeUtils, BookmarkManager, Env, Tools) { + var each = Tools.each, trim = Tools.trim; + var isIE = Env.ie; /** * Constructs a new selection instance. * * @constructor @@ -10592,11 +11162,11 @@ self.dom = dom; self.win = win; self.serializer = serializer; self.editor = editor; - + self.bookmarkManager = new BookmarkManager(self); self.controlSelection = new ControlSelection(self, editor); // No W3C Range support if (!self.win.getSelection) { self.tridentSel = new TridentSelection(self); @@ -10892,173 +11462,11 @@ * * // Restore the selection bookmark * tinymce.activeEditor.selection.moveToBookmark(bm); */ getBookmark: function(type, normalized) { - var self = this, dom = self.dom, rng, rng2, id, collapsed, name, element, chr = '&#xFEFF;', styles; - - function findIndex(name, element) { - var index = 0; - - 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 = self.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(self.dom.nodeIndex(childNodes[offset], normalized) + after); - } - - for (; container && container != root; container = container.parentNode) { - point.push(self.dom.nodeIndex(container, normalized)); - } - - return point; - } - - bookmark.start = getPoint(rng, true); - - if (!self.isCollapsed()) { - bookmark.end = getPoint(rng); - } - - return bookmark; - } - - if (type == 2) { - element = self.getNode(); - name = element ? element.nodeName : null; - - if (name == 'IMG') { - return {name: name, index: findIndex(name, element)}; - } - - if (self.tridentSel) { - return self.tridentSel.getBookmark(type); - } - - return getLocation(); - } - - // Handle simple range - if (type) { - return {rng: self.getRng()}; - } - - rng = self.getRng(); - id = dom.uniqueId(); - collapsed = self.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 = self.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)); - } - - self.moveToBookmark({id: id, keep: 1}); - - return {id: id}; + return this.bookmarkManager.getBookmark(type, normalized); }, /** * Restores the selection to the specified bookmark. * @@ -11073,154 +11481,11 @@ * * // Restore the selection bookmark * tinymce.activeEditor.selection.moveToBookmark(bm); */ moveToBookmark: function(bookmark) { - var self = this, dom = self.dom, 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 - each(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 && !isOpera) { - 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 && !isIE) { - node.innerHTML = '<br data-mce-bogus="1" />'; - } - - return node; - } - - if (bookmark) { - if (bookmark.start) { - rng = dom.createRng(); - root = dom.getRoot(); - - if (self.tridentSel) { - return self.tridentSel.moveToBookmark(bookmark); - } - - if (setEndPoint(true) && setEndPoint()) { - self.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); - self.setRng(rng); - } - } else if (bookmark.name) { - self.select(dom.select(bookmark.name)[bookmark.index]); - } else if (bookmark.rng) { - self.setRng(bookmark.rng); - } - } + return this.bookmarkManager.moveToBookmark(bookmark); }, /** * Selects the specified element. This will place the start and end of the selection range around the element. * @@ -11806,10 +12071,130 @@ }; return Selection; }); +// Included from: js/tinymce/classes/dom/ElementUtils.js + +/** + * ElementUtils.js + * + * Copyright, Moxiecode Systems AB + * Released under LGPL License. + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Utility class for various element specific functions. + * + * @private + */ +define("tinymce/dom/ElementUtils", [ + "tinymce/dom/BookmarkManager", + "tinymce/util/Tools" +], function(BookmarkManager, Tools) { + var each = Tools.each; + + function ElementUtils(dom) { + /** + * Compares two nodes and checks if it's attributes and styles matches. + * This doesn't compare classes as items since their order is significant. + * + * @method compare + * @param {Node} node1 First node to compare with. + * @param {Node} node2 Second node to compare with. + * @return {boolean} True/false if the nodes are the same or not. + */ + this.compare = function(node1, node2) { + // Not the same name + if (node1.nodeName != node2.nodeName) { + return false; + } + + /** + * Returns all the nodes attributes excluding internal ones, styles and classes. + * + * @private + * @param {Node} node Node to get attributes from. + * @return {Object} Name/value object with attributes and attribute values. + */ + function getAttribs(node) { + var attribs = {}; + + each(dom.getAttribs(node), function(attr) { + var name = attr.nodeName.toLowerCase(); + + // Don't compare internal attributes or style + if (name.indexOf('_') !== 0 && name !== 'style' && name !== 'data-mce-style') { + attribs[name] = dom.getAttrib(node, name); + } + }); + + return attribs; + } + + /** + * Compares two objects checks if it's key + value exists in the other one. + * + * @private + * @param {Object} obj1 First object to compare. + * @param {Object} obj2 Second object to compare. + * @return {boolean} True/false if the objects matches or not. + */ + function compareObjects(obj1, obj2) { + var value, name; + + for (name in obj1) { + // Obj1 has item obj2 doesn't have + if (obj1.hasOwnProperty(name)) { + value = obj2[name]; + + // Obj2 doesn't have obj1 item + if (typeof value == "undefined") { + return false; + } + + // Obj2 item has a different value + if (obj1[name] != value) { + return false; + } + + // Delete similar value + delete obj2[name]; + } + } + + // Check if obj 2 has something obj 1 doesn't have + for (name in obj2) { + // Obj2 has item obj1 doesn't have + if (obj2.hasOwnProperty(name)) { + return false; + } + } + + return true; + } + + // Attribs are not the same + if (!compareObjects(getAttribs(node1), getAttribs(node2))) { + return false; + } + + // Styles are not the same + if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) { + return false; + } + + return !BookmarkManager.isBookmarkNode(node1) && !BookmarkManager.isBookmarkNode(node2); + }; + } + + return ElementUtils; +}); + // Included from: js/tinymce/classes/fmt/Preview.js /** * Preview.js * @@ -11989,13 +12374,15 @@ * tinymce.activeEditor.formatter.apply('mycustomformat'); */ define("tinymce/Formatter", [ "tinymce/dom/TreeWalker", "tinymce/dom/RangeUtils", + "tinymce/dom/BookmarkManager", + "tinymce/dom/ElementUtils", "tinymce/util/Tools", "tinymce/fmt/Preview" -], function(TreeWalker, RangeUtils, Tools, Preview) { +], function(TreeWalker, RangeUtils, BookmarkManager, ElementUtils, Tools, Preview) { /** * Constructs a new formatter instance. * * @constructor Formatter * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to. @@ -12015,11 +12402,12 @@ TRUE = true, formatChangeData, undef, getContentEditable = dom.getContentEditable, disableCaretContainer, - markCaretContainersBogus; + markCaretContainersBogus, + isBookmarkNode = BookmarkManager.isBookmarkNode; var each = Tools.each, grep = Tools.grep, walk = Tools.walk, extend = Tools.extend; @@ -12040,11 +12428,10 @@ return node.nodeType === 1 && node.id === '_mce_caret'; } function defaultFormats() { register({ - valigntop: [ {selector: 'td,th', styles: {'verticalAlign': 'top'}} ], valignmiddle: [ @@ -12331,11 +12718,11 @@ } }); // get the index of the bookmarks each(node.childNodes, function(n, index) { - if (n.nodeName === "SPAN" && dom.getAttrib(n, "data-mce-type") == "bookmark") { + if (isBookmarkNode(n)) { if (n.id == bookmark.id + "_start") { startIndex = index; } else if (n.id == bookmark.id + "_end") { endIndex = index; } @@ -13908,123 +14295,20 @@ } } } /** - * Checks if the specified node is a bookmark node or not. - * - * @private - * @param {Node} node Node to check if it's a bookmark node or not. - * @return {Boolean} true/false if the node is a bookmark node. - */ - function isBookmarkNode(node) { - return node && node.nodeType == 1 && node.getAttribute('data-mce-type') == 'bookmark'; - } - - /** * Merges the next/previous sibling element if they match. * * @private * @param {Node} prev Previous node to compare/merge. * @param {Node} next Next node to compare/merge. * @return {Node} Next node if we didn't merge and prev node if we did. */ function mergeSiblings(prev, next) { - var sibling, tmpSibling; + var sibling, tmpSibling, elementUtils = new ElementUtils(dom); - /** - * Compares two nodes and checks if it's attributes and styles matches. - * This doesn't compare classes as items since their order is significant. - * - * @private - * @param {Node} node1 First node to compare with. - * @param {Node} node2 Second node to compare with. - * @return {boolean} True/false if the nodes are the same or not. - */ - function compareElements(node1, node2) { - // Not the same name - if (node1.nodeName != node2.nodeName) { - return FALSE; - } - - /** - * Returns all the nodes attributes excluding internal ones, styles and classes. - * - * @private - * @param {Node} node Node to get attributes from. - * @return {Object} Name/value object with attributes and attribute values. - */ - function getAttribs(node) { - var attribs = {}; - - each(dom.getAttribs(node), function(attr) { - var name = attr.nodeName.toLowerCase(); - - // Don't compare internal attributes or style - if (name.indexOf('_') !== 0 && name !== 'style' && name !== 'data-mce-style') { - attribs[name] = dom.getAttrib(node, name); - } - }); - - return attribs; - } - - /** - * Compares two objects checks if it's key + value exists in the other one. - * - * @private - * @param {Object} obj1 First object to compare. - * @param {Object} obj2 Second object to compare. - * @return {boolean} True/false if the objects matches or not. - */ - function compareObjects(obj1, obj2) { - var value, name; - - for (name in obj1) { - // Obj1 has item obj2 doesn't have - if (obj1.hasOwnProperty(name)) { - value = obj2[name]; - - // Obj2 doesn't have obj1 item - if (value === undef) { - return FALSE; - } - - // Obj2 item has a different value - if (obj1[name] != value) { - return FALSE; - } - - // Delete similar value - delete obj2[name]; - } - } - - // Check if obj 2 has something obj 1 doesn't have - for (name in obj2) { - // Obj2 has item obj1 doesn't have - if (obj2.hasOwnProperty(name)) { - return FALSE; - } - } - - return TRUE; - } - - // Attribs are not the same - if (!compareObjects(getAttribs(node1), getAttribs(node2))) { - return FALSE; - } - - // Styles are not the same - if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) { - return FALSE; - } - - return !isBookmarkNode(node1) && !isBookmarkNode(node2); - } - function findElementSibling(node, sibling_name) { for (sibling = node; sibling; sibling = sibling[sibling_name]) { if (sibling.nodeType == 3 && sibling.nodeValue.length !== 0) { return node; } @@ -14042,11 +14326,11 @@ // If previous sibling is empty then jump over it prev = findElementSibling(prev, 'previousSibling'); next = findElementSibling(next, 'nextSibling'); // Compare next and previous nodes - if (compareElements(prev, next)) { + if (elementUtils.compare(prev, next)) { // Append nodes between for (sibling = prev.nextSibling; sibling && sibling != next;) { tmpSibling = sibling; sibling = sibling.nextSibling; prev.appendChild(tmpSibling); @@ -14245,11 +14529,11 @@ container = rng.startContainer; offset = rng.startOffset; node = container; if (container.nodeType == 3) { - if (offset != container.nodeValue.length || container.nodeValue === INVISIBLE_CHAR) { + if (offset != container.nodeValue.length) { hasContentAfter = true; } node = node.parentNode; } @@ -14827,10 +15111,14 @@ // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p> function trimInlineElementsOnLeftSideOfBlock(block) { var node = block, firstChilds = [], i; + if (!node) { + return; + } + // Find inner most first child ex: <p><i><b>*</b></i></p> while ((node = node.firstChild)) { if (dom.isBlock(node)) { return; } @@ -14867,29 +15155,40 @@ node = node.nextSibling; } } + if (!root) { + return; + } + // Old IE versions doesn't properly render blocks with br elements in them // For example <p><br></p> wont be rendered correctly in a contentEditable area // until you remove the br producing <p></p> if (Env.ie && Env.ie < 9 && parentBlock && parentBlock.firstChild) { if (parentBlock.firstChild == parentBlock.lastChild && parentBlock.firstChild.tagName == 'BR') { dom.remove(parentBlock.firstChild); } } - if (root.nodeName == 'LI') { + if (/^(LI|DT|DD)$/.test(root.nodeName)) { var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild); - if (firstChild && /^(UL|OL)$/.test(firstChild.nodeName)) { + if (firstChild && /^(UL|OL|DL)$/.test(firstChild.nodeName)) { root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild); } } rng = dom.createRng(); + // Normalize whitespace to remove empty text nodes. Fix for: #6904 + // Gecko will be able to place the caret in empty text nodes but it won't render propery + // Older IE versions will sometimes crash so for now ignore all IE versions + if (!Env.ie) { + root.normalize(); + } + if (root.hasChildNodes()) { walker = new TreeWalker(root, root); while ((node = walker.current())) { if (node.nodeType == 3) { @@ -14949,11 +15248,11 @@ } // Creates a new block element by cloning the current one or creating a new one if the name is specified // This function will also copy any text formatting from the parent block and add it to the new one function createNewBlock(name) { - var node = container, block, clonedNode, caretNode; + var node = container, block, clonedNode, caretNode, textInlineElements = schema.getTextInlineElements(); if (name || parentBlockName == "TABLE") { block = dom.create(name || newBlockName); setForcedBlockAttrs(block); } else { @@ -14963,11 +15262,11 @@ caretNode = block; // Clone any parent styles if (settings.keep_styles !== false) { do { - if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U|VAR|CITE|DFN|CODE|MARK|Q|SUP|SUB|SAMP)$/.test(node.nodeName)) { + if (textInlineElements[node.nodeName]) { // Never clone a caret containers if (node.id == '_mce_caret') { continue; } @@ -15124,11 +15423,11 @@ } function getContainerBlock() { var containerBlockParent = containerBlock.parentNode; - if (containerBlockParent.nodeName == 'LI') { + if (/^(LI|DT|DD)$/.test(containerBlockParent.nodeName)) { return containerBlockParent; } return containerBlock; } @@ -15354,12 +15653,12 @@ if (containerBlockName == 'LI' && !evt.ctrlKey) { parentBlock = containerBlock; parentBlockName = containerBlockName; } - // Handle enter in LI - if (parentBlockName == 'LI') { + // Handle enter in list item + if (/^(LI|DT|DD)$/.test(parentBlockName)) { if (!newBlockName && shiftKey) { insertBr(); return; } @@ -15600,12 +15899,13 @@ * @class tinymce.EditorCommands */ define("tinymce/EditorCommands", [ "tinymce/html/Serializer", "tinymce/Env", - "tinymce/util/Tools" -], function(Serializer, Env, Tools) { + "tinymce/util/Tools", + "tinymce/dom/ElementUtils" +], function(Serializer, Env, Tools, ElementUtils) { // Added for compression purposes var each = Tools.each, extend = Tools.extend; var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode; var isGecko = Env.gecko, isIE = Env.ie; var TRUE = true, FALSE = false; @@ -15896,11 +16196,12 @@ selection.select(value); }, mceInsertContent: function(command, ui, value) { var parser, serializer, parentNode, rootNode, fragment, args; - var marker, rng, node, node2, bookmarkHtml; + var marker, rng, node, node2, bookmarkHtml, merge; + var textInlineElements = editor.schema.getTextInlineElements(); function trimOrPaddLeftRight(html) { var rng, container, offset; rng = selection.getRng(true); @@ -15926,10 +16227,41 @@ } return html; } + function markInlineFormatElements(fragment) { + if (merge) { + for (node = fragment.firstChild; node; node = node.walk(true)) { + if (textInlineElements[node.name]) { + node.attr('data-mce-new', "true"); + } + } + } + } + + function reduceInlineTextElements() { + if (merge) { + var root = editor.getBody(), elementUtils = new ElementUtils(dom); + + each(dom.select('*[data-mce-new]'), function(node) { + node.removeAttribute('data-mce-new'); + + for (var testNode = node.parentNode; testNode && testNode != root; testNode = testNode.parentNode) { + if (elementUtils.compare(testNode, node)) { + dom.remove(node, true); + } + } + }); + } + } + + if (typeof(value) != 'string') { + merge = value.merge; + value = value.content; + } + // Check for whitespace before/after value if (/^ | $/.test(value)) { value = trimOrPaddLeftRight(value); } @@ -15973,10 +16305,12 @@ // Parse the fragment within the context of the parent node var parserArgs = {context: parentNode.nodeName.toLowerCase()}; fragment = parser.parse(value, parserArgs); + markInlineFormatElements(fragment); + // Move the caret to a more suitable location node = fragment.lastChild; if (node.attr('id') == 'mce_marker') { marker = node; @@ -16039,10 +16373,12 @@ } else { dom.setOuterHTML(parentNode, value); } } + reduceInlineTextElements(); + marker = dom.get('mce_marker'); selection.scrollIntoView(marker); // Move selection before marker and remove it rng = dom.createRng(); @@ -16341,15 +16677,13 @@ * @param {Object} settings Optional settings object. */ function URI(url, settings) { var self = this, baseUri, base_url; - // Trim whitespace url = trim(url); - - // Default settings settings = self.settings = settings || {}; + baseUri = settings.base_uri; // Strange app protocol that isn't http/https or local anchor // For example: mailto,skype,tel etc. if (/^([\w\-]+):([^\/]{2})/i.test(url) || /^\s*#/.test(url)) { self.source = url; @@ -16358,20 +16692,21 @@ var isProtocolRelative = url.indexOf('//') === 0; // Absolute path with no host, fake host and protocol if (url.indexOf('/') === 0 && !isProtocolRelative) { - url = (settings.base_uri ? settings.base_uri.protocol || 'http' : 'http') + '://mce_host' + url; + url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url; } // Relative path http:// or protocol relative //path if (!/^[\w\-]*:?\/\//.test(url)) { base_url = settings.base_uri ? settings.base_uri.path : new URI(location.href).directory; if (settings.base_uri.protocol === "") { url = '//mce_host' + self.toAbsPath(base_url, url); } else { - url = ((settings.base_uri && settings.base_uri.protocol) || 'http') + '://mce_host' + self.toAbsPath(base_url, url); + url = /([^#?]*)([#?]?.*)/.exec(url); + url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(base_url, url[1]) + url[2]; } } // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something @@ -16389,11 +16724,10 @@ } self[v] = part; }); - baseUri = settings.base_uri; if (baseUri) { if (!self.protocol) { self.protocol = baseUri.protocol; } @@ -16899,11 +17233,12 @@ "tinymce/util/Tools" ], function(Tools) { var nativeEvents = Tools.makeMap( "focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " + "mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " + - "draggesture dragdrop drop drag submit", + "draggesture dragdrop drop drag submit " + + "compositionstart compositionend compositionupdate", ' ' ); function Dispatcher(settings) { var self = this, scope, bindings = {}, toggleEvent; @@ -17956,13 +18291,15 @@ "tinymce/util/Tools", "tinymce/dom/DOMUtils" ], function(Tools, DOMUtils) { "use strict"; + var count = 0; + return { id: function() { - return DOMUtils.DOM.uniqueId(); + return 'mceu_' + (count++); }, createFragment: function(html) { return DOMUtils.DOM.createFragment(html); }, @@ -21192,13 +21529,121 @@ "tinymce/ui/Resizable", "tinymce/ui/DomUtils" ], function(Panel, Movable, Resizable, DomUtils) { "use strict"; - var documentClickHandler, documentScrollHandler, visiblePanels = []; + var documentClickHandler, documentScrollHandler, windowResizeHandler, visiblePanels = []; var zOrder = [], hasModal; + function bindDocumentClickHandler() { + function isChildOf(ctrl, parent) { + while (ctrl) { + if (ctrl == parent) { + return true; + } + + ctrl = ctrl.parent(); + } + } + + if (!documentClickHandler) { + documentClickHandler = function(e) { + // Gecko fires click event and in the wrong order on Mac so lets normalize + if (e.button == 2) { + return; + } + + // Hide any float panel when a click is out side that float panel and the + // float panels direct parent for example a click on a menu button + var i = visiblePanels.length; + while (i--) { + var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target); + + if (panel.settings.autohide) { + if (clickCtrl) { + if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) { + continue; + } + } + + e = panel.fire('autohide', {target: e.target}); + if (!e.isDefaultPrevented()) { + panel.hide(); + } + } + } + }; + + DomUtils.on(document, 'click', documentClickHandler); + } + } + + function bindDocumentScrollHandler() { + if (!documentScrollHandler) { + documentScrollHandler = function() { + var i; + + i = visiblePanels.length; + while (i--) { + repositionPanel(visiblePanels[i]); + } + }; + + DomUtils.on(window, 'scroll', documentScrollHandler); + } + } + + function bindWindowResizeHandler() { + if (!windowResizeHandler) { + windowResizeHandler = function() { + FloatPanel.hideAll(); + }; + + DomUtils.on(window, 'resize', windowResizeHandler); + } + } + + /** + * Repositions the panel to the top of page if the panel is outside of the visual viewport. It will + * also reposition all child panels of the current panel. + */ + function repositionPanel(panel) { + var scrollY = DomUtils.getViewPort().y; + + function toggleFixedChildPanels(fixed, deltaY) { + var parent; + + for (var i = 0; i < visiblePanels.length; i++) { + if (visiblePanels[i] != panel) { + parent = visiblePanels[i].parent(); + + while (parent && (parent = parent.parent())) { + if (parent == panel) { + visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint(); + } + } + } + } + } + + if (panel.settings.autofix) { + if (!panel._fixed) { + panel._autoFixY = panel.layoutRect().y; + + if (panel._autoFixY < scrollY) { + panel.fixed(true).layoutRect({y: 0}).repaint(); + toggleFixedChildPanels(true, scrollY - panel._autoFixY); + } + } else { + if (panel._autoFixY > scrollY) { + panel.fixed(false).layoutRect({y: panel._autoFixY}).repaint(); + toggleFixedChildPanels(false, panel._autoFixY - scrollY); + } + } + } + } + var FloatPanel = Panel.extend({ Mixins: [Movable, Resizable], /** * Constructs a new control instance with the specified settings. @@ -21236,110 +21681,25 @@ } FloatPanel.currentZIndex = zIndex; } - function isChildOf(ctrl, parent) { - while (ctrl) { - if (ctrl == parent) { - return true; - } - - ctrl = ctrl.parent(); - } - } - - /** - * Repositions the panel to the top of page if the panel is outside of the visual viewport. It will - * also reposition all child panels of the current panel. - */ - function repositionPanel(panel) { - var scrollY = DomUtils.getViewPort().y; - - function toggleFixedChildPanels(fixed, deltaY) { - var parent; - - for (var i = 0; i < visiblePanels.length; i++) { - if (visiblePanels[i] != panel) { - parent = visiblePanels[i].parent(); - - while (parent && (parent = parent.parent())) { - if (parent == panel) { - visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint(); - } - } - } - } - } - - if (panel.settings.autofix) { - if (!panel._fixed) { - panel._autoFixY = panel.layoutRect().y; - - if (panel._autoFixY < scrollY) { - panel.fixed(true).layoutRect({y: 0}).repaint(); - toggleFixedChildPanels(true, scrollY - panel._autoFixY); - } - } else { - if (panel._autoFixY > scrollY) { - panel.fixed(false).layoutRect({y: panel._autoFixY}).repaint(); - toggleFixedChildPanels(false, panel._autoFixY - scrollY); - } - } - } - } - self._super(settings); self._eventsRoot = self; self.addClass('floatpanel'); // Hide floatpanes on click out side the root button if (settings.autohide) { - if (!documentClickHandler) { - documentClickHandler = function(e) { - // Hide any float panel when a click is out side that float panel and the - // float panels direct parent for example a click on a menu button - var i = visiblePanels.length; - while (i--) { - var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target); - - if (panel.settings.autohide) { - if (clickCtrl) { - if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) { - continue; - } - } - - e = panel.fire('autohide', {target: e.target}); - if (!e.isDefaultPrevented()) { - panel.hide(); - } - } - } - }; - - DomUtils.on(document, 'click', documentClickHandler); - } - + bindDocumentClickHandler(); + bindWindowResizeHandler(); visiblePanels.push(self); } if (settings.autofix) { - if (!documentScrollHandler) { - documentScrollHandler = function() { - var i; + bindDocumentScrollHandler(); - i = visiblePanels.length; - while (i--) { - repositionPanel(visiblePanels[i]); - } - }; - - DomUtils.on(window, 'scroll', documentScrollHandler); - } - self.on('move', function() { repositionPanel(this); }); } @@ -21450,11 +21810,12 @@ removeVisiblePanel(this); return this._super(); }, /** - * Hides all visible the float panels. + * Hide all visible float panels with he autohide setting enabled. This is for + * manually hiding floating menus or panels. * * @method hideAll */ hideAll: function() { FloatPanel.hideAll(); @@ -21493,11 +21854,12 @@ return self._super(); } }); /** - * Hides all visible the float panels. + * Hide all visible float panels with he autohide setting enabled. This is for + * manually hiding floating menus or panels. * * @static * @method hideAll */ FloatPanel.hideAll = function() { @@ -23899,12 +24261,17 @@ case 'shift': shortcut[value] = true; break; default: - shortcut.charCode = value.charCodeAt(0); - shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0); + // Allow numeric keycodes like ctrl+219 for ctrl+[ + if (/^[0-9]{2,}$/.test(value)) { + shortcut.keyCode = parseInt(value, 10); + } else { + shortcut.charCode = value.charCodeAt(0); + shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0); + } } }); shortcuts[ (shortcut.ctrl ? 'ctrl' : '') + ',' + @@ -24707,11 +25074,18 @@ internalName = 'data-mce-' + name; // Add internal attribute if we need to we don't on a refresh of the document if (!node.attributes.map[internalName]) { if (name === "style") { - node.attr(internalName, dom.serializeStyle(dom.parseStyle(value), node.name)); + value = dom.serializeStyle(dom.parseStyle(value), node.name); + + if (!value.length) { + value = null; + } + + node.attr(internalName, value); + node.attr(name, value); } else if (name === "tabindex") { node.attr(internalName, value); node.attr(name, null); } else { node.attr(internalName, self.convertURL(value, name, node.name)); @@ -24724,11 +25098,11 @@ self.parser.addNodeFilter('script', function(nodes) { var i = nodes.length, node; while (i--) { node = nodes[i]; - node.attr('type', 'mce-' + (node.attr('type') || 'text/javascript')); + node.attr('type', 'mce-' + (node.attr('type') || 'no/type')); } }); self.parser.addNodeFilter('#cdata', function(nodes) { var i = nodes.length, node; @@ -25338,12 +25712,22 @@ self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); return true; } // Browser commands - self.getDoc().execCommand(cmd, ui, value); - self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); + try { + state = self.getDoc().execCommand(cmd, ui, value); + } catch (ex) { + // Ignore old IE errors + } + + if (state) { + self.fire('ExecCommand', {command: cmd, ui: ui, value: value}); + return true; + } + + return false; }, /** * Returns a command specific state, for example if bold is enabled or not. * @@ -25361,12 +25745,12 @@ // Registred commands if ((queryItem = self.queryStateCommands[cmd])) { returnVal = queryItem.func.call(queryItem.scope); - // Fall though on true - if (returnVal !== true) { + // Fall though on non boolean returns + if (returnVal === true || returnVal === false) { return returnVal; } } // Editor commands @@ -25452,12 +25836,10 @@ */ hide: function() { var self = this, doc = self.getDoc(); if (!self.hidden) { - self.hidden = true; - // Fixed bug where IE has a blinking cursor left from the editor if (ie && doc && !self.inline) { doc.execCommand('SelectAll'); } @@ -25474,10 +25856,11 @@ } else { DOM.hide(self.getContainer()); DOM.setStyle(self.id, 'display', self.orgDisplay); } + self.hidden = true; self.fire('hide'); } }, /** @@ -25737,12 +26120,17 @@ /** * Inserts content at caret position. * * @method insertContent * @param {String} content Content to insert. + * @param {Object} args Optional args to pass to insert call. */ - insertContent: function(content) { + insertContent: function(content, args) { + if (args) { + content = extend({content: content}, args); + } + this.execCommand('mceInsertContent', false, content); }, /** * Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. @@ -26493,19 +26881,19 @@ * Minor version of TinyMCE build. * * @property minorVersion * @type String */ - minorVersion : '0.26', + minorVersion : '0.28', /** * Release date of TinyMCE build. * * @property releaseDate * @type String */ - releaseDate: '2014-05-06', + releaseDate: '2014-05-27', /** * Collection of editor instances. * * @property editors @@ -26537,13 +26925,20 @@ setup: function() { var self = this, baseURL, documentBaseURL, suffix = "", preInit, src; // Get base URL for the current document - documentBaseURL = document.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); - if (!/[\/\\]$/.test(documentBaseURL)) { - documentBaseURL += '/'; + documentBaseURL = document.location.href; + + // Check if the URL is a document based format like: http://site/dir/file + // leave other formats like applewebdata://... intact + if (/^[^:]+:\/\/[^\/]+\//.test(documentBaseURL)) { + documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); + + if (!/[\/\\]$/.test(documentBaseURL)) { + documentBaseURL += '/'; + } } // If tinymce is defined and has a base use that or use the old tinyMCEPreInit preInit = window.tinymce || window.tinyMCEPreInit; if (preInit) { @@ -29473,17 +29868,23 @@ * Recalcs label widths. * * @private */ recalcLabels: function() { - var self = this, maxLabelWidth = 0, labels = [], i, labelGap; + var self = this, maxLabelWidth = 0, labels = [], i, labelGap, items; if (self.settings.labelGapCalc === false) { return; } - self.items().filter('formitem').each(function(item) { + if (self.settings.labelGapCalc == "children") { + items = self.find('formitem'); + } else { + items = self.items(); + } + + items.filter('formitem').each(function(item) { var labelCtrl = item.items()[0], labelWidth = labelCtrl.getEl().clientWidth; maxLabelWidth = labelWidth > maxLabelWidth ? labelWidth : maxLabelWidth; labels.push(labelCtrl); }); @@ -29620,28 +30021,34 @@ * * @class tinymce.ui.FilePicker * @extends tinymce.ui.ComboBox */ define("tinymce/ui/FilePicker", [ - "tinymce/ui/ComboBox" -], function(ComboBox) { + "tinymce/ui/ComboBox", + "tinymce/util/Tools" +], function(ComboBox, Tools) { "use strict"; return ComboBox.extend({ /** * Constructs a new control instance with the specified settings. * * @constructor * @param {Object} settings Name/value object with settings. */ init: function(settings) { - var self = this, editor = tinymce.activeEditor, fileBrowserCallback; + var self = this, editor = tinymce.activeEditor, fileBrowserCallback, fileBrowserCallbackTypes; settings.spellcheck = false; + fileBrowserCallbackTypes = editor.settings.file_browser_callback_types; + if (fileBrowserCallbackTypes) { + fileBrowserCallbackTypes = Tools.makeMap(fileBrowserCallbackTypes, /[, ]/); + } + fileBrowserCallback = editor.settings.file_browser_callback; - if (fileBrowserCallback) { + if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[settings.filetype])) { settings.icon = 'browse'; settings.onaction = function() { fileBrowserCallback( self.getEl('inp').id, @@ -30153,10 +30560,11 @@ format.name = formatName; newFormats.push(format); } menuItem.format = formatName; + menuItem.cmd = format.cmd; } menu.push(menuItem); }); @@ -30199,24 +30607,36 @@ return editor.formatter.getCssText(this.settings.format); } }, onPostRender: function() { - var self = this, formatName = this.settings.format; + var self = this; - if (formatName) { - self.parent().on('show', function() { + self.parent().on('show', function() { + var formatName, command; + + formatName = self.settings.format; + if (formatName) { self.disabled(!editor.formatter.canApply(formatName)); self.active(editor.formatter.match(formatName)); - }); - } + } + + command = self.settings.cmd; + if (command) { + self.active(editor.queryCommandState(command)); + } + }); }, onclick: function() { if (this.settings.format) { toggleFormat(this.settings.format); } + + if (this.settings.cmd) { + editor.execCommand(this.settings.cmd); + } } } }; } @@ -30307,36 +30727,27 @@ } } }); }); - function hasUndo() { - return editor.undoManager ? editor.undoManager.hasUndo() : false; - } + function toggleUndoRedoState(type) { + return function() { + var self = this; - function hasRedo() { - return editor.undoManager ? editor.undoManager.hasRedo() : false; - } + type = type == 'redo' ? 'hasRedo' : 'hasUndo'; - function toggleUndoState() { - var self = this; + function checkState() { + return editor.undoManager ? editor.undoManager[type]() : false; + } - self.disabled(!hasUndo()); - editor.on('Undo Redo AddUndo TypingUndo', function() { - self.disabled(!hasUndo()); - }); + self.disabled(!checkState()); + editor.on('Undo Redo AddUndo TypingUndo ClearUndos', function() { + self.disabled(!checkState()); + }); + }; } - function toggleRedoState() { - var self = this; - - self.disabled(!hasRedo()); - editor.on('Undo Redo AddUndo TypingUndo', function() { - self.disabled(!hasRedo()); - }); - } - function toggleVisualAidState() { var self = this; editor.on('VisualAid', function(e) { self.active(e.hasVisual); @@ -30345,17 +30756,17 @@ self.active(editor.hasVisual); } editor.addButton('undo', { tooltip: 'Undo', - onPostRender: toggleUndoState, + onPostRender: toggleUndoRedoState('undo'), cmd: 'undo' }); editor.addButton('redo', { tooltip: 'Redo', - onPostRender: toggleRedoState, + onPostRender: toggleUndoRedoState('redo'), cmd: 'redo' }); editor.addMenuItem('newdocument', { text: 'New document', @@ -30366,19 +30777,19 @@ editor.addMenuItem('undo', { text: 'Undo', icon: 'undo', shortcut: 'Ctrl+Z', - onPostRender: toggleUndoState, + onPostRender: toggleUndoRedoState('undo'), cmd: 'undo' }); editor.addMenuItem('redo', { text: 'Redo', icon: 'redo', shortcut: 'Ctrl+Y', - onPostRender: toggleRedoState, + onPostRender: toggleUndoRedoState('redo'), cmd: 'redo' }); editor.addMenuItem('visualaid', { text: 'Visual aids', @@ -30509,11 +30920,18 @@ editor.addButton('fontsizeselect', function() { var items = [], defaultFontsizeFormats = '8pt 10pt 12pt 14pt 18pt 24pt 36pt'; var fontsize_formats = editor.settings.fontsize_formats || defaultFontsizeFormats; each(fontsize_formats.split(' '), function(item) { - items.push({text: item, value: item}); + var text = item, value = item; + // Allow text=value font sizes. + var values = item.split('='); + if (values.length > 1) { + text = values[0]; + value = values[1]; + } + items.push({text: text, value: value}); }); return { type: 'listbox', text: 'Font Sizes', @@ -31351,24 +31769,34 @@ * @constructor * @param {Object} settings Name/value object with settings. * @setting {Array} values Array with values to add to list box. */ init: function(settings) { - var self = this, values, i, selected, selectedText, lastItemCtrl; + var self = this, values, selected, selectedText, lastItemCtrl; - self._values = values = settings.values; - if (values) { - for (i = 0; i < values.length; i++) { - selected = values[i].selected || settings.value === values[i].value; + function setSelected(menuValues) { + // Try to find a selected value + for (var i = 0; i < menuValues.length; i++) { + selected = menuValues[i].selected || settings.value === menuValues[i].value; if (selected) { - selectedText = selectedText || values[i].text; - self._value = values[i].value; + selectedText = selectedText || menuValues[i].text; + self._value = menuValues[i].value; break; } + + // If the value has a submenu, try to find the selected values in that menu + if (menuValues[i].menu) { + setSelected(menuValues[i].menu); + } } + } + self._values = values = settings.values; + if (values) { + setSelected(values); + // Default with first item if (!selected && values.length > 0) { selectedText = values[0].text; self._value = values[0].value; } @@ -31404,11 +31832,11 @@ * @method value * @param {String} [value] Value to be set. * @return {Boolean/tinymce.ui.ListBox} Value or self if it's a set operation. */ value: function(value) { - var self = this, active, selectedText, menu, i; + var self = this, active, selectedText, menu; function activateByValue(menu, value) { menu.items().each(function(ctrl) { active = ctrl.value() === value; @@ -31422,24 +31850,32 @@ activateByValue(ctrl.menu, value); } }); } + function setActiveValues(menuValues) { + for (var i = 0; i < menuValues.length; i++) { + active = menuValues[i].value == value; + + if (active) { + selectedText = selectedText || menuValues[i].text; + } + + menuValues[i].active = active; + + if (menuValues[i].menu) { + setActiveValues(menuValues[i].menu); + } + } + } + if (typeof(value) != "undefined") { if (self.menu) { activateByValue(self.menu, value); } else { menu = self.settings.menu; - for (i = 0; i < menu.length; i++) { - active = menu[i].value == value; - - if (active) { - selectedText = selectedText || menu[i].text; - } - - menu[i].active = active; - } + setActiveValues(menu); } self.text(selectedText || this.settings.text); } @@ -31577,16 +32013,20 @@ menu.itemDefaults = parent.settings.itemDefaults; } menu = self.menu = Factory.create(menu).parent(self).renderTo(); menu.reflow(); - menu.fire('show'); menu.on('cancel', function(e) { e.stopPropagation(); self.focus(); menu.hide(); }); + menu.on('show hide', function(e) { + e.control.items().each(function(ctrl) { + ctrl.active(ctrl.settings.selected); + }); + }).fire('show'); menu.on('hide', function(e) { if (e.control === menu) { self.removeClass('selected'); } @@ -32679,7 +33119,7 @@ return self; }; }; }); -expose(["tinymce/dom/Sizzle","tinymce/html/Styles","tinymce/dom/EventUtils","tinymce/dom/TreeWalker","tinymce/util/Tools","tinymce/dom/Range","tinymce/html/Entities","tinymce/Env","tinymce/dom/StyleSheetLoader","tinymce/dom/DOMUtils","tinymce/dom/ScriptLoader","tinymce/AddOnManager","tinymce/html/Node","tinymce/html/Schema","tinymce/html/SaxParser","tinymce/html/DomParser","tinymce/html/Writer","tinymce/html/Serializer","tinymce/dom/Serializer","tinymce/dom/TridentSelection","tinymce/util/VK","tinymce/dom/ControlSelection","tinymce/dom/RangeUtils","tinymce/dom/Selection","tinymce/fmt/Preview","tinymce/Formatter","tinymce/UndoManager","tinymce/EnterKey","tinymce/ForceBlocks","tinymce/EditorCommands","tinymce/util/URI","tinymce/util/Class","tinymce/util/EventDispatcher","tinymce/ui/Selector","tinymce/ui/Collection","tinymce/ui/DomUtils","tinymce/ui/Control","tinymce/ui/Factory","tinymce/ui/KeyboardNavigation","tinymce/ui/Container","tinymce/ui/DragHelper","tinymce/ui/Scrollable","tinymce/ui/Panel","tinymce/ui/Movable","tinymce/ui/Resizable","tinymce/ui/FloatPanel","tinymce/ui/Window","tinymce/ui/MessageBox","tinymce/WindowManager","tinymce/util/Quirks","tinymce/util/Observable","tinymce/EditorObservable","tinymce/Shortcuts","tinymce/Editor","tinymce/util/I18n","tinymce/FocusManager","tinymce/EditorManager","tinymce/LegacyInput","tinymce/util/XHR","tinymce/util/JSON","tinymce/util/JSONRequest","tinymce/util/JSONP","tinymce/util/LocalStorage","tinymce/Compat","tinymce/ui/Layout","tinymce/ui/AbsoluteLayout","tinymce/ui/Tooltip","tinymce/ui/Widget","tinymce/ui/Button","tinymce/ui/ButtonGroup","tinymce/ui/Checkbox","tinymce/ui/PanelButton","tinymce/ui/ColorButton","tinymce/ui/ComboBox","tinymce/ui/Path","tinymce/ui/ElementPath","tinymce/ui/FormItem","tinymce/ui/Form","tinymce/ui/FieldSet","tinymce/ui/FilePicker","tinymce/ui/FitLayout","tinymce/ui/FlexLayout","tinymce/ui/FlowLayout","tinymce/ui/FormatControls","tinymce/ui/GridLayout","tinymce/ui/Iframe","tinymce/ui/Label","tinymce/ui/Toolbar","tinymce/ui/MenuBar","tinymce/ui/MenuButton","tinymce/ui/ListBox","tinymce/ui/MenuItem","tinymce/ui/Menu","tinymce/ui/Radio","tinymce/ui/ResizeHandle","tinymce/ui/Spacer","tinymce/ui/SplitButton","tinymce/ui/StackLayout","tinymce/ui/TabPanel","tinymce/ui/TextBox","tinymce/ui/Throbber"]); +expose(["tinymce/dom/Sizzle","tinymce/html/Styles","tinymce/dom/EventUtils","tinymce/dom/TreeWalker","tinymce/util/Tools","tinymce/dom/Range","tinymce/html/Entities","tinymce/Env","tinymce/dom/DOMUtils","tinymce/dom/ScriptLoader","tinymce/AddOnManager","tinymce/html/Node","tinymce/html/Schema","tinymce/html/SaxParser","tinymce/html/DomParser","tinymce/html/Writer","tinymce/html/Serializer","tinymce/dom/Serializer","tinymce/dom/TridentSelection","tinymce/util/VK","tinymce/dom/ControlSelection","tinymce/dom/BookmarkManager","tinymce/dom/Selection","tinymce/dom/ElementUtils","tinymce/Formatter","tinymce/UndoManager","tinymce/EnterKey","tinymce/ForceBlocks","tinymce/EditorCommands","tinymce/util/URI","tinymce/util/Class","tinymce/util/EventDispatcher","tinymce/ui/Selector","tinymce/ui/Collection","tinymce/ui/DomUtils","tinymce/ui/Control","tinymce/ui/Factory","tinymce/ui/KeyboardNavigation","tinymce/ui/Container","tinymce/ui/DragHelper","tinymce/ui/Scrollable","tinymce/ui/Panel","tinymce/ui/Movable","tinymce/ui/Resizable","tinymce/ui/FloatPanel","tinymce/ui/Window","tinymce/ui/MessageBox","tinymce/WindowManager","tinymce/util/Quirks","tinymce/util/Observable","tinymce/EditorObservable","tinymce/Shortcuts","tinymce/Editor","tinymce/util/I18n","tinymce/FocusManager","tinymce/EditorManager","tinymce/LegacyInput","tinymce/util/XHR","tinymce/util/JSON","tinymce/util/JSONRequest","tinymce/util/JSONP","tinymce/util/LocalStorage","tinymce/Compat","tinymce/ui/Layout","tinymce/ui/AbsoluteLayout","tinymce/ui/Tooltip","tinymce/ui/Widget","tinymce/ui/Button","tinymce/ui/ButtonGroup","tinymce/ui/Checkbox","tinymce/ui/PanelButton","tinymce/ui/ColorButton","tinymce/ui/ComboBox","tinymce/ui/Path","tinymce/ui/ElementPath","tinymce/ui/FormItem","tinymce/ui/Form","tinymce/ui/FieldSet","tinymce/ui/FilePicker","tinymce/ui/FitLayout","tinymce/ui/FlexLayout","tinymce/ui/FlowLayout","tinymce/ui/FormatControls","tinymce/ui/GridLayout","tinymce/ui/Iframe","tinymce/ui/Label","tinymce/ui/Toolbar","tinymce/ui/MenuBar","tinymce/ui/MenuButton","tinymce/ui/ListBox","tinymce/ui/MenuItem","tinymce/ui/Menu","tinymce/ui/Radio","tinymce/ui/ResizeHandle","tinymce/ui/Spacer","tinymce/ui/SplitButton","tinymce/ui/StackLayout","tinymce/ui/TabPanel","tinymce/ui/TextBox","tinymce/ui/Throbber"]); })(this); \ No newline at end of file